Sagar Patel commited on
Commit
28032c3
·
1 Parent(s): 2982073

Fix voice transcription handling

Browse files
backend/modal_api.py CHANGED
@@ -19,15 +19,15 @@ REQUEST_TIMEOUT_SECONDS = 120
19
 
20
 
21
  def transcribe_audio(
22
- audio_path: str | Path | None,
23
- fallback: Callable[[str | Path | None], str],
24
  ) -> str:
25
  """Transcribe audio through Modal, falling back locally if unavailable."""
 
26
  endpoint_url = os.getenv(MODAL_TRANSCRIBE_URL_ENV)
27
- if not endpoint_url or audio_path is None:
28
  return fallback(audio_path)
29
 
30
- path = Path(audio_path)
31
  if not path.exists():
32
  return fallback(audio_path)
33
 
@@ -79,3 +79,22 @@ def _auth_headers() -> dict[str, str]:
79
  if not token:
80
  return {}
81
  return {"Authorization": f"Bearer {token}"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
 
21
  def transcribe_audio(
22
+ audio_path: Any,
23
+ fallback: Callable[[Any], str],
24
  ) -> str:
25
  """Transcribe audio through Modal, falling back locally if unavailable."""
26
+ path = _coerce_audio_path(audio_path)
27
  endpoint_url = os.getenv(MODAL_TRANSCRIBE_URL_ENV)
28
+ if not endpoint_url or path is None:
29
  return fallback(audio_path)
30
 
 
31
  if not path.exists():
32
  return fallback(audio_path)
33
 
 
79
  if not token:
80
  return {}
81
  return {"Authorization": f"Bearer {token}"}
82
+
83
+
84
+ def _coerce_audio_path(audio_value: Any) -> Path | None:
85
+ """Extract a local audio filepath from Gradio audio values."""
86
+ if audio_value is None:
87
+ return None
88
+ if isinstance(audio_value, (str, Path)):
89
+ return Path(audio_value)
90
+ if isinstance(audio_value, dict):
91
+ for key in ("path", "name", "file", "filepath"):
92
+ value = audio_value.get(key)
93
+ if value:
94
+ return Path(value)
95
+ if isinstance(audio_value, (list, tuple)):
96
+ for value in audio_value:
97
+ path = _coerce_audio_path(value)
98
+ if path is not None:
99
+ return path
100
+ return None
tests/test_modal_api.py CHANGED
@@ -82,6 +82,21 @@ def test_transcribe_audio_uses_modal_response(tmp_path: Path, monkeypatch) -> No
82
  assert transcript == "Sold 12 mangoes"
83
 
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  def test_transcribe_audio_falls_back_when_url_missing(tmp_path: Path, monkeypatch) -> None:
86
  audio_path = tmp_path / "audio.wav"
87
  audio_path.write_bytes(b"audio")
 
82
  assert transcript == "Sold 12 mangoes"
83
 
84
 
85
+ def test_transcribe_audio_accepts_gradio_dict_payload(tmp_path: Path, monkeypatch) -> None:
86
+ audio_path = tmp_path / "audio.wav"
87
+ audio_path.write_bytes(b"audio")
88
+ monkeypatch.setenv(modal_api.MODAL_TRANSCRIBE_URL_ENV, "https://modal.example/transcribe")
89
+
90
+ def fake_post(*args, **kwargs) -> FakeResponse:
91
+ return FakeResponse({"transcript": "Paid 500 for supplies"})
92
+
93
+ monkeypatch.setattr(modal_api.requests, "post", fake_post)
94
+
95
+ transcript = modal_api.transcribe_audio({"path": str(audio_path)}, fallback=lambda _: "fallback")
96
+
97
+ assert transcript == "Paid 500 for supplies"
98
+
99
+
100
  def test_transcribe_audio_falls_back_when_url_missing(tmp_path: Path, monkeypatch) -> None:
101
  audio_path = tmp_path / "audio.wav"
102
  audio_path.write_bytes(b"audio")
tests/test_speech.py CHANGED
@@ -15,3 +15,10 @@ def test_transcribe_audio_requires_existing_file(tmp_path: Path) -> None:
15
 
16
  with pytest.raises(TranscriptionError, match="does not exist"):
17
  transcribe_audio(missing_file)
 
 
 
 
 
 
 
 
15
 
16
  with pytest.raises(TranscriptionError, match="does not exist"):
17
  transcribe_audio(missing_file)
18
+
19
+
20
+ def test_transcribe_audio_accepts_gradio_dict_for_missing_file(tmp_path: Path) -> None:
21
+ missing_file = tmp_path / "missing.wav"
22
+
23
+ with pytest.raises(TranscriptionError, match="does not exist"):
24
+ transcribe_audio({"path": str(missing_file)})
tests/test_ui_audio_flow.py CHANGED
@@ -6,8 +6,8 @@ from voiceledger.ui import gradio_app
6
 
7
 
8
  def test_transcribe_and_parse_audio_handles_transcription_error(monkeypatch) -> None:
9
- def fail_transcription(_: str | None) -> str:
10
- raise gradio_app.TranscriptionError("test failure")
11
 
12
  monkeypatch.setattr(gradio_app.modal_api, "transcribe_audio", lambda audio, fallback: fail_transcription(audio))
13
 
 
6
 
7
 
8
  def test_transcribe_and_parse_audio_handles_transcription_error(monkeypatch) -> None:
9
+ def fail_transcription(_: object) -> str:
10
+ raise RuntimeError("test failure")
11
 
12
  monkeypatch.setattr(gradio_app.modal_api, "transcribe_audio", lambda audio, fallback: fail_transcription(audio))
13
 
voiceledger/speech/transcribe.py CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
5
  from functools import lru_cache
6
  from pathlib import Path
7
  from collections.abc import Iterable
8
- from typing import Protocol
9
 
10
 
11
  MODEL_SIZE = "small"
@@ -28,17 +28,17 @@ class TranscriptionError(RuntimeError):
28
  """Raised when audio transcription fails."""
29
 
30
 
31
- def transcribe_audio(audio_path: str | Path | None) -> str:
32
  """Transcribe an audio file with the faster-whisper small model.
33
 
34
  The model is loaded lazily and cached after the first call. This keeps the
35
  Gradio app responsive at startup while still using the requested small
36
  open-source speech model for actual transcription.
37
  """
38
- if audio_path is None:
 
39
  raise TranscriptionError("No audio file was provided.")
40
 
41
- path = Path(audio_path)
42
  if not path.exists():
43
  raise TranscriptionError(f"Audio file does not exist: {path}")
44
 
@@ -62,6 +62,25 @@ def transcribe_audio(audio_path: str | Path | None) -> str:
62
  return transcript.strip()
63
 
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  @lru_cache(maxsize=1)
66
  def _get_model() -> WhisperModelProtocol:
67
  """Load and cache the faster-whisper small model."""
 
5
  from functools import lru_cache
6
  from pathlib import Path
7
  from collections.abc import Iterable
8
+ from typing import Any, Protocol
9
 
10
 
11
  MODEL_SIZE = "small"
 
28
  """Raised when audio transcription fails."""
29
 
30
 
31
+ def transcribe_audio(audio_path: Any) -> str:
32
  """Transcribe an audio file with the faster-whisper small model.
33
 
34
  The model is loaded lazily and cached after the first call. This keeps the
35
  Gradio app responsive at startup while still using the requested small
36
  open-source speech model for actual transcription.
37
  """
38
+ path = _coerce_audio_path(audio_path)
39
+ if path is None:
40
  raise TranscriptionError("No audio file was provided.")
41
 
 
42
  if not path.exists():
43
  raise TranscriptionError(f"Audio file does not exist: {path}")
44
 
 
62
  return transcript.strip()
63
 
64
 
65
+ def _coerce_audio_path(audio_value: Any) -> Path | None:
66
+ """Extract a local audio filepath from Gradio audio values."""
67
+ if audio_value is None:
68
+ return None
69
+ if isinstance(audio_value, (str, Path)):
70
+ return Path(audio_value)
71
+ if isinstance(audio_value, dict):
72
+ for key in ("path", "name", "file", "filepath"):
73
+ value = audio_value.get(key)
74
+ if value:
75
+ return Path(value)
76
+ if isinstance(audio_value, (list, tuple)):
77
+ for value in audio_value:
78
+ path = _coerce_audio_path(value)
79
+ if path is not None:
80
+ return path
81
+ return None
82
+
83
+
84
  @lru_cache(maxsize=1)
85
  def _get_model() -> WhisperModelProtocol:
86
  """Load and cache the faster-whisper small model."""
voiceledger/ui/gradio_app.py CHANGED
@@ -299,11 +299,11 @@ def _parse_note(note: str) -> tuple[dict[str, Any], dict[str, Any], str]:
299
  return payload, payload, status
300
 
301
 
302
- def _transcribe_and_parse_audio(audio_path: str | None) -> tuple[str, dict[str, Any], dict[str, Any] | None, str]:
303
  """Transcribe recorded audio, parse the transcript, and return UI updates."""
304
  try:
305
  transcript = modal_api.transcribe_audio(audio_path, fallback=local_transcribe_audio)
306
- except TranscriptionError as exc:
307
  empty_payload = _empty_transaction_payload()
308
  return "", empty_payload, None, f"Transcription failed: {exc}"
309
 
 
299
  return payload, payload, status
300
 
301
 
302
+ def _transcribe_and_parse_audio(audio_path: Any) -> tuple[str, dict[str, Any], dict[str, Any] | None, str]:
303
  """Transcribe recorded audio, parse the transcript, and return UI updates."""
304
  try:
305
  transcript = modal_api.transcribe_audio(audio_path, fallback=local_transcribe_audio)
306
+ except Exception as exc:
307
  empty_payload = _empty_transaction_payload()
308
  return "", empty_payload, None, f"Transcription failed: {exc}"
309