Ratnesh-dev commited on
Commit
cab7fa7
·
1 Parent(s): f0c493a

Revert repository state to e4b57c8

Browse files
README.md CHANGED
@@ -4,63 +4,76 @@ emoji: ⚡
4
  colorFrom: indigo
5
  colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 5.42.0
8
  python_version: "3.12"
9
  app_file: app.py
10
  pinned: false
11
  license: mit
12
- short_description: Phase 1 ZeroGPU Parakeet transcription API
13
  ---
14
 
15
  # Phase 1 rebuild: Parakeet-only
16
 
17
- This repository is still using `plan/rebuild_plan.md` as the source of truth, but the active Phase 1 implementation now closely follows the working ZeroGPU reference Space:
18
 
19
- - `rishiraj/parakeet-tdt-0.6b-v3`
 
 
 
 
20
 
21
- ## Active scope
22
- - Parakeet-only transcription
23
- - ZeroGPU execution via `@spaces.GPU(duration=8)`
24
- - dependency set aligned with the working reference Space
25
- - minimal `/transcribe_parakeet` API route
26
-
27
- ## Not implemented yet
28
  - pyannote diarization
29
- - transcript/diarization merge
30
  - combined pipeline
31
 
 
 
 
32
  ## Request inputs
33
  - `audio_file`
34
- - `model_options_json` (currently accepted for interface compatibility; the reference code path does not actively use it)
35
 
36
  ## Response shape
37
  ```json
38
  {
39
- "model": "nvidia/parakeet-tdt-0.6b-v3",
 
 
 
40
  "audio_file": "/tmp/gradio/example.wav",
41
- "device": "cuda",
42
  "duration_seconds": 300.0,
43
- "long_audio": {
44
- "threshold_seconds": 480,
45
- "applied": false
46
- },
47
- "segment_count": 42,
48
- "segments": [
49
- {
50
- "start": 0.0,
51
- "end": 7.09,
52
- "segment": "Ladies and gentlemen..."
53
- }
54
- ],
55
- "text": "Ladies and gentlemen...",
56
- "request_id": "uuid",
57
  "execution_plan": {
58
- "mode": "reference_space_replication",
59
- "duration_seconds": 300.0,
60
- "model_options": {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  }
62
  }
63
  ```
64
 
65
  ## Legacy code
66
- The previous implementation remains archived under `archive/legacy_pre_rebuild_*/`.
 
4
  colorFrom: indigo
5
  colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 6.6.0
8
  python_version: "3.12"
9
  app_file: app.py
10
  pinned: false
11
  license: mit
12
+ short_description: Phase 1 rebuild scaffold - Parakeet-only transcription API
13
  ---
14
 
15
  # Phase 1 rebuild: Parakeet-only
16
 
17
+ This repository has been reset for a phased rebuild using `plan/rebuild_plan.md` as the source of truth.
18
 
19
+ ## Current scope
20
+ Only Phase 1 is active:
21
+ - Parakeet-only transcription API
22
+ - CPU-side preload/probing/postprocessing
23
+ - narrow `@spaces.GPU(duration=120)` window around inference only
24
 
25
+ Not implemented yet:
 
 
 
 
 
 
26
  - pyannote diarization
27
+ - merge logic
28
  - combined pipeline
29
 
30
+ ## Active API
31
+ - `/transcribe_parakeet`
32
+
33
  ## Request inputs
34
  - `audio_file`
35
+ - `model_options_json` (optional JSON object)
36
 
37
  ## Response shape
38
  ```json
39
  {
40
+ "phase": 1,
41
+ "request_id": "uuid",
42
+ "model": "NVIDIA Parakeet v3",
43
+ "model_id": "nvidia/parakeet-tdt-0.6b-v3",
44
  "audio_file": "/tmp/gradio/example.wav",
 
45
  "duration_seconds": 300.0,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  "execution_plan": {
47
+ "mode": "single_pass",
48
+ "chunking": {
49
+ "implemented": false,
50
+ "reason": "Phase 1 scaffold keeps chunk planning on CPU but does not split audio yet."
51
+ }
52
+ },
53
+ "preload": {
54
+ "cache_hit": true,
55
+ "load_seconds": 0.0
56
+ },
57
+ "zerogpu_timing": {
58
+ "gpu_window_seconds": 3.12,
59
+ "inference_seconds": 2.87
60
+ },
61
+ "wall_clock_timing": {
62
+ "probe_seconds": 0.03,
63
+ "preload_seconds": 0.0,
64
+ "postprocess_seconds": 0.01,
65
+ "total_seconds": 3.22
66
+ },
67
+ "raw_output": {
68
+ "result": {},
69
+ "runtime": {}
70
+ },
71
+ "normalized_output": {
72
+ "text": "...",
73
+ "word_timestamps": []
74
  }
75
  }
76
  ```
