Skip to content

Commit

Permalink
Allow creating transcription passages without intervals (#297)
Browse files Browse the repository at this point in the history
  • Loading branch information
martinmr authored Mar 31, 2024
1 parent f3278c0 commit bca5774
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 25 deletions.
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ anyhow = "1.0.75"
chrono = { version = "0.4.31", features = ["serde"] }
derive_builder = "0.12.0"
fs_extra = "1.3.0"
git2 = "0.18.0"
git2 = "0.18.3"
indoc = "2.0.4"
lazy_static = "1.4.0"
mantra-miner = "0.1.1"
Expand All @@ -29,13 +29,15 @@ rusqlite_migration = "1.0.2"
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.107"
strum = { version = "0.25.0", features = ["derive"] }
tantivy = "0.21.0"
tantivy = "0.21.1"
tempfile = "3.8.0"
thiserror = "1.0.48"
typeshare = "1.0.1"
url = "2.4.1"
ustr = { version = "0.9.0", features = ["serialization"] }
walkdir = "2.4.0"
# Fix the dependency to work around a compile issue. See: https://github.com/gyscos/zstd-rs/issues/270#issuecomment-2026322823.
zstd-sys = "=2.0.9"

[profile.bench]
debug = true
126 changes: 111 additions & 15 deletions src/data/course_generator/transcription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ pub enum TranscriptionAsset {
#[serde(default)]
duration: Option<String>,

/// A link to an external copy (e.g. youtube video) of the track.
/// A link to an external copy (e.g., YouTube link) of the track.
#[serde(default)]
external_link: Option<TranscriptionLink>,
},
}
//>@transcription-asset

