feat: enable diarization by default with an opt-out

Request speaker diarization through OpenRouter's Azure provider options for
all transcription uploads. Add --no-diarization to explicitly disable it for
both plain text and SRT output; returned speaker labels let existing SRT
cue generation split at speaker changes.

Default to 10-minute chunks when diarization is enabled because Microsoft
reports failures around 15 minutes and longer. Preserve the 30-minute
default with the opt-out and honor explicit --chunk-minutes values in both
modes. Document the options, duration limits, and output behavior.

Test Plan:
- python3 -B -m unittest -v: all 32 tests passed, including request options,
  default upload limits, opt-out combinations, and SRT speaker boundaries.
- bash -n transcribe: passed.
- ./transcribe --help: verified the new option and chunk defaults.
- git diff --cached --check: passed.
- Provider responses were mocked; no live API request was made.
This commit is contained in:
ddidderr committed 2026-09-12 09:04:29 +02:00
1 parent a5c535cfb9
commit 24b1030776
4 files changed
+121 -10

No files matched your search

+74
View File
@@ -41,6 +41,15 @@ class TranscribeTests(unittest.TestCase):
def assert_cleaned(self):
self.assertTrue(all(not Path(r["staging"]).exists() for r in self.requests()))
def assert_diarization(self, enabled):
for request in self.requests():
self.assertEqual(request["provider"], {
"options": {"azure": {"diarization": {"enabled": enabled}}},
})
self.assertIs(
request["provider"]["options"]["azure"]["diarization"]["enabled"], enabled,
)
def test_long_text_uploads_bounded_mp3_and_stitches_once(self):
result = self.run_cli("--chunk-minutes", "1")
self.assertEqual(result.returncode, 0, result.stderr)
@@ -48,7 +57,9 @@ class TranscribeTests(unittest.TestCase):
self.assertIn("chunk 2/2", result.stderr)
requests = self.requests()
self.assertEqual(len(requests), 2)
self.assertTrue(all(r["upload_duration"] <= 60 for r in requests))
self.assertAlmostEqual(requests[0]["chunk"]["end"] - requests[1]["chunk"]["start"], 6)
self.assert_diarization(True)
self.assert_cleaned()
def test_long_srt_uses_original_timeline_and_continuous_numbers(self):
@@ -59,6 +70,7 @@ class TranscribeTests(unittest.TestCase):
"2\n00:00:56,700 --> 00:00:57,800\nboundary continues.\n\n"
"3\n00:01:05,000 --> 00:01:05,400\nLast.\n\n"
))
self.assert_diarization(True)
self.assert_cleaned()
def test_short_text_retains_provider_formatting(self):
@@ -66,12 +78,74 @@ class TranscribeTests(unittest.TestCase):
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, "Provider's original formatting.\nNext line.\n")
self.assertEqual(len(self.requests()), 1)
self.assertIsNone(self.requests()[0]["response_format"])
self.assert_diarization(True)
self.assert_cleaned()
def test_short_srt_still_requests_word_timestamps(self):
result = self.run_cli("--srt", duration=5)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, "1\n00:00:01,000 --> 00:00:01,400\nHello.\n\n")
self.assertEqual(self.requests()[0]["response_format"], "verbose_json")
self.assertEqual(self.requests()[0]["timestamp_granularities"], ["segment", "word"])
self.assert_diarization(True)
def test_no_diarization_applies_to_text_srt_and_every_chunk(self):
for output in ((), ("--srt",)):
for chunked in (False, True):
with self.subTest(output=output, chunked=chunked):
self.log.unlink(missing_ok=True)
options = ("--chunk-minutes", "1") if chunked else ()
result = self.run_cli(
*output, *options, "--no-diarization", duration=70 if chunked else 5,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue(result.stdout)
self.assertEqual(len(self.requests()), 2 if chunked else 1)
self.assert_diarization(False)
for request in self.requests():
if output or chunked:
self.assertEqual(request["response_format"], "verbose_json")
self.assertEqual(request["timestamp_granularities"], ["segment", "word"])
else:
self.assertIsNone(request["response_format"])
self.assertIsNone(request["timestamp_granularities"])
self.assertLessEqual(request["upload_duration"], 60)
self.assert_cleaned()
# The opt-out must also preserve a limit specified after it.
self.log.unlink()
result = self.run_cli("--no-diarization", "--chunk-minutes", "1")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(len(self.requests()), 2)
self.assert_diarization(False)
self.assert_cleaned()
def test_default_upload_limits_depend_on_diarization(self):
for options, limit in (((), 600), (("--no-diarization",), 1800)):
with self.subTest(options=options):
self.log.unlink(missing_ok=True)
result = self.run_cli(*options, duration=limit + 10)
self.assertEqual(result.returncode, 0, result.stderr)
requests = self.requests()
self.assertEqual(len(requests), 2)
self.assertGreaterEqual(requests[0]["upload_duration"], limit - 0.1)
self.assertTrue(all(r["upload_duration"] <= limit for r in requests))
self.assert_diarization(not options)
self.assert_cleaned()
def test_diarization_breaks_srt_at_speaker_changes(self):
result = self.run_cli("--srt", mode="speakers", duration=5)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, (
"1\n00:00:01,000 --> 00:00:01,400\nHello\n\n"
"2\n00:00:01,500 --> 00:00:01,900\nthere\n\n"
))
self.assert_diarization(True)
self.log.unlink()
result = self.run_cli("--no-diarization", "--srt", mode="speakers", duration=5)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, "1\n00:00:01,000 --> 00:00:01,900\nHello there\n\n")
self.assert_diarization(False)
def test_failed_later_chunk_has_no_partial_stdout_and_cleans_up(self):
for mode in ("http-error", "bad-json", "api-error", "missing-words"):