77
 
78
  ## Legacy code
79
+ The previous implementation was archived under `archive/legacy_pre_rebuild_*/` so the rebuild context stays clean.
app.py CHANGED
@@ -1,9 +1,13 @@
1
  from __future__ import annotations
2
 
 
 
 
3
  import gradio as gr
4
  import spaces
5
 
6
- from src.parakeet.model import preload_parakeet_model, transcribe_audio, transcribe_audio_cpu
 
7
  from src.parakeet.service import run_transcribe_parakeet_request
8
  from src.runtime.errors import format_exception_for_client
9
  from src.runtime.logging import configure_logging, get_logger, log_event
@@ -15,16 +19,28 @@ _STARTUP_PRELOAD = preload_parakeet_model(strict=False)
15
  log_event(logger, "startup.parakeet_preload", status=_STARTUP_PRELOAD)
16
 
17
 
18
- @spaces.GPU(duration=12)
19
- def _gpu_transcribe_parakeet(audio_file: str, session_dir: str) -> dict:
20
- return transcribe_audio(audio_path=audio_file, session_dir=session_dir, runtime_device="cuda")
21
-
22
-
23
- def _cpu_transcribe_parakeet(audio_file: str, session_dir: str) -> dict:
24
- return transcribe_audio_cpu(audio_path=audio_file, session_dir=session_dir)
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
 
27
- def transcribe_parakeet(audio_file: str, model_options_json: str | None) -> dict:
28
  try:
29
  return run_transcribe_parakeet_request(
30
  audio_file=audio_file,
@@ -32,33 +48,25 @@ def transcribe_parakeet(audio_file: str, model_options_json: str | None) -> dict
32
  gpu_runner=_gpu_transcribe_parakeet,
33
  )
34
  except Exception as exc:
35
- logger.exception("Parakeet GPU request failed")
36
- raise gr.Error(format_exception_for_client(exc)) from exc
37
-
38
-
39
- def transcribe_parakeet_cpu_route(audio_file: str, model_options_json: str | None) -> dict:
40
- try:
41
- return run_transcribe_parakeet_request(
42
- audio_file=audio_file,
43
- model_options_json=model_options_json,
44
- gpu_runner=_cpu_transcribe_parakeet,
45
- )
46
- except Exception as exc:
47
- logger.exception("Parakeet CPU request failed")
48
  raise gr.Error(format_exception_for_client(exc)) from exc
49
 
50
 
51
  with gr.Blocks(title="Phase 1 - Parakeet only") as demo:
52
  gr.Markdown(
53
  "# Phase 1 rebuild: Parakeet-only transcription\n\n"
54
- "This implementation follows the working ZeroGPU reference space and now also exposes a CPU-only debug route."
 
55
  )
56
 
57
  audio_file = gr.Audio(sources=["upload"], type="filepath", label="Audio file")
58
- model_options_json = gr.Textbox(label="model_options_json", lines=4, value="{}")
59
- with gr.Row():
60
- run_button = gr.Button("Run /transcribe_parakeet")
61
- run_cpu_button = gr.Button("Run /transcribe_parakeet_cpu")
 
 
 
62
  output = gr.JSON(label="Parakeet transcription JSON")
63
 
64
  run_button.click(
@@ -66,16 +74,12 @@ with gr.Blocks(title="Phase 1 - Parakeet only") as demo:
66
  inputs=[audio_file, model_options_json],
67
  outputs=output,
68
  api_name="transcribe_parakeet",
69
- api_description="Run Parakeet transcription using the working ZeroGPU reference-space code path.",
70
- )
71
-
72
- run_cpu_button.click(
73
- fn=transcribe_parakeet_cpu_route,
74
- inputs=[audio_file, model_options_json],
75
- outputs=output,
76
- api_name="transcribe_parakeet_cpu",
77
- api_description="Run Parakeet transcription on CPU only, with no ZeroGPU decorator, for debugging.",
78
  )
79
 
80
 
81
- demo.queue().launch()
 
1
  from __future__ import annotations
2
 
3
+ import time
4
+ from typing import Any
5
+
6
  import gradio as gr
7
  import spaces
8
 
