voidful commited on
Commit
5d30c6f
·
verified ·
1 Parent(s): 8474679

Instrument request latency by verification stage

Browse files
app.py CHANGED
@@ -126,6 +126,11 @@ from glyph_hybrid_adapter import (
126
  HardenedGlyphHybridAdapter,
127
  )
128
  from glyph_hybrid_evidence import GatePolicy as GlyphEvidenceGatePolicy
 
 
 
 
 
129
  from production import (
130
  StopHysteresisController,
131
  GenerationChunkSpec,
@@ -532,25 +537,30 @@ def _get_ecapa_encoder():
532
  global _ECAPA_ENCODER
533
  with _ECAPA_LOCK:
534
  if _ECAPA_ENCODER is None:
535
- import torchaudio
536
-
537
- # SpeechBrain 1.0.3 still probes this API during import, while the
538
- # ZeroGPU torchaudio build has removed it. Audio loading below is
539
- # handled by librosa, so an empty compatibility result is correct.
540
- if not hasattr(torchaudio, "list_audio_backends"):
541
- torchaudio.list_audio_backends = lambda: []
542
- from speechbrain.inference.speaker import EncoderClassifier
543
-
544
- cache_dir = os.path.join(os.environ.get("HF_HOME", "/tmp"), "speechbrain", "ecapa")
545
- _ECAPA_ENCODER = EncoderClassifier.from_hparams(
546
- source=ECAPA_DIR,
547
- # The upstream hyperparams otherwise points back to the repo
548
- # and SpeechBrain 1.0.3 uses a removed Hub keyword. Keep every
549
- # weight fetch inside the already pinned local snapshot.
550
- overrides={"pretrained_path": ECAPA_DIR},
551
- savedir=cache_dir,
552
- run_opts={"device": "cpu"},
553
- )
 
 
 
 
 
554
  return _ECAPA_ENCODER
555
 
556
 
@@ -756,11 +766,15 @@ def _generate_chunk(
756
  expected_steps=expected_steps,
757
  hard_stop_steps=hard_stop_steps,
758
  )
759
- try:
760
- audio = model.generate(**kwargs)
761
- finally:
762
- if _STOP_CONTROLLER is not None:
763
- _STOP_CONTROLLER.end()
 
 
 
 
764
  stop_reason = (
765
  _STOP_CONTROLLER.last_stop_reason
766
  if _STOP_CONTROLLER is not None
@@ -787,8 +801,6 @@ def _generate_chunk(
787
  f"min_len={min_len} "
788
  f"short_headroom_floor_applied={short_headroom_floor_applied}"
789
  )
790
- audio = audio.detach().float().cpu().numpy().reshape(-1)
791
-
792
  def outcome(waveform: np.ndarray) -> _ChunkGenerationOutcome:
793
  normalized = np.asarray(waveform, dtype=np.float32).reshape(-1)
794
  return _ChunkGenerationOutcome(
@@ -1569,6 +1581,7 @@ def _local_endpoint_role_rows(
1569
  )
1570
 
1571
 
 
1572
  def _assemble_trajectory_audio(
1573
  trajectory: tuple[np.ndarray, ...],
1574
  chunks: tuple[str, ...],
@@ -3428,14 +3441,15 @@ def _synthesize_glyph_hybrid(
3428
  immutable_segments,
3429
  semantic_commitment=commitment,
3430
  )
3431
- assembly = assemble_hybrid_pcm16(
3432
- profile_state.bundle,
3433
- immutable_segments,
3434
- semantic_commitment=commitment,
3435
- expected_plan_sha256=assembly_plan_sha256,
3436
- speed=1.0,
3437
- post_gain=1.0,
3438
- )
 
3439
  verify_hybrid_assembly(
3440
  assembly,
3441
  trusted_bundle=profile_state.bundle,
@@ -3551,6 +3565,7 @@ def _maybe_synthesize_glyph_speaker_request(
3551
 
3552
 
3553
  @gpu
 
3554
  def tts_speaker(
3555
  text: str,
3556
  speaker: str = DEFAULT_SPEAKER,
@@ -3579,6 +3594,7 @@ def tts_speaker(
3579
 
3580
 
3581
  @gpu
 
3582
  def tts_reference(
3583
  text: str,
3584
  reference_wav: str,
@@ -3589,16 +3605,17 @@ def tts_reference(
3589
  if not reference_wav:
3590
  raise gr.Error("請先錄音或上傳參考音檔。")
3591
  try:
3592
- centroid = extract_windowed_speaker_embedding(
3593
- reference_wav,
3594
- _get_ecapa_encoder(),
3595
- device="cpu",
3596
- min_duration_seconds=3.0,
3597
- window_seconds=3.0,
3598
- hop_seconds=1.5,
3599
- max_windows=12,
3600
- full_clip_max_seconds=12.0,
3601
- )
 
3602
  except ValueError as error:
3603
  raise gr.Error(str(error)) from error
3604
  return pcm16_audio_output(
@@ -3607,6 +3624,7 @@ def tts_reference(
3607
 
3608
 
3609
  @gpu
 
3610
  def tts_longform(
3611
  text: str,
3612
  speaker: str = DEFAULT_SPEAKER,
 
126
  HardenedGlyphHybridAdapter,
127
  )
128
  from glyph_hybrid_evidence import GatePolicy as GlyphEvidenceGatePolicy
129
+ from latency_timing import (
130
+ latency_stage,
131
+ timed_latency_request,
132
+ timed_latency_stage,
133
+ )
134
  from production import (
135
  StopHysteresisController,
136
  GenerationChunkSpec,
 
537
  global _ECAPA_ENCODER
538
  with _ECAPA_LOCK:
539
  if _ECAPA_ENCODER is None:
540
+ with latency_stage("ecapa"):
541
+ import torchaudio
542
+
543
+ # SpeechBrain 1.0.3 still probes this API during import, while the
544
+ # ZeroGPU torchaudio build has removed it. Audio loading below is
545
+ # handled by librosa, so an empty compatibility result is correct.
546
+ if not hasattr(torchaudio, "list_audio_backends"):
547
+ torchaudio.list_audio_backends = lambda: []
548
+ from speechbrain.inference.speaker import EncoderClassifier
549
+
550
+ cache_dir = os.path.join(
551
+ os.environ.get("HF_HOME", "/tmp"),
552
+ "speechbrain",
553
+ "ecapa",
554
+ )
555
+ _ECAPA_ENCODER = EncoderClassifier.from_hparams(
556
+ source=ECAPA_DIR,
557
+ # The upstream hyperparams otherwise points back to the repo
558
+ # and SpeechBrain 1.0.3 uses a removed Hub keyword. Keep every
559
+ # weight fetch inside the already pinned local snapshot.
560
+ overrides={"pretrained_path": ECAPA_DIR},
561
+ savedir=cache_dir,
562
+ run_opts={"device": "cpu"},
563
+ )
564
  return _ECAPA_ENCODER
565
 
566
 
 
766
  expected_steps=expected_steps,
767
  hard_stop_steps=hard_stop_steps,
768
  )
769
+ with latency_stage("generation"):
770
+ try:
771
+ audio = model.generate(**kwargs)
772
+ finally:
773
+ if _STOP_CONTROLLER is not None:
774
+ _STOP_CONTROLLER.end()
775
+ # Copying to CPU closes the asynchronous CUDA work inside this stage,
776
+ # so the reported wall time is not shifted into later verification.
777
+ audio = audio.detach().float().cpu().numpy().reshape(-1)
778
  stop_reason = (
779
  _STOP_CONTROLLER.last_stop_reason
780
  if _STOP_CONTROLLER is not None
 
801
  f"min_len={min_len} "
802
  f"short_headroom_floor_applied={short_headroom_floor_applied}"
803
  )
 
 
804
  def outcome(waveform: np.ndarray) -> _ChunkGenerationOutcome:
805
  normalized = np.asarray(waveform, dtype=np.float32).reshape(-1)
806
  return _ChunkGenerationOutcome(
 
1581
  )
1582
 
1583
 
1584
+ @timed_latency_stage("assemble")
1585
  def _assemble_trajectory_audio(
1586
  trajectory: tuple[np.ndarray, ...],
1587
  chunks: tuple[str, ...],
 
3441
  immutable_segments,
3442
  semantic_commitment=commitment,
3443
  )
3444
+ with latency_stage("assemble"):
3445
+ assembly = assemble_hybrid_pcm16(
3446
+ profile_state.bundle,
3447
+ immutable_segments,
3448
+ semantic_commitment=commitment,
3449
+ expected_plan_sha256=assembly_plan_sha256,
3450
+ speed=1.0,
3451
+ post_gain=1.0,
3452
+ )
3453
  verify_hybrid_assembly(
3454
  assembly,
3455
  trusted_bundle=profile_state.bundle,
 
3565
 
3566
 
3567
  @gpu
3568
+ @timed_latency_request
3569
  def tts_speaker(
3570
  text: str,
3571
  speaker: str = DEFAULT_SPEAKER,
 
3594
 
3595
 
3596
  @gpu
3597
+ @timed_latency_request
3598
  def tts_reference(
3599
  text: str,
3600
  reference_wav: str,
 
3605
  if not reference_wav:
3606
  raise gr.Error("請先錄音或上傳參考音檔。")
3607
  try:
3608
+ with latency_stage("ecapa"):
3609
+ centroid = extract_windowed_speaker_embedding(
3610
+ reference_wav,
3611
+ _get_ecapa_encoder(),
3612
+ device="cpu",
3613
+ min_duration_seconds=3.0,
3614
+ window_seconds=3.0,
3615
+ hop_seconds=1.5,
3616
+ max_windows=12,
3617
+ full_clip_max_seconds=12.0,
3618
+ )
3619
  except ValueError as error:
3620
  raise gr.Error(str(error)) from error
3621
  return pcm16_audio_output(
 
3624
 
3625
 
3626
  @gpu
3627
+ @timed_latency_request
3628
  def tts_longform(
3629
  text: str,
3630
  speaker: str = DEFAULT_SPEAKER,
audio_artifact_gate.py CHANGED
@@ -24,6 +24,8 @@ from typing import Any
24
 
25
  import numpy as np
26
 
 
 
27
 
28
  @dataclass(frozen=True)
29
  class EchoSmearingGateConfig:
@@ -268,6 +270,7 @@ def _window_cepstrum(
268
  return np.asarray(cepstrum[: max_delay_samples + 1], dtype=np.float64)
269
 
270
 
 
271
  def evaluate_echo_smearing(
272
  audio: Any,
273
  sample_rate: Any,
 
24
 
25
  import numpy as np
26
 
27
+ from latency_timing import timed_latency_stage
28
+
29
 
30
  @dataclass(frozen=True)
31
  class EchoSmearingGateConfig:
 
270
  return np.asarray(cepstrum[: max_delay_samples + 1], dtype=np.float64)
271
 
272
 
273
+ @timed_latency_stage("echo")
274
  def evaluate_echo_smearing(
275
  audio: Any,
276
  sample_rate: Any,
latency_timing.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Content-free, request-local latency instrumentation.
2
+
3
+ The production Space emits one fixed-schema latency line per public synthesis
4
+ request. Only stage names, elapsed seconds, and call counts are retained; no
5
+ request text, waveform, transcript, seed, or exception data enters this module.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from contextlib import contextmanager
11
+ from contextvars import ContextVar
12
+ from dataclasses import dataclass, field
13
+ from functools import wraps
14
+ from time import perf_counter
15
+ from typing import Callable, Iterator
16
+
17
+
18
+ LATENCY_STAGE_NAMES = (
19
+ "request_total",
20
+ "generation",
21
+ "turbo_asr",
22
+ "large_v3_asr",
23
+ "ecapa",
24
+ "squim",
25
+ "echo",
26
+ "f0",
27
+ "assemble",
28
+ )
29
+ _TIMED_STAGE_NAMES = frozenset(LATENCY_STAGE_NAMES[1:])
30
+ LATENCY_LOG_PREFIX = "[BlueMagpie] latency "
31
+
32
+
33
+ @dataclass
34
+ class RequestLatency:
35
+ """Mutable request-local totals with a fixed, content-free schema."""
36
+
37
+ seconds: dict[str, float] = field(
38
+ default_factory=lambda: {name: 0.0 for name in LATENCY_STAGE_NAMES}
39
+ )
40
+ calls: dict[str, int] = field(
41
+ default_factory=lambda: {name: 0 for name in LATENCY_STAGE_NAMES}
42
+ )
43
+ active_depths: dict[str, int] = field(default_factory=dict)
44
+
45
+ def add(self, stage: str, elapsed_seconds: float) -> None:
46
+ if stage not in LATENCY_STAGE_NAMES:
47
+ raise ValueError("unknown latency stage")
48
+ self.seconds[stage] += max(0.0, float(elapsed_seconds))
49
+ self.calls[stage] += 1
50
+
51
+
52
+ _CURRENT_REQUEST: ContextVar[RequestLatency | None] = ContextVar(
53
+ "bluemagpie_current_request_latency",
54
+ default=None,
55
+ )
56
+
57
+
58
+ def format_latency_log(totals: RequestLatency) -> str:
59
+ """Return the fixed-schema log line without accepting request content."""
60
+
61
+ fields: list[str] = []
62
+ for stage in LATENCY_STAGE_NAMES:
63
+ fields.append(f"{stage}_seconds={totals.seconds[stage]:.6f}")
64
+ fields.append(f"{stage}_calls={totals.calls[stage]}")
65
+ return LATENCY_LOG_PREFIX + " ".join(fields)
66
+
67
+
68
+ @contextmanager
69
+ def latency_request(
70
+ *,
71
+ emit: Callable[[str], object] | None = None,
72
+ ) -> Iterator[RequestLatency]:
73
+ """Open one request timer and emit exactly once at the outermost boundary."""
74
+
75
+ existing = _CURRENT_REQUEST.get()
76
+ if existing is not None:
77
+ yield existing
78
+ return
79
+
80
+ totals = RequestLatency()
81
+ token = _CURRENT_REQUEST.set(totals)
82
+ started = perf_counter()
83
+ try:
84
+ yield totals
85
+ finally:
86
+ totals.add("request_total", perf_counter() - started)
87
+ _CURRENT_REQUEST.reset(token)
88
+ (emit or print)(format_latency_log(totals))
89
+
90
+
91
+ @contextmanager
92
+ def latency_stage(stage: str) -> Iterator[None]:
93
+ """Accumulate one stage call when a public request timer is active.
94
+
95
+ Re-entering the same stage is deliberately folded into its outer call. It
96
+ keeps a high-level ECAPA operation from double-counting a nested lazy load
97
+ or lower-level speaker measurement.
98
+ """
99
+
100
+ if stage not in _TIMED_STAGE_NAMES:
101
+ raise ValueError("unknown latency stage")
102
+ totals = _CURRENT_REQUEST.get()
103
+ if totals is None:
104
+ yield
105
+ return
106
+
107
+ depth = totals.active_depths.get(stage, 0)
108
+ totals.active_depths[stage] = depth + 1
109
+ if depth:
110
+ try:
111
+ yield
112
+ finally:
113
+ totals.active_depths[stage] -= 1
114
+ return
115
+
116
+ started = perf_counter()
117
+ try:
118
+ yield
119
+ finally:
120
+ totals.add(stage, perf_counter() - started)
121
+ totals.active_depths.pop(stage, None)
122
+
123
+
124
+ def timed_latency_stage(stage: str):
125
+ """Decorate a synchronous function as one fixed latency stage."""
126
+
127
+ if stage not in _TIMED_STAGE_NAMES:
128
+ raise ValueError("unknown latency stage")
129
+
130
+ def decorate(function):
131
+ @wraps(function)
132
+ def timed(*args, **kwargs):
133
+ with latency_stage(stage):
134
+ return function(*args, **kwargs)
135
+
136
+ return timed
137
+
138
+ return decorate
139
+
140
+
141
+ def timed_latency_request(function):
142
+ """Decorate one public synchronous synthesis entry point."""
143
+
144
+ @wraps(function)
145
+ def timed(*args, **kwargs):
146
+ with latency_request():
147
+ return function(*args, **kwargs)
148
+
149
+ return timed
quality_runtime.py CHANGED
@@ -22,6 +22,7 @@ import numpy as np
22
  import torch
23
  import torch.nn.functional as torch_functional
24
 
 
25
  from production import (
26
  AsrComparison,
27
  CandidateSequenceSelection,
@@ -499,6 +500,7 @@ def _bounded_squim_windows(waveform: np.ndarray) -> tuple[np.ndarray, ...]:
499
 
500
 
501
  @torch.inference_mode()
 
502
  def squim_objective_evidence_from_audio(
503
  audio: np.ndarray | Sequence[float],
504
  sample_rate: int,
@@ -1078,7 +1080,7 @@ def _split_whisper_audio(
1078
 
1079
 
1080
  @torch.inference_mode()
1081
- def transcribe_whisper(
1082
  audio: np.ndarray | Sequence[float],
1083
  sample_rate: int,
1084
  *,
@@ -1089,7 +1091,7 @@ def transcribe_whisper(
1089
  max_new_tokens: int = 128,
1090
  max_verification_segments: int = WHISPER_MAX_VERIFICATION_SEGMENTS,
1091
  ) -> str:
1092
- """Transcribe ndarray audio using deterministic decoding.
1093
 
1094
  ``runtime`` and ``lazy_asr`` are mutually exclusive injection points. An
1095
  empty decoded string is returned as-is; the semantic verifier will reject
@@ -1144,6 +1146,33 @@ def transcribe_whisper(
1144
  return " ".join(text for text in decoded_segments if text)
1145
 
1146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1147
  def transcribe_verification_whisper(
1148
  audio: np.ndarray | Sequence[float],
1149
  sample_rate: int,
@@ -1160,7 +1189,7 @@ def transcribe_verification_whisper(
1160
  selected_lazy = lazy_asr
1161
  if runtime is None and selected_lazy is None:
1162
  selected_lazy = _DEFAULT_VERIFICATION_WHISPER
1163
- return transcribe_whisper(
1164
  audio,
1165
  sample_rate,
1166
  lazy_asr=selected_lazy,
@@ -1303,6 +1332,7 @@ class SpeakerEvidence:
1303
  active_rms_db: float
1304
 
1305
 
 
1306
  def speaker_evidence_from_audio(
1307
  audio: np.ndarray | Sequence[float],
1308
  sample_rate: int,
@@ -1378,6 +1408,7 @@ def speaker_evidence_from_audio(
1378
  )
1379
 
1380
 
 
1381
  def release_speaker_evidence_from_audio(
1382
  audio: np.ndarray | Sequence[float],
1383
  sample_rate: int,
@@ -1509,6 +1540,7 @@ def endpoint_tail_energy_ratio(
1509
  return float(np.clip(tail_rms / peak, 0.0, 1.0))
1510
 
1511
 
 
1512
  def active_audio_median_f0_hz(
1513
  audio: np.ndarray | Sequence[float],
1514
  sample_rate: int,
 
22
  import torch
23
  import torch.nn.functional as torch_functional
24
 
25
+ from latency_timing import timed_latency_stage
26
  from production import (
27
  AsrComparison,
28
  CandidateSequenceSelection,
 
500
 
501
 
502
  @torch.inference_mode()
503
+ @timed_latency_stage("squim")
504
  def squim_objective_evidence_from_audio(
505
  audio: np.ndarray | Sequence[float],
506
  sample_rate: int,
 
1080
 
1081
 
1082
  @torch.inference_mode()
1083
+ def _transcribe_whisper(
1084
  audio: np.ndarray | Sequence[float],
1085
  sample_rate: int,
1086
  *,
 
1091
  max_new_tokens: int = 128,
1092
  max_verification_segments: int = WHISPER_MAX_VERIFICATION_SEGMENTS,
1093
  ) -> str:
1094
+ """Transcribe ndarray audio using the shared deterministic decoder.
1095
 
1096
  ``runtime`` and ``lazy_asr`` are mutually exclusive injection points. An
1097
  empty decoded string is returned as-is; the semantic verifier will reject
 
1146
  return " ".join(text for text in decoded_segments if text)
1147
 
1148
 
1149
+ @timed_latency_stage("turbo_asr")
1150
+ def transcribe_whisper(
1151
+ audio: np.ndarray | Sequence[float],
1152
+ sample_rate: int,
1153
+ *,
1154
+ lazy_asr: LazyWhisperASR | None = None,
1155
+ runtime: WhisperRuntime | None = None,
1156
+ language: str = "zh",
1157
+ task: str = "transcribe",
1158
+ max_new_tokens: int = 128,
1159
+ max_verification_segments: int = WHISPER_MAX_VERIFICATION_SEGMENTS,
1160
+ ) -> str:
1161
+ """Transcribe with the pinned Turbo candidate-screening ASR."""
1162
+
1163
+ return _transcribe_whisper(
1164
+ audio,
1165
+ sample_rate,
1166
+ lazy_asr=lazy_asr,
1167
+ runtime=runtime,
1168
+ language=language,
1169
+ task=task,
1170
+ max_new_tokens=max_new_tokens,
1171
+ max_verification_segments=max_verification_segments,
1172
+ )
1173
+
1174
+
1175
+ @timed_latency_stage("large_v3_asr")
1176
  def transcribe_verification_whisper(
1177
  audio: np.ndarray | Sequence[float],
1178
  sample_rate: int,
 
1189
  selected_lazy = lazy_asr
1190
  if runtime is None and selected_lazy is None:
1191
  selected_lazy = _DEFAULT_VERIFICATION_WHISPER
1192
+ return _transcribe_whisper(
1193
  audio,
1194
  sample_rate,
1195
  lazy_asr=selected_lazy,
 
1332
  active_rms_db: float
1333
 
1334
 
1335
+ @timed_latency_stage("ecapa")
1336
  def speaker_evidence_from_audio(
1337
  audio: np.ndarray | Sequence[float],
1338
  sample_rate: int,
 
1408
  )
1409
 
1410
 
1411
+ @timed_latency_stage("ecapa")
1412
  def release_speaker_evidence_from_audio(
1413
  audio: np.ndarray | Sequence[float],
1414
  sample_rate: int,
 
1540
  return float(np.clip(tail_rms / peak, 0.0, 1.0))
1541
 
1542
 
1543
+ @timed_latency_stage("f0")
1544
  def active_audio_median_f0_hz(
1545
  audio: np.ndarray | Sequence[float],
1546
  sample_rate: int,
tests/test_glyph_app_integration.py CHANGED
@@ -18,6 +18,7 @@ from glyph_assets import (
18
  compute_ordered_attempt_ledger_sha256,
19
  )
20
  from glyph_hybrid_plan import GlyphHybridPlanError
 
21
  from production import count_speech_units, pcm16_audio_output
22
  from quality_runtime import CandidateGenerationContext
23
 
@@ -31,6 +32,7 @@ class FakeGradioError(Exception):
31
 
32
 
33
  def _isolated_nodes(names: tuple[str, ...], namespace: dict[str, object]):
 
34
  tree = ast.parse(APP_PATH.read_text(encoding="utf-8"))
35
  selected = [
36
  node
 
18
  compute_ordered_attempt_ledger_sha256,
19
  )
20
  from glyph_hybrid_plan import GlyphHybridPlanError
21
+ from latency_timing import latency_stage
22
  from production import count_speech_units, pcm16_audio_output
23
  from quality_runtime import CandidateGenerationContext
24
 
 
32
 
33
 
34
  def _isolated_nodes(names: tuple[str, ...], namespace: dict[str, object]):
35
+ namespace.setdefault("latency_stage", latency_stage)
36
  tree = ast.parse(APP_PATH.read_text(encoding="utf-8"))
37
  selected = [
38
  node
tests/test_inference_cleanup.py CHANGED
@@ -7,6 +7,7 @@ import numpy as np
7
  import pytest
8
  import torch
9
 
 
10
  from production import count_speech_units
11
  from quality_runtime import (
12
  CandidateObservation,
@@ -21,6 +22,8 @@ ROOT = Path(__file__).resolve().parents[1]
21
 
22
 
23
  def _isolated_app_function(name, namespace):
 
 
24
  app_path = ROOT / "app.py"
25
  tree = ast.parse(app_path.read_text(encoding="utf-8"))
26
  function = next(
 
7
  import pytest
8
  import torch
9
 
10
+ from latency_timing import latency_stage, timed_latency_stage
11
  from production import count_speech_units
12
  from quality_runtime import (
13
  CandidateObservation,
 
22
 
23
 
24
  def _isolated_app_function(name, namespace):
25
+ namespace.setdefault("latency_stage", latency_stage)
26
+ namespace.setdefault("timed_latency_stage", timed_latency_stage)
27
  app_path = ROOT / "app.py"
28
  tree = ast.parse(app_path.read_text(encoding="utf-8"))
29
  function = next(
tests/test_latency_timing.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ from pathlib import Path
3
+ from types import SimpleNamespace
4
+
5
+ import numpy as np
6
+ import pytest
7
+ import torch
8
+
9
+ import latency_timing
10
+ from quality_runtime import (
11
+ WhisperRuntime,
12
+ transcribe_verification_whisper,
13
+ transcribe_whisper,
14
+ )
15
+
16
+
17
+ ROOT = Path(__file__).resolve().parents[1]
18
+
19
+
20
+ def _clock(monkeypatch, values):
21
+ ticks = iter(values)
22
+ monkeypatch.setattr(latency_timing, "perf_counter", lambda: next(ticks))
23
+
24
+
25
+ def _fields(line):
26
+ assert line.startswith(latency_timing.LATENCY_LOG_PREFIX)
27
+ return dict(
28
+ field.split("=", 1)
29
+ for field in line.removeprefix(latency_timing.LATENCY_LOG_PREFIX).split()
30
+ )
31
+
32
+
33
+ def test_request_timer_emits_fixed_content_free_cumulative_schema(monkeypatch):
34
+ _clock(monkeypatch, (10.0, 11.0, 13.5, 14.0, 15.0, 20.0))
35
+ emitted = []
36
+
37
+ with latency_timing.latency_request(emit=emitted.append) as totals:
38
+ with latency_timing.latency_stage("generation"):
39
+ pass
40
+ with latency_timing.latency_stage("turbo_asr"):
41
+ pass
42
+
43
+ assert len(emitted) == 1
44
+ fields = _fields(emitted[0])
45
+ assert tuple(fields) == tuple(
46
+ item
47
+ for stage in latency_timing.LATENCY_STAGE_NAMES
48
+ for item in (f"{stage}_seconds", f"{stage}_calls")
49
+ )
50
+ assert fields["request_total_seconds"] == "10.000000"
51
+ assert fields["request_total_calls"] == "1"
52
+ assert fields["generation_seconds"] == "2.500000"
53
+ assert fields["generation_calls"] == "1"
54
+ assert fields["turbo_asr_seconds"] == "1.000000"
55
+ assert fields["turbo_asr_calls"] == "1"
56
+ assert fields["large_v3_asr_seconds"] == "0.000000"
57
+ assert fields["large_v3_asr_calls"] == "0"
58
+ assert totals.active_depths == {}
59
+
60
+
61
+ def test_nested_request_and_same_stage_emit_and_count_only_once(monkeypatch):
62
+ _clock(monkeypatch, (0.0, 1.0, 5.0, 6.0))
63
+ emitted = []
64
+
65
+ with latency_timing.latency_request(emit=emitted.append):
66
+ with latency_timing.latency_request(emit=emitted.append):
67
+ with latency_timing.latency_stage("ecapa"):
68
+ with latency_timing.latency_stage("ecapa"):
69
+ pass
70
+
71
+ fields = _fields(emitted[0])
72
+ assert len(emitted) == 1
73
+ assert fields["request_total_seconds"] == "6.000000"
74
+ assert fields["request_total_calls"] == "1"
75
+ assert fields["ecapa_seconds"] == "4.000000"
76
+ assert fields["ecapa_calls"] == "1"
77
+
78
+
79
+ def test_public_decorator_logs_on_failure_without_request_arguments(
80
+ monkeypatch,
81
+ capsys,
82
+ ):
83
+ _clock(monkeypatch, (2.0, 3.0))
84
+
85
+ @latency_timing.timed_latency_request
86
+ def fail(private_value):
87
+ raise RuntimeError(private_value)
88
+
89
+ with pytest.raises(RuntimeError, match="private-payload"):
90
+ fail("private-payload")
91
+
92
+ line = capsys.readouterr().out.strip()
93
+ assert line.startswith(latency_timing.LATENCY_LOG_PREFIX)
94
+ assert "private-payload" not in line
95
+ assert _fields(line)["request_total_seconds"] == "1.000000"
96
+
97
+
98
+ class _Processor:
99
+ def __call__(self, audio, **_kwargs):
100
+ rows = len(audio) if isinstance(audio, list) else 1
101
+ return SimpleNamespace(
102
+ input_features=torch.ones((rows, 4, 4)),
103
+ attention_mask=torch.ones((rows, 4), dtype=torch.long),
104
+ )
105
+
106
+ def batch_decode(self, token_ids, **_kwargs):
107
+ return ["內容完整"] * int(token_ids.shape[0])
108
+
109
+
110
+ class _Model:
111
+ def generate(self, features, **_kwargs):
112
+ return torch.ones((features.shape[0], 2), dtype=torch.long)
113
+
114
+
115
+ def test_dual_asr_wrappers_accumulate_under_distinct_stage_names():
116
+ runtime = WhisperRuntime(
117
+ processor=_Processor(),
118
+ model=_Model(),
119
+ device=torch.device("cpu"),
120
+ dtype=torch.float32,
121
+ )
122
+ emitted = []
123
+ waveform = np.ones(4_000, dtype=np.float32)
124
+
125
+ with latency_timing.latency_request(emit=emitted.append):
126
+ assert transcribe_whisper(waveform, 16_000, runtime=runtime) == "內容完整"
127
+ assert (
128
+ transcribe_verification_whisper(
129
+ waveform,
130
+ 16_000,
131
+ runtime=runtime,
132
+ )
133
+ == "內容完整"
134
+ )
135
+
136
+ fields = _fields(emitted[0])
137
+ assert fields["turbo_asr_calls"] == "1"
138
+ assert fields["large_v3_asr_calls"] == "1"
139
+ assert float(fields["turbo_asr_seconds"]) >= 0.0
140
+ assert float(fields["large_v3_asr_seconds"]) >= 0.0
141
+
142
+
143
+ def _decorator_stage(node):
144
+ for decorator in node.decorator_list:
145
+ if (
146
+ isinstance(decorator, ast.Call)
147
+ and isinstance(decorator.func, ast.Name)
148
+ and decorator.func.id == "timed_latency_stage"
149
+ and len(decorator.args) == 1
150
+ and isinstance(decorator.args[0], ast.Constant)
151
+ ):
152
+ return decorator.args[0].value
153
+ return None
154
+
155
+
156
+ def test_space_wires_every_requested_latency_stage_without_gate_changes():
157
+ app_tree = ast.parse((ROOT / "app.py").read_text(encoding="utf-8"))
158
+ quality_tree = ast.parse(
159
+ (ROOT / "quality_runtime.py").read_text(encoding="utf-8")
160
+ )
161
+ echo_tree = ast.parse(
162
+ (ROOT / "audio_artifact_gate.py").read_text(encoding="utf-8")
163
+ )
164
+ app_functions = {
165
+ node.name: node for node in app_tree.body if isinstance(node, ast.FunctionDef)
166
+ }
167
+ quality_functions = {
168
+ node.name: node
169
+ for node in quality_tree.body
170
+ if isinstance(node, ast.FunctionDef)
171
+ }
172
+ echo_functions = {
173
+ node.name: node
174
+ for node in echo_tree.body
175
+ if isinstance(node, ast.FunctionDef)
176
+ }
177
+
178
+ assert _decorator_stage(app_functions["_assemble_trajectory_audio"]) == "assemble"
179
+ generate_source = ast.get_source_segment(
180
+ (ROOT / "app.py").read_text(encoding="utf-8"),
181
+ app_functions["_generate_chunk"],
182
+ )
183
+ assert generate_source is not None
184
+ assert 'with latency_stage("generation"):' in generate_source
185
+ assert _decorator_stage(quality_functions["transcribe_whisper"]) == "turbo_asr"
186
+ assert (
187
+ _decorator_stage(quality_functions["transcribe_verification_whisper"])
188
+ == "large_v3_asr"
189
+ )
190
+ assert _decorator_stage(quality_functions["speaker_evidence_from_audio"]) == "ecapa"
191
+ assert (
192
+ _decorator_stage(quality_functions["release_speaker_evidence_from_audio"])
193
+ == "ecapa"
194
+ )
195
+ assert (
196
+ _decorator_stage(quality_functions["squim_objective_evidence_from_audio"])
197
+ == "squim"
198
+ )
199
+ assert _decorator_stage(quality_functions["active_audio_median_f0_hz"]) == "f0"
200
+ assert _decorator_stage(echo_functions["evaluate_echo_smearing"]) == "echo"
201
+
202
+ for name in ("tts_speaker", "tts_reference", "tts_longform"):
203
+ decorators = {
204
+ decorator.id
205
+ for decorator in app_functions[name].decorator_list
206
+ if isinstance(decorator, ast.Name)
207
+ }
208
+ assert decorators == {"gpu", "timed_latency_request"}
tests/test_release_pins.py CHANGED
@@ -17,6 +17,7 @@ from production import (
17
  punctuation_pause_seconds,
18
  split_quality_text_for_tts,
19
  )
 
20
  from quality_runtime import LocalIndependentGateEvidence
21
 
22
 
@@ -115,6 +116,7 @@ def _isolated_assemble_trajectory_audio():
115
  "count_speech_units": count_speech_units,
116
  "finish_audio": lambda waveform, _sample_rate, **_kwargs: waveform,
117
  "pcm16_verification_waveform": pcm16_verification_waveform,
 
118
  }
119
  exec(compile(module, str(app_path), "exec"), namespace)
120
  return namespace["_assemble_trajectory_audio"]
 
17
  punctuation_pause_seconds,
18
  split_quality_text_for_tts,
19
  )
20
+ from latency_timing import timed_latency_stage
21
  from quality_runtime import LocalIndependentGateEvidence
22
 
23
 
 
116
  "count_speech_units": count_speech_units,
117
  "finish_audio": lambda waveform, _sample_rate, **_kwargs: waveform,
118
  "pcm16_verification_waveform": pcm16_verification_waveform,
119
+ "timed_latency_stage": timed_latency_stage,
120
  }
121
  exec(compile(module, str(app_path), "exec"), namespace)
122
  return namespace["_assemble_trajectory_audio"]