Spaces:
Running on Zero
Running on Zero
Instrument request latency by verification stage
Browse files- app.py +62 -44
- audio_artifact_gate.py +3 -0
- latency_timing.py +149 -0
- quality_runtime.py +35 -3
- tests/test_glyph_app_integration.py +2 -0
- tests/test_inference_cleanup.py +3 -0
- tests/test_latency_timing.py +208 -0
- tests/test_release_pins.py +2 -0
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 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 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 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
_STOP_CONTROLLER
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 3432 |
-
|
| 3433 |
-
|
| 3434 |
-
|
| 3435 |
-
|
| 3436 |
-
|
| 3437 |
-
|
| 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 |
-
|
| 3593 |
-
|
| 3594 |
-
|
| 3595 |
-
|
| 3596 |
-
|
| 3597 |
-
|
| 3598 |
-
|
| 3599 |
-
|
| 3600 |
-
|
| 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
|
| 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
|
| 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
|
| 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"]
|