9
+ from src.parakeet.config import PARAKEET_MODEL_LABEL
10
+ from src.parakeet.model import preload_parakeet_model, run_parakeet_inference
11
  from src.parakeet.service import run_transcribe_parakeet_request
12
  from src.runtime.errors import format_exception_for_client
13
  from src.runtime.logging import configure_logging, get_logger, log_event
 
19
  log_event(logger, "startup.parakeet_preload", status=_STARTUP_PRELOAD)
20
 
21
 
22
+ @spaces.GPU(duration=120)
23
+ def _gpu_transcribe_parakeet(
24
+ audio_file: str,
25
+ duration_seconds: float | None,
26
+ model_options: dict[str, Any],
27
+ ) -> dict[str, Any]:
28
+ gpu_started_at = time.perf_counter()
29
+ result = run_parakeet_inference(
30
+ audio_file=audio_file,
31
+ duration_seconds=duration_seconds,
32
+ model_options=model_options,
33
+ )
34
+ return {
35
+ "raw_output": result["raw_output"],
36
+ "zerogpu_timing": {
37
+ "gpu_window_seconds": round(time.perf_counter() - gpu_started_at, 4),
38
+ **result["timing"],
39
+ },
40
+ }
41
 
42
 
43
+ def transcribe_parakeet(audio_file: str, model_options_json: str | None) -> dict[str, Any]:
44
  try:
45
  return run_transcribe_parakeet_request(
46
  audio_file=audio_file,
 
48
  gpu_runner=_gpu_transcribe_parakeet,
49
  )
50
  except Exception as exc:
51
+ logger.exception("Parakeet request failed")
 
 
 
 
 
 
 
 
 
 
 
 
52
  raise gr.Error(format_exception_for_client(exc)) from exc
53
 
54
 
55
  with gr.Blocks(title="Phase 1 - Parakeet only") as demo:
56
  gr.Markdown(
57
  "# Phase 1 rebuild: Parakeet-only transcription\n\n"
58
+ "The old implementation has been archived. This Space now exposes only the Phase 1 "
59
+ "Parakeet transcription API while ZeroGPU usage is benchmarked and stabilized."
60
  )
61
 
62
  audio_file = gr.Audio(sources=["upload"], type="filepath", label="Audio file")
63
+ model_options_json = gr.Textbox(
64
+ label="model_options_json",
65
+ lines=8,
66
+ value="{}",
67
+ placeholder='{"compute_dtype": "bfloat16"}',
68
+ )
69
+ run_button = gr.Button("Run /transcribe_parakeet")
70
  output = gr.JSON(label="Parakeet transcription JSON")
71
 
72
  run_button.click(
 
74
  inputs=[audio_file, model_options_json],
75
  outputs=output,
76
  api_name="transcribe_parakeet",
77
+ api_description=(
78
+ f"Run {PARAKEET_MODEL_LABEL} only. "
79
+ "This Phase 1 route performs CPU-side duration probe/preload/postprocessing and keeps the "
80
+ "decorated ZeroGPU window narrowly scoped to model inference."
81
+ ),
 
 
 
 
82
  )
83
 
84
 
85
+ demo.queue(default_concurrency_limit=1).launch(ssr_mode=False, theme=gr.themes.Ocean())
packages.txt CHANGED
@@ -1,2 +1 @@
1
  ffmpeg
2
- libsndfile1
 
1
  ffmpeg
 
requirements.txt CHANGED
@@ -1,5 +1,7 @@
 
 
1
  Cython
2
  cuda-python
 
 
3
  nemo_toolkit[asr] @ git+https://github.com/NVIDIA/NeMo.git@main
4
- pydub
5
- numpy
 
1
+ # Phase 1 only: Parakeet scaffold.
2
+ # Freeze final exact versions after Phase 1 is benchmarked.
3
  Cython
4
  cuda-python
5
+ torch==2.6.0
6
+ torchaudio==2.6.0
7
  nemo_toolkit[asr] @ git+https://github.com/NVIDIA/NeMo.git@main
 
 
src/parakeet/model.py CHANGED
@@ -1,163 +1,169 @@
1
  from __future__ import annotations
2
 
3
  import gc
4
- import os
5
- from pathlib import Path
6
  from typing import Any
7
 
8
- import numpy as np
9
- import torch
10
- from nemo.collections.asr.models import ASRModel
11
- from pydub import AudioSegment
12
 
13
- from src.parakeet.config import PARAKEET_MODEL_ID
14
 
