From 1017fe73af60e1e1eef744f5266ffa218b3fe6a8 Mon Sep 17 00:00:00 2001 From: Cameron Cordes Date: Fri, 12 Jun 2026 16:21:41 -0400 Subject: [PATCH] Include start offset in voice-name window tag Clones that don't start at 0:00 are tagged with where the reference window begins (grandma-at1m32s-30s), so voices cloned from different sections of the same source are distinguishable in the voice list. Zero-start names keep the existing -30s form. Co-Authored-By: Claude Fable 5 --- src/ai/tts.rs | 74 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/src/ai/tts.rs b/src/ai/tts.rs index 415dcbf..08d9dcd 100644 --- a/src/ai/tts.rs +++ b/src/ai/tts.rs @@ -177,13 +177,25 @@ fn tts_ref_seconds() -> u32 { .unwrap_or(30) } -/// Tag a (sanitized) voice name with the reference-clip cap used to create it, -/// e.g. `grandma` → `grandma-30s`. The tag makes the ref length visible in the -/// voice list so clones of the same source at different caps can be compared. +/// Tag a (sanitized) voice name with the reference window used to create it: +/// `grandma` → `grandma-30s` (from the start), or `grandma-at1m32s-30s` (30s +/// window starting at 1:32). The tag makes the window visible in the voice +/// list so clones of the same source from different sections can be compared. /// Skips the append when the name already ends in the same tag; keeps the /// 64-char bound by truncating the base name, never the tag. -fn append_ref_seconds(name: &str, secs: u32) -> String { - let suffix = format!("-{secs}s"); +fn append_ref_window(name: &str, start: f64, secs: u32) -> String { + let start_whole = start.round().max(0.0) as u64; + let suffix = if start_whole > 0 { + // ':' isn't in the safe voice-name charset, so 1:32 becomes 1m32s. + let at = if start_whole >= 60 { + format!("at{}m{:02}s", start_whole / 60, start_whole % 60) + } else { + format!("at{start_whole}s") + }; + format!("-{at}-{secs}s") + } else { + format!("-{secs}s") + }; if name.ends_with(&suffix) { return name.to_string(); } @@ -880,7 +892,7 @@ pub async fn create_voice_upload_handler( }; // Tag the name with the ref-clip length (e.g. `grandma-30s`) so the // library shows which reference length produced each clone. - let name = append_ref_seconds(&name, ref_duration.round().max(1.0) as u32); + let name = append_ref_window(&name, ref_start, ref_duration.round().max(1.0) as u32); if file_bytes.is_empty() { span.set_status(Status::error("voice_file is required")); return HttpResponse::BadRequest().json(json!({ "error": "voice_file is required" })); @@ -970,7 +982,8 @@ pub async fn create_voice_from_library_handler( }; // Tag the name with the ref-clip length (e.g. `grandma-30s`) so the // library shows which reference length produced each clone. - let voice_name = append_ref_seconds(&voice_name, ref_duration.round().max(1.0) as u32); + let voice_name = + append_ref_window(&voice_name, ref_start, ref_duration.round().max(1.0) as u32); let library = match libraries::resolve_library_param(&app_state, req.library.as_deref()) { Ok(Some(l)) => l, @@ -1075,24 +1088,53 @@ mod tests { } #[test] - fn append_ref_seconds_tags_name() { - assert_eq!(append_ref_seconds("grandma", 30), "grandma-30s"); - assert_eq!(append_ref_seconds("voice_01", 15), "voice_01-15s"); + fn append_ref_window_tags_name() { + assert_eq!(append_ref_window("grandma", 0.0, 30), "grandma-30s"); + assert_eq!(append_ref_window("voice_01", 0.0, 15), "voice_01-15s"); } #[test] - fn append_ref_seconds_is_idempotent_for_same_cap() { - assert_eq!(append_ref_seconds("grandma-30s", 30), "grandma-30s"); - // A different cap still appends — that's the comparison use-case. - assert_eq!(append_ref_seconds("grandma-15s", 30), "grandma-15s-30s"); + fn append_ref_window_includes_nonzero_start() { + // Sub-minute starts stay in seconds; longer ones read as XmYYs since + // ':' isn't allowed in voice names. + assert_eq!(append_ref_window("grandma", 45.0, 30), "grandma-at45s-30s"); + assert_eq!( + append_ref_window("grandma", 92.4, 30), + "grandma-at1m32s-30s" + ); + assert_eq!( + append_ref_window("grandma", 600.0, 12), + "grandma-at10m00s-12s" + ); + // A start that rounds to zero is "from the start". + assert_eq!(append_ref_window("grandma", 0.3, 30), "grandma-30s"); } #[test] - fn append_ref_seconds_keeps_64_char_bound() { + fn append_ref_window_is_idempotent_for_same_window() { + assert_eq!(append_ref_window("grandma-30s", 0.0, 30), "grandma-30s"); + assert_eq!( + append_ref_window("grandma-at45s-30s", 45.0, 30), + "grandma-at45s-30s" + ); + // A different window still appends — that's the comparison use-case. + assert_eq!(append_ref_window("grandma-15s", 0.0, 30), "grandma-15s-30s"); + assert_eq!( + append_ref_window("grandma-30s", 45.0, 30), + "grandma-30s-at45s-30s" + ); + } + + #[test] + fn append_ref_window_keeps_64_char_bound() { let long = "a".repeat(64); - let tagged = append_ref_seconds(&long, 30); + let tagged = append_ref_window(&long, 0.0, 30); assert_eq!(tagged.len(), 64); assert!(tagged.ends_with("-30s")); + + let tagged = append_ref_window(&long, 92.0, 30); + assert_eq!(tagged.len(), 64); + assert!(tagged.ends_with("-at1m32s-30s")); } #[test]