impl TranscriptionAsset {
/// Returns the short ID of the asset, which wil be used to generate the exercise IDs.
/// Returns the short ID of the asset, which will be used to generate the exercise IDs.
#[must_use]
pub fn short_id(&self) -> &str {
match self {
Expand All @@ -93,9 +93,13 @@ pub struct TranscriptionPassages {
/// The asset to transcribe.
pub asset: TranscriptionAsset,

/// The ranges `[start, end]` of the passages to transcribe. Stored as a map maping a unique ID
/// The ranges `[start, end]` of the passages to transcribe. Stored as a map mapping a unique ID
/// to the start and end of the passage. A map is used instead of a list because reordering the
/// passages would change the resulting exercise IDs.
///
/// If the map is empty, one passage is assumed to cover the entire asset and the ID for the
/// exercises will not include a passage ID.
#[serde(default)]
pub intervals: HashMap<usize, (String, String)>,
}
//>@transcription-passages
Expand Down Expand Up @@ -192,7 +196,7 @@ pub struct TranscriptionConfig {
pub inlined_passages: Vec<TranscriptionPassages>,

/// If true, the course will skip creating the singing lesson. This is useful when the course
/// contains backing tracks that have not melodies, for example. Both the singing and the
/// contains backing tracks that have no melodies, for example. Both the singing and the
/// advanced singing lessons will be skipped. Because other transcription courses that depend on
/// this lesson will use the singing lesson to create the dependency, the lesson will be
/// created, but will be empty.
Expand All @@ -209,8 +213,11 @@ pub struct TranscriptionConfig {

impl TranscriptionConfig {
/// Returns the ID for a given exercise given the lesson ID and the exercise index.
fn exercise_id(lesson_id: Ustr, asset_id: &str, passage_id: usize) -> Ustr {
Ustr::from(&format!("{lesson_id}::{asset_id}::{passage_id}"))
fn exercise_id(lesson_id: Ustr, asset_id: &str, passage_id: Option<usize>) -> Ustr {
match passage_id {
Some(passage_id) => Ustr::from(&format!("{lesson_id}::{asset_id}::{passage_id}")),
None => Ustr::from(&format!("{lesson_id}::{asset_id}")),
}
}

/// Returns the ID of the singing lesson for the given course.
Expand All @@ -224,11 +231,30 @@ impl TranscriptionConfig {
lesson_id: Ustr,
passages: &TranscriptionPassages,
) -> Vec<ExerciseManifest> {
// Generate the default exercise if no passages are provided.
if passages.intervals.is_empty() {
return vec![ExerciseManifest {
id: Self::exercise_id(lesson_id, passages.asset.short_id(), None),
lesson_id,
course_id: course_manifest.id,
name: format!("{} - Singing", course_manifest.name),
description: None,
exercise_type: ExerciseType::Procedural,
exercise_asset: passages.generate_exercise_asset(
SINGING_DESCRIPTION,
"Start of passage",
"End of passage",
None,
),
}];
}

// Generate an exercise for each passage.
passages
.intervals
.iter()
.map(|(passage_id, (start, end))| ExerciseManifest {
id: Self::exercise_id(lesson_id, passages.asset.short_id(), *passage_id),
id: Self::exercise_id(lesson_id, passages.asset.short_id(), Some(*passage_id)),
lesson_id,
course_id: course_manifest.id,
name: format!("{} - Singing", course_manifest.name),
Expand Down Expand Up @@ -303,11 +329,30 @@ impl TranscriptionConfig {
lesson_id: Ustr,
passages: &TranscriptionPassages,
) -> Vec<ExerciseManifest> {
// Generate the default exercise if no passages are provided.
if passages.intervals.is_empty() {
return vec![ExerciseManifest {
id: Self::exercise_id(lesson_id, passages.asset.short_id(), None),
lesson_id,
course_id: course_manifest.id,
name: format!("{} - Advanced Singing", course_manifest.name),
description: None,
exercise_type: ExerciseType::Procedural,
exercise_asset: passages.generate_exercise_asset(
ADVANCED_SINGING_DESCRIPTION,
"Start of passage",
"End of passage",
None,
),
}];
}

// Generate an exercise for each passage.
passages
.intervals
.iter()
.map(|(passage_id, (start, end))| ExerciseManifest {
id: Self::exercise_id(lesson_id, passages.asset.short_id(), *passage_id),
id: Self::exercise_id(lesson_id, passages.asset.short_id(), Some(*passage_id)),
lesson_id,
course_id: course_manifest.id,
name: format!("{} - Advanced Singing", course_manifest.name),
Expand Down Expand Up @@ -383,11 +428,33 @@ impl TranscriptionConfig {
passages: &TranscriptionPassages,
instrument: &Instrument,
) -> Vec<ExerciseManifest> {
// Generate the default exercise if no passages are provided.
if passages.intervals.is_empty() {
return vec![ExerciseManifest {
id: Self::exercise_id(lesson_id, passages.asset.short_id(), None),
lesson_id,
course_id: course_manifest.id,
name: format!(
"{} - Transcription - {}",
course_manifest.name, instrument.name
),
description: None,
exercise_type: ExerciseType::Procedural,
exercise_asset: passages.generate_exercise_asset(
TRANSCRIPTION_DESCRIPTION,
"Start of passage",
"End of passage",
Some(instrument),
),
}];
}

// Generate an exercise for each passage.
passages
.intervals
.iter()
.map(|(passage_id, (start, end))| ExerciseManifest {
id: Self::exercise_id(lesson_id, passages.asset.short_id(), *passage_id),
id: Self::exercise_id(lesson_id, passages.asset.short_id(), Some(*passage_id)),
lesson_id,
course_id: course_manifest.id,
name: format!(
Expand Down Expand Up @@ -491,11 +558,33 @@ impl TranscriptionConfig {
passages: &TranscriptionPassages,
insturment: &Instrument,
) -> Vec<ExerciseManifest> {
// Generate the default exercise if no passages are provided.
if passages.intervals.is_empty() {
return vec![ExerciseManifest {
id: Self::exercise_id(lesson_id, passages.asset.short_id(), None),
lesson_id,
course_id: course_manifest.id,
name: format!(
"{} - Advanced Transcription - {}",
course_manifest.name, insturment.name
),
description: None,
exercise_type: ExerciseType::Procedural,
exercise_asset: passages.generate_exercise_asset(
ADVANCED_TRANSCRIPTION_DESCRIPTION,
"Start of passage",
"End of passage",
Some(insturment),
),
}];
}

// Generate an exercise for each passage.
passages
.intervals
.iter()
.map(|(passage_id, (start, end))| ExerciseManifest {
id: Self::exercise_id(lesson_id, passages.asset.short_id(), *passage_id),
id: Self::exercise_id(lesson_id, passages.asset.short_id(), Some(*passage_id)),
lesson_id,
course_id: course_manifest.id,
name: format!(
Expand Down Expand Up @@ -772,13 +861,20 @@ mod test {
/// Verifies generating IDs for the exercises in the course.
#[test]
fn exercise_id() {
// Generate the ID for an exercise with a passage ID.
let lesson_id = Ustr::from("lesson_id");
let asset_id = "asset_id";
let passage_id = 2;
assert_eq!(
TranscriptionConfig::exercise_id(lesson_id, &asset_id, passage_id),
TranscriptionConfig::exercise_id(lesson_id, &asset_id, Some(passage_id)),
Ustr::from("lesson_id::asset_id::2")
);

// Generate the ID for an exercise with the default passage.
assert_eq!(
TranscriptionConfig::exercise_id(lesson_id, &asset_id, None),
Ustr::from("lesson_id::asset_id")
);
}

/// Verifies generating the lesson ID for the singing lesson.
Expand Down Expand Up @@ -895,7 +991,7 @@ mod test {
/// Verifies creating the course based on the passages in the passage directory.
#[test]
fn open_passage_directory() -> Result<()> {
// Create the passages directory.
// Create the `passages` directory.
let temp_dir = tempfile::tempdir()?;
let passages_dir = temp_dir.path().join("passages");
fs::create_dir(&passages_dir)?;
Expand Down Expand Up @@ -945,7 +1041,7 @@ mod test {
/// Verifies creating the course based on an empty passage directory.
#[test]
fn open_passage_directory_empty() -> Result<()> {
// Create the passages directory.
// Create the `passages` directory.
let temp_dir = tempfile::tempdir()?;

// Open the empty passages directory and verify there are no passages.
Expand All @@ -965,7 +1061,7 @@ mod test {
/// Verifies that opening the passage directory fails if there are passages with duplicate IDs.
#[test]
fn open_passage_directory_duplicate() -> Result<()> {
// Create the passages directory.
// Create the `passages` directory.
let temp_dir = tempfile::tempdir()?;
let passages_dir = temp_dir.path().join("passages");
fs::create_dir(&passages_dir)?;
Expand Down Expand Up @@ -1014,7 +1110,7 @@ mod test {
/// Verifies that opening the passage directory fails if the directory does not exist.
#[test]
fn open_passage_directory_bad_directory() -> Result<()> {
// Create the course directory but not the passages directory.
// Create the course directory but not the `passages` directory.
let temp_dir = tempfile::tempdir()?;

// Open the passages directory and verify the method fails.
Expand Down
5 changes: 2 additions & 3 deletions src/scheduler/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,11 @@ impl ScoreCache {
// All the superseding units must have a score equal or greater than the superseding score.
let scores = superseding_ids
.iter()
.map(|id| self.get_unit_score(*id).unwrap_or_default())
.filter(Option::is_some)
.filter_map(|id| self.get_unit_score(*id).unwrap_or_default())
.collect::<Vec<_>>();
scores
.iter()
.all(|score| score.unwrap() >= self.data.options.superseding_score)
.all(|score| *score >= self.data.options.superseding_score)
}

/// Recursively check if each superseding unit has itself been superseded by another unit and
Expand Down
17 changes: 12 additions & 5 deletions tests/transcription_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ lazy_static! {
};
}

/// Returns a course builder with an transcription generator.
/// Returns a course builder with a transcription generator.
fn transcription_builder(
course_id: Ustr,
course_index: usize,
Expand All @@ -52,12 +52,12 @@ fn transcription_builder(
skip_singing_lessons: bool,
skip_advanced_lessons: bool,
) -> CourseBuilder {
// Create the passages for the course. Half of the passages will be stored in the passages
// Create the passages for the course. Half of the passages will be stored in the `passages`
// directory, and the other half will be inlined in the course manifest.
let mut asset_builders = Vec::new();
let mut inlined_passages = Vec::new();
for i in 0..num_passages {
// Create the passages.
// Create the passages. Create half of them with explicit intervals and half without.
let passages = TranscriptionPassages {
asset: TranscriptionAsset::Track {
short_id: format!("passages_{}", i),
Expand All @@ -67,7 +67,14 @@ fn transcription_builder(
duration: None,
external_link: None,
},
intervals: HashMap::from([(0, ("0:00".to_string(), "0:01".to_string()))]),
intervals: if i % 2 == 0 {
HashMap::from([
(0, ("0:00".to_string(), "0:01".to_string())),
(1, ("0:05".to_string(), "0:10".to_string())),
])
} else {
HashMap::new()
},
};

// In odd iterations, add the passage to the inlined passages.
Expand All @@ -76,7 +83,7 @@ fn transcription_builder(
continue;
}

// In even iterations, write the passage to the passages directory.
// In even iterations, write the passage to the `passages` directory.
let passage_path = format!("passages/passages_{}.json", i);
asset_builders.push(AssetBuilder {
file_name: passage_path.clone(),
Expand Down

0 comments on commit bca5774

Please sign in to comment.