15
- MODEL_NAME = PARAKEET_MODEL_ID
16
- MODEL = ASRModel.from_pretrained(model_name=MODEL_NAME)
17
- MODEL.eval()
18
-
19
-
20
- def preload_parakeet_model(strict: bool = True) -> dict[str, Any]:
21
- return {
22
- "ok": True,
23
- "model": MODEL_NAME,
24
- "device_detected_at_import": "not_checked",
25
- "cache_hit": True,
26
- "load_seconds": 0.0,
27
- "strict": strict,
28
- }
29
 
 
 
 
 
30
 
31
- def get_audio_segment(audio_path: str, start_second: float, end_second: float) -> tuple[int, np.ndarray] | None:
32
- if not audio_path or not Path(audio_path).exists():
33
- return None
34
  try:
35
- start_ms = max(0, int(start_second * 1000))
36
- end_ms = int(end_second * 1000)
37
- if end_ms <= start_ms:
38
- end_ms = start_ms + 100
39
-
40
- audio = AudioSegment.from_file(audio_path)
41
- clipped_audio = audio[start_ms:end_ms]
42
-
43
- samples = np.array(clipped_audio.get_array_of_samples())
44
- if clipped_audio.channels == 2:
45
- samples = samples.reshape((-1, 2)).mean(axis=1).astype(samples.dtype)
46
-
47
- frame_rate = clipped_audio.frame_rate or audio.frame_rate
48
- if samples.size == 0 or frame_rate <= 0:
49
- return None
50
- return frame_rate, samples
51
  except Exception:
52
- return None
53
-
54
-
55
- def _prepare_audio(audio_path: str, session_dir: str) -> tuple[str, str | None, float]:
56
- audio = AudioSegment.from_file(audio_path)
57
- duration_sec = float(audio.duration_seconds)
58
- target_sr = 16000
59
- processed_audio_path: str | None = None
60
-
61
- original_frame_rate = audio.frame_rate
62
- original_channels = audio.channels
63
 
64
- if audio.frame_rate != target_sr:
65
- audio = audio.set_frame_rate(target_sr)
66
- if audio.channels == 2:
67
- audio = audio.set_channels(1)
68
- elif audio.channels > 2:
69
- raise ValueError(f"Audio has {audio.channels} channels. Only mono (1) or stereo (2) supported.")
70
 
71
- needs_export = (audio.frame_rate != original_frame_rate) or (audio.channels != original_channels)
72
- if needs_export:
73
- processed_audio_path = str(Path(session_dir) / f"{Path(audio_path).stem}_resampled.wav")
74
- audio.export(processed_audio_path, format="wav")
75
- return processed_audio_path, processed_audio_path, duration_sec
76
- return audio_path, processed_audio_path, duration_sec
77
 
78
 
79
- def build_segment_rows(segment_timestamps: list[dict[str, Any]]) -> list[list[str]]:
80
- return [[f"{ts['start']:.2f}", f"{ts['end']:.2f}", ts['segment']] for ts in segment_timestamps]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
 
83
- def transcribe_audio(audio_path: str, session_dir: str, runtime_device: str) -> dict[str, Any]:
84
- if not audio_path:
85
- raise ValueError("No audio file path provided for transcription.")
 
 
 
 
 
 
 
 
 
 
 
86
 
87
- actual_device = runtime_device
88
- processed_audio_path: str | None = None
89
- transcribe_path = audio_path
90
- duration_sec = 0.0
91
- long_audio_settings_applied = False
92
 
 
 
 
 
 
93
  try:
94
- transcribe_path, processed_audio_path, duration_sec = _prepare_audio(audio_path, session_dir)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
- MODEL.to(actual_device)
97
- MODEL.to(torch.float32)
 
 
98
 
99
- if duration_sec > 480:
100
  try:
101
- MODEL.change_attention_model("rel_pos_local_attn", [256, 256])
102
- MODEL.change_subsampling_conv_chunking_factor(1)
103
- long_audio_settings_applied = True
104
- except Exception:
105
- long_audio_settings_applied = False
106
-
107
- if actual_device == "cuda":
108
- MODEL.to(torch.bfloat16)
109
-
110
- output = MODEL.transcribe([transcribe_path], timestamps=True)
111
-
112
- if (
113
- not output
114
- or not isinstance(output, list)
115
- or not output[0]
116
- or not hasattr(output[0], "timestamp")
117
- or not output[0].timestamp
118
- or "segment" not in output[0].timestamp
119
- ):
120
- raise RuntimeError("Transcription failed or produced unexpected output format.")
121
-
122
- segment_timestamps = output[0].timestamp["segment"]
123
- text = " ".join(ts["segment"].strip() for ts in segment_timestamps)
124
 
