Skip to content
This repository was archived by the owner on Jul 19, 2026. It is now read-only.
Merged
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
3 changes: 2 additions & 1 deletion src-tauri/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ pub struct CliArgs {
#[arg(long, value_name = "MODEL")]
pub model: Option<String>,

/// Output format for offline transcription: plain (default), inline, srt, vtt, json, karaoke.
/// Output format for offline transcription: plain (default), inline, srt, vtt, json, karaoke, speaker.
/// `speaker` groups diarized turns as `[Speaker N] (M:SS - M:SS)` blocks.
#[arg(long, value_name = "FORMAT")]
pub format: Option<String>,

Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/cli_transcription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ is auto-detected); the value is ignored."
// Resolve output format: explicit --format wins, else infer from -o extension, else plain.
let fmt = match format {
Some(f) => OutputFormat::from_cli(f).with_context(|| {
format!("Unknown --format '{f}'. Use plain|inline|srt|vtt|json|karaoke.")
format!("Unknown --format '{f}'. Use plain|inline|srt|vtt|json|karaoke|speaker.")
})?,
None => output
.and_then(|p| p.extension())
Expand Down
90 changes: 90 additions & 0 deletions src-tauri/src/transcript_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ pub enum OutputFormat {
Vtt,
Json,
Karaoke,
/// Speaker-grouped transcript: consecutive same-speaker segments collapsed
/// into one block headed `[Speaker N] (M:SS - M:SS)` followed by the text.
/// Intended for diarized output (`--diarize`).
Speaker,
}

impl OutputFormat {
Expand All @@ -44,6 +48,7 @@ impl OutputFormat {
"vtt" | "webvtt" => Some(Self::Vtt),
"json" => Some(Self::Json),
"karaoke" => Some(Self::Karaoke),
"speaker" | "speakers" | "diarized" => Some(Self::Speaker),
_ => None,
}
}
Expand Down Expand Up @@ -90,6 +95,13 @@ pub fn fmt_inline(secs: f32) -> String {
format!("{:02}:{:02}", total / 60, total % 60)
}

/// `M:SS` clock for speaker-block headers — minutes have no leading zero and may
/// exceed 59 (e.g. `0:01`, `2:14`, `75:30`). Matches the `(0:01 - 0:16)` style.
pub fn fmt_clock(secs: f32) -> String {
let total = secs.max(0.0).round() as u64;
format!("{}:{:02}", total / 60, total % 60)
}

fn hmsms(secs: f32) -> (u64, u64, u64, u64) {
let clamped = secs.max(0.0);
let total_ms = (clamped * 1000.0).round() as u64;
Expand Down Expand Up @@ -421,6 +433,41 @@ pub fn render(
}

OutputFormat::Karaoke => render_karaoke(words.unwrap_or(&[]), speakers),

// Speaker-grouped blocks: `[Speaker N] (M:SS - M:SS)\n<turn text>` with a
// blank line between turns. Consecutive same-speaker sub-segments collapse
// into one block.
OutputFormat::Speaker => {
let mut out = String::new();
let mut i = 0;
while i < resolved.len() {
let sp = resolved[i].1;
let mut j = i + 1;
while j < resolved.len() && resolved[j].1 == sp {
j += 1;
}
let start = resolved[i].0.start;
let end = resolved[j - 1].0.end;
let label = sp
.map(speaker_label)
.unwrap_or_else(|| "Speaker ?".to_string());
let text = resolved[i..j]
.iter()
.map(|(s, _)| s.text.trim())
.filter(|t| !t.is_empty())
.collect::<Vec<_>>()
.join(" ");
out.push_str(&format!(
"[{}] ({} - {})\n{}\n\n",
label,
fmt_clock(start),
fmt_clock(end),
text
));
i = j;
}
out.trim_end().to_string()
}
}
}

Expand Down Expand Up @@ -699,4 +746,47 @@ mod tests {
assert!(OutputFormat::Karaoke.is_word_level());
assert!(!OutputFormat::Srt.is_word_level());
}

#[test]
fn fmt_clock_has_no_leading_zero_minute() {
assert_eq!(fmt_clock(1.0), "0:01");
assert_eq!(fmt_clock(16.0), "0:16");
assert_eq!(fmt_clock(134.0), "2:14");
assert_eq!(fmt_clock(4530.0), "75:30");
}

#[test]
fn speaker_format_groups_turns_with_clock_headers() {
let segs = vec![seg(1.0, 16.0, "Hi there."), seg(17.0, 56.0, "Vastu is.")];
let turns = vec![
SpeakerTurn {
start: 0.0,
end: 16.5,
speaker: SpeakerId(1),
},
SpeakerTurn {
start: 16.5,
end: 60.0,
speaker: SpeakerId(0),
},
];
let out = render("", &segs, None, Some(&turns), OutputFormat::Speaker);
assert!(out.contains("[Speaker 2] (0:01 - 0:16)\nHi there."));
assert!(out.contains("[Speaker 1] (0:17 - 0:56)\nVastu is."));
// one blank line between turn blocks, no trailing blank
assert!(out.contains("Hi there.\n\n[Speaker 1]"));
assert!(!out.ends_with('\n'));
}

#[test]
fn speaker_format_parses_from_cli() {
assert_eq!(
OutputFormat::from_cli("speaker"),
Some(OutputFormat::Speaker)
);
assert_eq!(
OutputFormat::from_cli("diarized"),
Some(OutputFormat::Speaker)
);
}
}
6 changes: 6 additions & 0 deletions src/components/settings/transcribe/TranscribeFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ export const TranscribeFile: FC = () => {
/>
)}

{diarize && (
<p className="text-xs text-text/50 whitespace-pre-line leading-relaxed border-l-2 border-amber-500/40 pl-3">
{t("settings.transcribe.diarizeCliNote")}
</p>
)}

{busy && progress && (
<div className="space-y-2">
<ProgressBar
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/ar/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/bg/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/cs/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/he/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/it/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/ja/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/ko/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/pl/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/pt/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/ru/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Диаризация работает на CPU и медленная для длинных файлов — а запуск в приложении может прерваться при переключении вкладок. Для надёжного результата со спикерами используйте CLI:\necho --transcribe-file \"ФАЙЛ\" -o \"ВЫХОД.txt\" --diarize --format speaker"
},
"capture": {
"captureSuccess": "Saved to capture folder",
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/sv/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/tr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/uk/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/vi/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/zh-TW/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/zh/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@
"done": "Done"
},
"saveTo": "Save to…",
"savedTo": "Saved to {{path}}"
"savedTo": "Saved to {{path}}",
"diarizeCliNote": "Diarization runs on CPU and is slow for long files — and an in-app run can stop if you switch tabs. For reliable speaker-labelled output, use the CLI:\necho --transcribe-file \"FILE\" -o \"OUT.txt\" --diarize --format speaker"
}
},
"footer": {
Expand Down
Loading