feat(captions): wire font, shadow, dimmed color, bg toggle into ASS generation
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -547,17 +547,52 @@ pub fn vtt_to_ass_with_karaoke(
|
||||
|
||||
let out_path = Path::new(temp_dir).join("karaoke.ass");
|
||||
|
||||
let font_size = style.map_or(45, |s| (s.font_size as f64 * 2.5).round() as u32);
|
||||
// Font
|
||||
let font_name = style.map_or("Arial".to_string(), |s| s.font_family.clone());
|
||||
let font_size = style.map_or(50, |s| (s.font_size as f64 * 2.75).round() as u32);
|
||||
let bold_flag: i32 = if style.map_or(true, |s| s.bold) { -1 } else { 0 };
|
||||
|
||||
// Colors
|
||||
let primary_colour = style.map_or("&H00FFFFFF".to_string(), |s| hex_to_ass_color(&s.text_color));
|
||||
let secondary_colour = "&H73CCCCCC".to_string();
|
||||
let outline_colour = "&H00000000".to_string();
|
||||
let back_colour = style.map_or("&H80000000".to_string(), |s| opacity_to_ass_back_colour(s.background_opacity));
|
||||
let text_outline = style.map_or(true, |s| s.text_outline);
|
||||
let (border_style, outline_val, shadow_val) = if text_outline {
|
||||
(4, 2, 0)
|
||||
let outline_colour = style.map_or("&H00000000".to_string(), |s| hex_to_ass_color(&s.outline_color));
|
||||
|
||||
// Dimmed color (SecondaryColour) — auto derives from text color, custom uses direct color
|
||||
let secondary_colour = style.map_or("&H73CCCCCC".to_string(), |s| {
|
||||
if s.dimmed_color_mode == "custom" {
|
||||
hex_to_ass_color(&s.dimmed_color)
|
||||
} else {
|
||||
(3, 0, 0)
|
||||
let alpha = ((1.0 - s.dimmed_opacity.clamp(0.0, 1.0)) * 255.0).round() as u8;
|
||||
hex_to_ass_color_with_alpha(&s.text_color, alpha)
|
||||
}
|
||||
});
|
||||
|
||||
// Background/Shadow mutual exclusivity
|
||||
let bg_enabled = style.map_or(true, |s| s.background_enabled);
|
||||
let shadow_enabled = style.map_or(false, |s| s.shadow_enabled);
|
||||
let text_outline = style.map_or(true, |s| s.text_outline);
|
||||
|
||||
let (border_style, outline_val, shadow_val, back_colour) = if bg_enabled {
|
||||
// Background mode: box + optional outline, no shadow
|
||||
let bc = style.map_or("&H80000000".to_string(), |s| {
|
||||
let alpha = ((1.0 - s.background_opacity.clamp(0.0, 1.0)) * 255.0).round() as u8;
|
||||
hex_to_ass_color_with_alpha(&s.background_color, alpha)
|
||||
});
|
||||
let bs = if text_outline { 4 } else { 3 };
|
||||
let ol = if text_outline { 2 } else { 0 };
|
||||
(bs, ol, 0, bc)
|
||||
} else if shadow_enabled {
|
||||
// Shadow mode: outline + shadow, no box
|
||||
let sc = style.map_or("&H00000000".to_string(), |s| hex_to_ass_color(&s.shadow_color));
|
||||
let sd = style.map_or(2, |s| s.shadow_depth) as i32;
|
||||
let ol = if text_outline { 2 } else { 0 };
|
||||
(1, ol, sd, sc)
|
||||
} else {
|
||||
// Neither: just outline if enabled
|
||||
let ol = if text_outline { 2 } else { 0 };
|
||||
(1, ol, 0, "&HFF000000".to_string()) // fully transparent back
|
||||
};
|
||||
|
||||
let word_highlight = style.map_or(true, |s| s.word_highlight);
|
||||
let margin_v: i32 = 40;
|
||||
|
||||
let mut output = String::new();
|
||||
@@ -573,9 +608,9 @@ pub fn vtt_to_ass_with_karaoke(
|
||||
output.push_str("[V4+ Styles]\n");
|
||||
output.push_str("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n");
|
||||
output.push_str(&format!(
|
||||
"Style: Default,Arial,{},{},{},{},{},0,0,0,0,100,100,0,0,{},{},{},2,20,20,{},1\n",
|
||||
font_size, primary_colour, secondary_colour, outline_colour, back_colour,
|
||||
border_style, outline_val, shadow_val, margin_v
|
||||
"Style: Default,{},{},{},{},{},{},{},0,0,0,100,100,0,0,{},{},{},2,20,20,{},1\n",
|
||||
font_name, font_size, primary_colour, secondary_colour, outline_colour, back_colour,
|
||||
bold_flag, border_style, outline_val, shadow_val, margin_v
|
||||
));
|
||||
output.push('\n');
|
||||
|
||||
@@ -584,8 +619,9 @@ pub fn vtt_to_ass_with_karaoke(
|
||||
|
||||
let spoken_lines = extract_spoken_lines(&content);
|
||||
let gap_threshold = 2.0;
|
||||
let crossfade_ms = 300;
|
||||
let crossfade_s = crossfade_ms as f64 / 1000.0;
|
||||
let fade_ms = 100; // per-page fade in/out duration (sequential, no overlap)
|
||||
let fade_s = fade_ms as f64 / 1000.0;
|
||||
let hold_s = 100.0 / 1000.0; // hold fully visible after last word before fading out
|
||||
|
||||
let pages = group_into_pages(&spoken_lines, gap_threshold);
|
||||
let page_count = pages.len();
|
||||
@@ -615,21 +651,22 @@ pub fn vtt_to_ass_with_karaoke(
|
||||
let is_first = page_idx == 0;
|
||||
let is_last = page_idx == page_count - 1;
|
||||
|
||||
let display_start = if is_first {
|
||||
natural_start
|
||||
} else {
|
||||
(natural_start - crossfade_s).max(0.0)
|
||||
};
|
||||
// No overlap — each page fades in/out independently (sequential)
|
||||
let display_start = natural_start;
|
||||
let display_end = if is_last {
|
||||
natural_end
|
||||
} else {
|
||||
natural_end + crossfade_s
|
||||
natural_end + hold_s + fade_s
|
||||
};
|
||||
|
||||
let fade_in = if is_first { 0 } else { crossfade_ms };
|
||||
let fade_out = if is_last { 0 } else { crossfade_ms };
|
||||
let fade_in = if is_first { 0 } else { fade_ms };
|
||||
let fade_out = if is_last { 0 } else { fade_ms };
|
||||
|
||||
let karaoke_text = build_page_karaoke_text(&page_lines);
|
||||
let page_text = if word_highlight {
|
||||
build_page_karaoke_text(&page_lines)
|
||||
} else {
|
||||
page_lines.iter().map(|l| l.plain_text.as_str()).collect::<Vec<_>>().join("\\N")
|
||||
};
|
||||
|
||||
output.push_str(&format!(
|
||||
"Dialogue: 0,{},{},Default,,0,0,0,,{{\\an2\\pos({},{})\\fad({},{})}}{}\n",
|
||||
@@ -637,7 +674,7 @@ pub fn vtt_to_ass_with_karaoke(
|
||||
format_ass_timestamp(display_end),
|
||||
cx, y_bottom,
|
||||
fade_in, fade_out,
|
||||
karaoke_text
|
||||
page_text
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user