125
- return {
126
- "model": MODEL_NAME,
127
- "audio_file": audio_path,
128
- "device": actual_device,
129
- "duration_seconds": duration_sec,
130
- "long_audio": {
131
- "threshold_seconds": 480,
132
- "applied": long_audio_settings_applied,
133
- },
134
- "segment_count": len(segment_timestamps),
135
- "segments": segment_timestamps,
136
- "text": text,
137
- }
138
  except torch.cuda.OutOfMemoryError as exc:
139
- raise RuntimeError("CUDA out of memory while running Parakeet transcription.") from exc
 
 
140
  finally:
 
 
 
 
 
 
141
  try:
142
- if long_audio_settings_applied:
143
- MODEL.change_attention_model("rel_pos")
144
- MODEL.change_subsampling_conv_chunking_factor(-1)
145
- except Exception:
146
- pass
147
- try:
148
- if actual_device == "cuda":
149
- MODEL.cpu()
150
  gc.collect()
151
- if actual_device == "cuda":
152
  torch.cuda.empty_cache()
153
- except Exception:
154
- pass
155
- if processed_audio_path and os.path.exists(processed_audio_path):
156
- try:
157
- os.remove(processed_audio_path)
158
- except Exception:
159
- pass
160
 
161
-
162
- def transcribe_audio_cpu(audio_path: str, session_dir: str) -> dict[str, Any]:
163
- return transcribe_audio(audio_path=audio_path, session_dir=session_dir, runtime_device="cpu")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
  import gc
4
+ import time
 
5
  from typing import Any
6
 
7
+ from src.parakeet.config import PARAKEET_MODEL_ID, PARAKEET_MODEL_LABEL, PARAKEET_SINGLETON_KEY
8
+ from src.runtime.errors import DependencyLoadError, InferenceError
9
+ from src.runtime.json_utils import serialize_for_json
10
+ from src.runtime.preload import get_or_create_singleton, peek_singleton
11
 
 
12
 
13
+ def _load_parakeet_model() -> Any:
14
+ try:
15
+ import nemo.collections.asr as nemo_asr
16
+ except Exception as exc:
17
+ raise DependencyLoadError(f"Failed to import NeMo ASR for {PARAKEET_MODEL_LABEL}: {exc}") from exc
 
 
 
 
 
 
 
 
 
18
 
19
+ try:
20
+ model = nemo_asr.models.ASRModel.from_pretrained(model_name=PARAKEET_MODEL_ID)
21
+ except Exception as exc:
22
+ raise DependencyLoadError(f"Failed to download/load {PARAKEET_MODEL_ID}: {exc}") from exc
23
 
 
 
 
24
  try:
25
+ model.eval()
26
+ model.cpu()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  except Exception:
28
+ # Some wrappers may not expose cpu() until first device placement.
29
+ pass
30
+ return model
 
 
 
 
 
 
 
 
31
 
 
 
 
 
 
 
32
 
33
+ def get_cached_parakeet_model() -> Any:
34
+ model = peek_singleton(PARAKEET_SINGLETON_KEY)
35
+ if model is None:
36
+ raise DependencyLoadError("Parakeet model is not loaded. Call preload_parakeet_model() first.")
37
+ return model
 
38
 
39
 
40
+ def preload_parakeet_model(strict: bool = True) -> dict[str, Any]:
41
+ started_at = time.perf_counter()
42
+ try:
43
+ _, cache_hit = get_or_create_singleton(PARAKEET_SINGLETON_KEY, _load_parakeet_model)
44
+ return {
45
+ "ok": True,
46
+ "model": PARAKEET_MODEL_LABEL,
47
+ "model_id": PARAKEET_MODEL_ID,
48
+ "cache_hit": cache_hit,
49
+ "load_seconds": round(time.perf_counter() - started_at, 4),
50
+ }
51
+ except Exception as exc:
52
+ if strict:
53
+ raise
54
+ return {
55
+ "ok": False,
56
+ "model": PARAKEET_MODEL_LABEL,
57
+ "model_id": PARAKEET_MODEL_ID,
58
+ "cache_hit": False,
59
+ "load_seconds": round(time.perf_counter() - started_at, 4),
60
+ "error": str(exc),
61
+ }
62
 
63
 
