feat(diarization): per-speaker transcripts via TinyDiarize + --transcribe-file (#884)
* feat(transcribe): add TinyDiarize file transcription surface Catalog small.en-tdrz (hosted at akashmjn/tinydiarize-whisper.cpp, pinned commit) while keeping it out of every dictation-model selection path: its [SPEAKER_TURN] markup would type noise into the focused window. The checksum generator resolves side-repo models at their own pinned revision, parakeet-style. Add speech_recognition.diarization (strict-WAV fast path with ffmpeg fallback, tdrz transcription, speaker-turn parsing, timestamped formatting) and surface it through a tray Transcribe Audio File flow with on-demand model download plus a headless --transcribe-file CLI. TranscriptDialog offers copy and save of the diarized transcript. Closes #756 * docs(user-guide): document --transcribe-file * fix(transcribe): pass tdrz_enable as a pywhispercpp kwarg pywhispercpp's Model takes decode params as keyword arguments; a params= dict crashes on the C-level whisper_full_params object. Also let --transcribe-file report unexpected failures cleanly instead of dumping a traceback. * fix(tray): dismiss the tdrz download dialog on response * test(diarization): pin real wave/numpy/tempfile around sibling mocks Several test modules replace sys.modules entries with MagicMocks at import time and never restore them, so diarization's wave/numpy/ tempfile bindings resolved to mocks when this file ran after them in CI. An autouse fixture reimports the real stdlib modules per test, the same remedy test_audio_feedback already applies for wave. * fix(transcribe): verify tdrz model, preload libs, gate formats on ffmpeg - Hash the on-disk TinyDiarize model against the pinned digest before native code maps it, matching the dictation load path - Extract the bundled-lib preloader to utils/pywhispercpp_loader.py and preload before the lazy pywhispercpp import so the headless --transcribe-file path works on source-built installs without RPATH - Offer only WAV in the file picker when ffmpeg is absent; name the dependency in the filter label - Type the new tray/diarization callbacks; document --transcribe-file and the new modules in AGENTS.md * style(audio): finish type annotations on capture helpers and download path _setup_alsa_error_handler now declares Optional[Any] and its ctypes callback gets concrete parameter types; the remaining capture helpers take audio: Any (PyAudio has no importable type) and test_audio_input's device_index becomes Optional[int], which also clears the preexisting implicit-Optional error. _download_whispercpp_model gains -> None and its HF url is narrowed to str so the concatenation and download call stop adding new mypy findings. * style(tests): wrap long monkeypatch line in preload test * refactor(main): narrow transcribe-file errors and add file context * style(ui): annotate TranscriptDialog constructor * fix(diarize): log worker tracebacks, isort test helpers, document --transcribe-file --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
This commit is contained in:
co-authored by
Jatin K Malik
parent
b4babcaeca
commit
8ebc4ce2ba
@@ -123,10 +123,7 @@ class TestModelInfo(unittest.TestCase):
|
||||
|
||||
def test_available_models_contains_all_models(self):
|
||||
"""Test that AVAILABLE_MODELS contains all defined models."""
|
||||
from vocalinux.utils.whispercpp_model_info import (
|
||||
AVAILABLE_MODELS,
|
||||
WHISPERCPP_MODEL_INFO,
|
||||
)
|
||||
from vocalinux.utils.whispercpp_model_info import AVAILABLE_MODELS, WHISPERCPP_MODEL_INFO
|
||||
|
||||
self.assertEqual(set(AVAILABLE_MODELS), set(WHISPERCPP_MODEL_INFO.keys()))
|
||||
|
||||
@@ -164,6 +161,7 @@ class TestModelInfo(unittest.TestCase):
|
||||
"large-v3-turbo",
|
||||
"large-v3-turbo-q5_0",
|
||||
"large-v3-turbo-q8_0",
|
||||
"small.en-tdrz",
|
||||
]
|
||||
|
||||
self.assertEqual(AVAILABLE_MODELS, expected_models)
|
||||
@@ -173,6 +171,7 @@ class TestModelInfo(unittest.TestCase):
|
||||
from vocalinux.utils.whispercpp_model_info import (
|
||||
MODEL_SIZES,
|
||||
MODEL_VARIANTS_BY_SIZE,
|
||||
NON_DICTATION_MODELS,
|
||||
WHISPERCPP_MODEL_INFO,
|
||||
)
|
||||
|
||||
@@ -185,12 +184,42 @@ class TestModelInfo(unittest.TestCase):
|
||||
grouped_models = {
|
||||
model_name for variants in MODEL_VARIANTS_BY_SIZE.values() for model_name in variants
|
||||
}
|
||||
self.assertEqual(grouped_models, set(WHISPERCPP_MODEL_INFO.keys()))
|
||||
self.assertEqual(grouped_models, set(WHISPERCPP_MODEL_INFO.keys()) - NON_DICTATION_MODELS)
|
||||
self.assertFalse(
|
||||
any(model_name.endswith("-tdrz") for model_name in grouped_models),
|
||||
"TinyDiarize needs diarization-specific runtime support, not the base dictation picker",
|
||||
)
|
||||
|
||||
def test_tdrz_model_is_catalogued_but_not_a_dictation_model(self):
|
||||
"""TinyDiarize is fetchable through the catalog, never dictation-selectable."""
|
||||
from vocalinux.utils.whispercpp_model_info import (
|
||||
NON_DICTATION_MODELS,
|
||||
TDRZ_MODEL,
|
||||
WHISPERCPP_MODEL_INFO,
|
||||
is_dictation_model,
|
||||
whispercpp_model_file,
|
||||
whispercpp_model_source,
|
||||
)
|
||||
|
||||
self.assertEqual(TDRZ_MODEL, "small.en-tdrz")
|
||||
self.assertIn(TDRZ_MODEL, WHISPERCPP_MODEL_INFO)
|
||||
self.assertIn(TDRZ_MODEL, NON_DICTATION_MODELS)
|
||||
self.assertFalse(is_dictation_model(TDRZ_MODEL))
|
||||
self.assertTrue(is_dictation_model("small.en"))
|
||||
self.assertFalse(is_dictation_model("not-a-model"))
|
||||
|
||||
self.assertEqual(whispercpp_model_file(TDRZ_MODEL), "ggml-small.en-tdrz.bin")
|
||||
url = WHISPERCPP_MODEL_INFO[TDRZ_MODEL]["url"]
|
||||
self.assertIn("akashmjn/tinydiarize-whisper.cpp", url)
|
||||
repo, revision = whispercpp_model_source(TDRZ_MODEL)
|
||||
self.assertEqual(repo, "akashmjn/tinydiarize-whisper.cpp")
|
||||
self.assertTrue(revision)
|
||||
self.assertIn(f"/resolve/{revision}/", url)
|
||||
|
||||
main_repo, main_revision = whispercpp_model_source("small.en")
|
||||
self.assertEqual(main_repo, "ggerganov/whisper.cpp")
|
||||
self.assertEqual(main_revision, "")
|
||||
|
||||
def test_model_variant_helpers_identify_size_and_english_only_variants(self):
|
||||
"""Test helpers that drive the split whisper.cpp model selectors."""
|
||||
from vocalinux.utils.whispercpp_model_info import (
|
||||
@@ -575,10 +604,7 @@ class TestDetectVulkanSupport(unittest.TestCase):
|
||||
return MagicMock(returncode=1, stdout="")
|
||||
|
||||
with patch("subprocess.run", side_effect=run_side_effect):
|
||||
from vocalinux.utils.whispercpp_model_info import (
|
||||
ComputeBackend,
|
||||
detect_compute_backend,
|
||||
)
|
||||
from vocalinux.utils.whispercpp_model_info import ComputeBackend, detect_compute_backend
|
||||
|
||||
backend, backend_info = detect_compute_backend()
|
||||
self.assertEqual(backend, ComputeBackend.VULKAN)
|
||||
@@ -745,10 +771,7 @@ class TestDetectComputeBackend(unittest.TestCase):
|
||||
mock_vulkan.return_value = (True, "Intel Arc GPU")
|
||||
mock_cuda.return_value = (True, "NVIDIA RTX 3080 (10GB)")
|
||||
|
||||
from vocalinux.utils.whispercpp_model_info import (
|
||||
ComputeBackend,
|
||||
detect_compute_backend,
|
||||
)
|
||||
from vocalinux.utils.whispercpp_model_info import ComputeBackend, detect_compute_backend
|
||||
|
||||
backend, info = detect_compute_backend()
|
||||
|
||||
@@ -764,10 +787,7 @@ class TestDetectComputeBackend(unittest.TestCase):
|
||||
mock_vulkan.return_value = (False, None)
|
||||
mock_cuda.return_value = (True, "NVIDIA RTX 3080 (10GB)")
|
||||
|
||||
from vocalinux.utils.whispercpp_model_info import (
|
||||
ComputeBackend,
|
||||
detect_compute_backend,
|
||||
)
|
||||
from vocalinux.utils.whispercpp_model_info import ComputeBackend, detect_compute_backend
|
||||
|
||||
backend, info = detect_compute_backend()
|
||||
|
||||
@@ -785,10 +805,7 @@ class TestDetectComputeBackend(unittest.TestCase):
|
||||
mock_cuda.return_value = (False, None)
|
||||
mock_cpu.return_value = "Intel Core i7"
|
||||
|
||||
from vocalinux.utils.whispercpp_model_info import (
|
||||
ComputeBackend,
|
||||
detect_compute_backend,
|
||||
)
|
||||
from vocalinux.utils.whispercpp_model_info import ComputeBackend, detect_compute_backend
|
||||
|
||||
backend, info = detect_compute_backend()
|
||||
|
||||
@@ -1148,10 +1165,7 @@ class TestGetBackendDisplayName(unittest.TestCase):
|
||||
|
||||
def test_get_backend_display_name_returns_string(self):
|
||||
"""Test that get_backend_display_name returns a string."""
|
||||
from vocalinux.utils.whispercpp_model_info import (
|
||||
ComputeBackend,
|
||||
get_backend_display_name,
|
||||
)
|
||||
from vocalinux.utils.whispercpp_model_info import ComputeBackend, get_backend_display_name
|
||||
|
||||
result = get_backend_display_name(ComputeBackend.CPU)
|
||||
self.assertIsInstance(result, str)
|
||||
@@ -1159,30 +1173,21 @@ class TestGetBackendDisplayName(unittest.TestCase):
|
||||
|
||||
def test_get_backend_display_name_vulkan(self):
|
||||
"""Test display name for Vulkan backend."""
|
||||
from vocalinux.utils.whispercpp_model_info import (
|
||||
ComputeBackend,
|
||||
get_backend_display_name,
|
||||
)
|
||||
from vocalinux.utils.whispercpp_model_info import ComputeBackend, get_backend_display_name
|
||||
|
||||
result = get_backend_display_name(ComputeBackend.VULKAN)
|
||||
self.assertEqual(result, "Vulkan GPU")
|
||||
|
||||
def test_get_backend_display_name_cuda(self):
|
||||
"""Test display name for CUDA backend."""
|
||||
from vocalinux.utils.whispercpp_model_info import (
|
||||
ComputeBackend,
|
||||
get_backend_display_name,
|
||||
)
|
||||
from vocalinux.utils.whispercpp_model_info import ComputeBackend, get_backend_display_name
|
||||
|
||||
result = get_backend_display_name(ComputeBackend.CUDA)
|
||||
self.assertEqual(result, "NVIDIA CUDA")
|
||||
|
||||
def test_get_backend_display_name_cpu(self):
|
||||
"""Test display name for CPU backend."""
|
||||
from vocalinux.utils.whispercpp_model_info import (
|
||||
ComputeBackend,
|
||||
get_backend_display_name,
|
||||
)
|
||||
from vocalinux.utils.whispercpp_model_info import ComputeBackend, get_backend_display_name
|
||||
|
||||
result = get_backend_display_name(ComputeBackend.CPU)
|
||||
self.assertEqual(result, "CPU")
|
||||
|
||||
Reference in New Issue
Block a user