Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 72 additions & 5 deletions crates/project/src/caption_timing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,12 @@ pub fn synchronize_captions(project: &mut ProjectConfiguration, recording_durati
if !captions.source_timed {
let holds = timeline.hold_windows();
let mappings = build_source_to_edited_mappings(timeline, recording_durations);
let to_source = |output: f64| -> Option<f32> {
let output_end = timeline.duration() as f32;
let to_source = |output: f32| -> Option<f32> {
if output == output_end {
return mappings.last().map(|mapping| mapping.source_end as f32);
}
let output = f64::from(output);
let effective = output
- holds
.iter()
Expand All @@ -388,16 +393,16 @@ pub fn synchronize_captions(project: &mut ProjectConfiguration, recording_durati
.segments
.iter()
.filter_map(|caption| {
let start = to_source(f64::from(caption.start))?;
let end = to_source(f64::from(caption.end))?;
let start = to_source(caption.start)?;
let end = to_source(caption.end)?;
let words = caption
.words
.iter()
.filter_map(|word| {
Some(CaptionWord {
text: word.text.clone(),
start: to_source(f64::from(word.start))?,
end: to_source(f64::from(word.end))?,
start: to_source(word.start)?,
end: to_source(word.end)?,
})
})
.collect();
Expand Down Expand Up @@ -466,6 +471,68 @@ mod tests {
assert_eq!(track[0].end, 10.0);
}

#[test]
fn legacy_caption_migration_preserves_rounded_timeline_end() {
let durations = [41.0 / 15.0, 82.0 / 15.0];
for (timescale, hold_duration) in [
(1.0, 0.0),
(2.0, 0.0),
(0.5, 0.0),
(1.0, 0.1),
(1.0, 10000.1),
] {
let mut timeline: TimelineConfiguration = serde_json::from_value(serde_json::json!({
"segments": [
{"recordingSegment": 0, "start": 0.0, "end": durations[0], "timescale": timescale},
{"recordingSegment": 1, "start": 0.0, "end": durations[1], "timescale": timescale}
],
"transitions": [{"segmentIndex": 1, "type": "cross-fade", "duration": 0.2}],
"zoomSegments": []
}))
.unwrap();
if hold_duration > 0.0 {
timeline.text_segments.push(
serde_json::from_value(serde_json::json!({
"start": 1.0, "end": 1.0 + hold_duration, "layout": "fullscreen"
}))
.unwrap(),
);
}
let end = timeline.duration() as f32;
let mut project = project(false, end - 0.5, end);
project.timeline = Some(timeline);

synchronize_captions(&mut project, &durations);

let captions = project.captions.as_ref().unwrap();
assert_eq!(captions.segments.len(), 1, "{timescale}, {hold_duration}");
let source_end = durations.iter().sum::<f64>() as f32;
assert_eq!(captions.segments[0].end, source_end);
assert_eq!(captions.segments[0].words[0].end, source_end);
let track = &project.timeline.as_ref().unwrap().caption_segments;
assert_eq!(track.len(), 1);
assert!((track[0].end - f64::from(end)).abs() <= f64::from(end * f32::EPSILON));
let saved = serde_json::to_string(&project).unwrap();
synchronize_captions(&mut project, &durations);
assert_eq!(serde_json::to_string(&project).unwrap(), saved);
}
}

#[test]
fn legacy_caption_migration_rejects_times_beyond_rounded_timeline_end() {
let mut project = project(false, 8.0, 9.0_f32.next_up());
synchronize_captions(&mut project, &[10.0]);
assert!(project.captions.as_ref().unwrap().segments.is_empty());
assert!(
project
.timeline
.as_ref()
.unwrap()
.caption_segments
.is_empty()
);
}

#[test]
fn stale_caption_tracks_are_replaced_and_styles_preserved() {
let mut project = project(true, 8.0, 9.0);
Expand Down
7 changes: 0 additions & 7 deletions crates/verify/expectations.toml
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,6 @@ bound = { count = 1, missing = 30 }
reason = "NEW: same as editing/mapping/duration-covered, end to end: a 1 s fullscreen text at 5.4 s and the clip trimmed to 3.2 s give a 4.2 s timeline, but the export stops at the first unmapped frame: 96 frames instead of 126, and the text card is never rendered"
link = ""

[[xfail]]
case = "captions/legacy-migration/ends-at-timeline-end"
class = "caption-migration"
bound = { count = 1 }
reason = "NEW (minor): loading a legacy (output-timed) project drops a caption that ends exactly at the timeline end: synchronize_captions maps each end through source_start + (end - start) / timescale, which rounds just below the timeline end after a clip transition (clips 41/15 s and 82/15 s, 0.2 s transition: 7.9999999 < 8.0), so the end does not map and the whole caption is discarded"
link = ""

[[xfail]]
case = "playback/basic/cuts-speeds-reorder/realtime30-from25/avf"
class = "stale"
Expand Down
Loading