64
+ def _resolve_torch_dtype(torch_module: Any, dtype_name: str | None) -> Any:
65
+ normalized = (dtype_name or "").strip().lower()
66
+ mapping = {
67
+ "float32": torch_module.float32,
68
+ "float16": torch_module.float16,
69
+ "fp16": torch_module.float16,
70
+ "bfloat16": torch_module.bfloat16,
71
+ "bf16": torch_module.bfloat16,
72
+ }
73
+ if not normalized:
74
+ return None
75
+ if normalized not in mapping:
76
+ raise InferenceError(f"Unsupported compute_dtype: {dtype_name}")
77
+ return mapping[normalized]
78
 
 
 
 
 
 
79
 
80
+ def run_parakeet_inference(
81
+ audio_file: str,
82
+ duration_seconds: float | None,
83
+ model_options: dict[str, Any],
84
+ ) -> dict[str, Any]:
85
  try:
86
+ import torch
87
+ except Exception as exc:
88
+ raise DependencyLoadError(f"Failed to import torch: {exc}") from exc
89
+
90
+ model = get_cached_parakeet_model()
91
+ device = "cuda" if torch.cuda.is_available() else "cpu"
92
+ dtype_name = str(model_options.get("compute_dtype", "bfloat16"))
93
+ dtype = _resolve_torch_dtype(torch, dtype_name)
94
+
95
+ long_audio_threshold_seconds = float(model_options.get("long_audio_threshold_seconds", 480.0))
96
+ enable_long_audio_optimizations = bool(model_options.get("enable_long_audio_optimizations", True))
97
+ local_attention_left = int(model_options.get("local_attention_left", 256))
98
+ local_attention_right = int(model_options.get("local_attention_right", 256))
99
+ subsampling_conv_chunking_factor = int(model_options.get("subsampling_conv_chunking_factor", 1))
100
+ use_timestamps = bool(model_options.get("timestamps", True))
101
+ is_long_audio = duration_seconds is not None and duration_seconds > long_audio_threshold_seconds
102
+
103
+ applied_long_audio_settings = False
104
+ cleanup_notes: list[str] = []
105
+ inference_started_at = time.perf_counter()
106
 
107
+ try:
108
+ model.to(device)
109
+ if dtype is not None:
110
+ model.to(dtype)
111
 
112
+ if enable_long_audio_optimizations and is_long_audio:
113
  try:
114
+ model.change_attention_model("rel_pos_local_attn", [local_attention_left, local_attention_right])
115
+ model.change_subsampling_conv_chunking_factor(subsampling_conv_chunking_factor)
116
+ applied_long_audio_settings = True
117
+ except Exception as exc:
118
+ cleanup_notes.append(f"long_audio_optimizations_not_applied: {exc}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
+ transcribe_kwargs = {"timestamps": use_timestamps}
121
+ try:
122
+ outputs = model.transcribe([audio_file], use_lhotse=False, **transcribe_kwargs)
123
+ except TypeError:
124
+ outputs = model.transcribe([audio_file], **transcribe_kwargs)
 
 
 
 
 
 
 
 
125
  except torch.cuda.OutOfMemoryError as exc:
126
+ raise InferenceError("CUDA out of memory while running Parakeet transcription.") from exc
127
+ except Exception as exc:
128
+ raise InferenceError(f"Parakeet inference failed: {exc}") from exc
129
  finally:
130
+ if applied_long_audio_settings:
131
+ try:
132
+ model.change_attention_model("rel_pos")
133
+ model.change_subsampling_conv_chunking_factor(-1)
134
+ except Exception as exc:
135
+ cleanup_notes.append(f"long_audio_reset_failed: {exc}")
136
  try:
137
+ if device == "cuda":
138
+ model.cpu()
 
 
 
 
 
 
139
  gc.collect()
140
+ if device == "cuda":
141
  torch.cuda.empty_cache()
142
+ except Exception as exc:
143
+ cleanup_notes.append(f"cleanup_failed: {exc}")
 
 
 
 
 
144
 
145
+ serialized_item = serialize_for_json(outputs[0] if outputs else None)
146
+ return {
147
+ "raw_output": {
148
+ "result": serialized_item,
149
+ "runtime": {
150
+ "device": device,
151
+ "compute_dtype": dtype_name,
152
+ "timestamps": use_timestamps,
153
+ "long_audio": {
154
+ "duration_seconds": duration_seconds,
155
+ "threshold_seconds": long_audio_threshold_seconds,
156
+ "is_long_audio": is_long_audio,
157
+ "enabled": enable_long_audio_optimizations,
158
+ "applied": applied_long_audio_settings,
159
+ "local_attention_left": local_attention_left,
160
+ "local_attention_right": local_attention_right,
161
+ "subsampling_conv_chunking_factor": subsampling_conv_chunking_factor,
162
+ },
163
+ "cleanup_notes": cleanup_notes,
164
+ },
165
+ },
166
+ "timing": {
167
+ "inference_seconds": round(time.perf_counter() - inference_started_at, 4),
168
+ },
169
+ }
src/parakeet/service.py CHANGED
@@ -1,29 +1,41 @@
1
  from __future__ import annotations
2
 
3
- import shutil
4
- import tempfile
5
- from pathlib import Path
6
  from typing import Any, Callable
7
  from uuid import uuid4
8
 
9
- from src.runtime.audio import ensure_audio_file_exists
 
 
 
10
  from src.runtime.json_utils import parse_json_object
11
  from src.runtime.logging import get_logger, log_event
 
12
 
13
  logger = get_logger(__name__)
14
 
15
- GpuRunner = Callable[[str, str], dict[str, Any]]
16
 
17
 
18
  def build_effective_model_options(user_options: dict[str, Any]) -> dict[str, Any]:
19
- return dict(user_options)
 
 
20
 
21
 
22
  def build_parakeet_execution_plan(duration_seconds: float, model_options: dict[str, Any]) -> dict[str, Any]:
 
 
23
  return {
24
- "mode": "reference_space_replication",
25
- "duration_seconds": duration_seconds,
26
- "model_options": model_options,
 
 
 
 
 
 
 
27
  }
28
 
29
 
@@ -33,16 +45,54 @@ def run_transcribe_parakeet_request(
33
  gpu_runner: GpuRunner,
34
  ) -> dict[str, Any]:
35
  request_id = str(uuid4())
 
 
 
 
 
36
  audio_path = ensure_audio_file_exists(audio_file)
37
  model_options = build_effective_model_options(parse_json_object(model_options_json))
38
- session_dir = Path(tempfile.mkdtemp(prefix=f"parakeet_{request_id}_"))
39
-
40
- log_event(logger, "parakeet.request.start", request_id=request_id, audio_file=str(audio_path))
41
- try:
42
- response = gpu_runner(str(audio_path), str(session_dir))
43
- response["request_id"] = request_id
44
- response["execution_plan"] = build_parakeet_execution_plan(response.get("duration_seconds", 0.0), model_options)
45
- log_event(logger, "parakeet.request.end", request_id=request_id, duration_seconds=response.get("duration_seconds"))
46
- return response
47
- finally:
48
- shutil.rmtree(session_dir, ignore_errors=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
 
 
 
3
  from typing import Any, Callable
4
  from uuid import uuid4
5
 
6
+ from src.parakeet.config import DEFAULT_MODEL_OPTIONS, PARAKEET_MODEL_ID, PARAKEET_MODEL_LABEL
7
+ from src.parakeet.model import preload_parakeet_model
8
+ from src.parakeet.normalize import normalize_parakeet_result
9
+ from src.runtime.audio import ensure_audio_file_exists, probe_audio_duration_seconds
10
  from src.runtime.json_utils import parse_json_object
11
  from src.runtime.logging import get_logger, log_event
12
+ from src.runtime.timing import Stopwatch
13
 
14
  logger = get_logger(__name__)
15
 
16
+ GpuRunner = Callable[[str, float | None, dict[str, Any]], dict[str, Any]]
17
 
18
 
19
  def build_effective_model_options(user_options: dict[str, Any]) -> dict[str, Any]:
20
+ options = dict(DEFAULT_MODEL_OPTIONS)
21
+ options.update(user_options)
22
+ return options
23
 
24
 
25
  def build_parakeet_execution_plan(duration_seconds: float, model_options: dict[str, Any]) -> dict[str, Any]:
26
+ threshold = float(model_options.get("long_audio_threshold_seconds", DEFAULT_MODEL_OPTIONS["long_audio_threshold_seconds"]))
27
+ is_long_audio = duration_seconds > threshold
28
  return {
29
+ "mode": "single_pass",
30
+ "chunking": {
31
+ "implemented": False,
32
+ "reason": "Phase 1 scaffold keeps chunk planning on CPU but does not split audio yet.",
33
+ },
34
+ "long_audio": {
35
+ "threshold_seconds": threshold,
36
+ "is_long_audio": is_long_audio,
37
+ "optimizations_requested": bool(model_options.get("enable_long_audio_optimizations", True)),
38
+ },
39
  }
40
 
41
 
 
45
  gpu_runner: GpuRunner,
46
  ) -> dict[str, Any]:
47
  request_id = str(uuid4())
48
+ total_timer = Stopwatch()
49
+
50
+ log_event(logger, "parakeet.request.start", request_id=request_id, audio_file=audio_file)
51
+
52
+ probe_timer = Stopwatch()
53
  audio_path = ensure_audio_file_exists(audio_file)
54
  model_options = build_effective_model_options(parse_json_object(model_options_json))
55
+ duration_seconds = probe_audio_duration_seconds(str(audio_path))
56
+ execution_plan = build_parakeet_execution_plan(duration_seconds, model_options)
57
+ probe_seconds = probe_timer.elapsed_seconds()
58
+
59
+ preload_timer = Stopwatch()
60
+ preload_info = preload_parakeet_model(strict=True)
61
+ preload_seconds = preload_timer.elapsed_seconds()
62
+
63
+ gpu_response = gpu_runner(str(audio_path), duration_seconds, model_options)
64
+
65
+ postprocess_timer = Stopwatch()
66
+ normalized_output = normalize_parakeet_result(gpu_response["raw_output"]["result"])
67
+ postprocess_seconds = postprocess_timer.elapsed_seconds()
68
+
69
+ response = {
70
+ "phase": 1,
71
+ "request_id": request_id,
72
+ "model": PARAKEET_MODEL_LABEL,
73
+ "model_id": PARAKEET_MODEL_ID,
74
+ "audio_file": str(audio_path),
75
+ "duration_seconds": duration_seconds,
76
+ "execution_plan": execution_plan,
77
+ "preload": preload_info,
78
+ "zerogpu_timing": gpu_response["zerogpu_timing"],
79
+ "wall_clock_timing": {
80
+ "probe_seconds": probe_seconds,
81
+ "preload_seconds": preload_seconds,
82
+ "postprocess_seconds": postprocess_seconds,
83
+ "total_seconds": total_timer.elapsed_seconds(),
84
+ },
85
+ "raw_output": gpu_response["raw_output"],
86
+ "normalized_output": normalized_output,
87
+ }
88
+
89
+ log_event(
90
+ logger,
91
+ "parakeet.request.end",
92
+ request_id=request_id,
93
+ duration_seconds=duration_seconds,
94
+ zerogpu_timing=response["zerogpu_timing"],
95
+ wall_clock_timing=response["wall_clock_timing"],
96
+ preload_cache_hit=preload_info.get("cache_hit"),
97
+ )
98
+ return response
tests/test_parakeet_service.py CHANGED
@@ -4,18 +4,22 @@ from src.parakeet.service import build_effective_model_options, build_parakeet_e
4
 
5
 
6
  class ParakeetServiceTests(unittest.TestCase):
7
- def test_build_effective_model_options_returns_user_options(self) -> None:
8
  options = build_effective_model_options({"compute_dtype": "float16"})
9
  self.assertEqual(options["compute_dtype"], "float16")
 
10
 
11
- def test_build_parakeet_execution_plan_marks_reference_mode(self) -> None:
12
  plan = build_parakeet_execution_plan(
13
  duration_seconds=900.0,
14
- model_options={"timestamps": True},
 
 
 
15
  )
16
- self.assertEqual(plan["mode"], "reference_space_replication")
17
- self.assertEqual(plan["duration_seconds"], 900.0)
18
- self.assertTrue(plan["model_options"]["timestamps"])
19
 
20
 
21
  if __name__ == "__main__":
 
4
 
5
 
6
  class ParakeetServiceTests(unittest.TestCase):
7
+ def test_build_effective_model_options_merges_defaults(self) -> None:
8
  options = build_effective_model_options({"compute_dtype": "float16"})
9
  self.assertEqual(options["compute_dtype"], "float16")
10
+ self.assertTrue(options["timestamps"])
11
 
12
+ def test_build_parakeet_execution_plan_marks_long_audio(self) -> None:
13
  plan = build_parakeet_execution_plan(
14
  duration_seconds=900.0,
15
+ model_options={
16
+ "long_audio_threshold_seconds": 480.0,
17
+ "enable_long_audio_optimizations": True,
18
+ },
19
  )
20
+ self.assertEqual(plan["mode"], "single_pass")
21
+ self.assertTrue(plan["long_audio"]["is_long_audio"])
22
+ self.assertFalse(plan["chunking"]["implemented"])
23
 
24
 
25
  if __name__ == "__main__":