Spaces:
Sleeping
Sleeping
Commit ·
8a22550
1
Parent(s): ccb0e29
Fix WS recv race + session recycle 20 with async overlap
Browse files- Fix control_task lifecycle: properly await/cancel before returning to
outer WS loop, preventing 'cannot call recv' race condition
- Session recycling every 20 sentences (was 200) with async overlap:
new ONNX session pre-built in background thread while current session
finishes its prefetch queue
- Add soundfile dep for FLAC encoding support
- backend/pyproject.toml +1 -0
- backend/requirements.txt +1 -0
- backend/server.py +61 -9
- backend/tts.py +137 -49
- backend/uv.lock +81 -0
backend/pyproject.toml
CHANGED
|
@@ -13,6 +13,7 @@ dependencies = [
|
|
| 13 |
"onnxruntime>=1.20.0",
|
| 14 |
"kokoro-onnx>=0.2.6",
|
| 15 |
"requests>=2.32.0",
|
|
|
|
| 16 |
]
|
| 17 |
|
| 18 |
[tool.uv]
|
|
|
|
| 13 |
"onnxruntime>=1.20.0",
|
| 14 |
"kokoro-onnx>=0.2.6",
|
| 15 |
"requests>=2.32.0",
|
| 16 |
+
"soundfile>=0.12.0",
|
| 17 |
]
|
| 18 |
|
| 19 |
[tool.uv]
|
backend/requirements.txt
CHANGED
|
@@ -7,3 +7,4 @@ numpy>=1.26.0
|
|
| 7 |
onnxruntime>=1.20.0
|
| 8 |
kokoro-onnx>=0.2.6
|
| 9 |
requests>=2.32.0
|
|
|
|
|
|
| 7 |
onnxruntime>=1.20.0
|
| 8 |
kokoro-onnx>=0.2.6
|
| 9 |
requests>=2.32.0
|
| 10 |
+
soundfile>=0.12.0
|
backend/server.py
CHANGED
|
@@ -381,6 +381,8 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 381 |
last_key = None
|
| 382 |
cumulative_samples = 0
|
| 383 |
sample_rate = app.state.tts.sample_rate
|
|
|
|
|
|
|
| 384 |
try:
|
| 385 |
control_task: asyncio.Task[str] | None = asyncio.create_task(websocket.receive_text())
|
| 386 |
|
|
@@ -456,6 +458,10 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 456 |
await websocket.send_bytes(audio_chunk)
|
| 457 |
cumulative_samples += len(audio_chunk) // 2
|
| 458 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
# Optional realtime pacing.
|
| 460 |
# - streaming: send roughly in-time to reduce client buffer bloat.
|
| 461 |
# - downloads: realtime=false sends as fast as synthesis allows.
|
|
@@ -469,16 +475,62 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 469 |
if sleep_s > 0:
|
| 470 |
await asyncio.sleep(min(sleep_s, 0.25))
|
| 471 |
|
|
|
|
|
|
|
|
|
|
| 472 |
if control_task is not None:
|
| 473 |
-
control_task.
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
except Exception as e:
|
| 483 |
logger.error(f"Play stream error: {e}")
|
| 484 |
try:
|
|
|
|
| 381 |
last_key = None
|
| 382 |
cumulative_samples = 0
|
| 383 |
sample_rate = app.state.tts.sample_rate
|
| 384 |
+
# For downloads, accumulate PCM to encode as FLAC at the end.
|
| 385 |
+
download_pcm_chunks: list[bytes] = [] if not realtime else []
|
| 386 |
try:
|
| 387 |
control_task: asyncio.Task[str] | None = asyncio.create_task(websocket.receive_text())
|
| 388 |
|
|
|
|
| 458 |
await websocket.send_bytes(audio_chunk)
|
| 459 |
cumulative_samples += len(audio_chunk) // 2
|
| 460 |
|
| 461 |
+
# Accumulate PCM for FLAC encoding (downloads only).
|
| 462 |
+
if not realtime:
|
| 463 |
+
download_pcm_chunks.append(audio_chunk)
|
| 464 |
+
|
| 465 |
# Optional realtime pacing.
|
| 466 |
# - streaming: send roughly in-time to reduce client buffer bloat.
|
| 467 |
# - downloads: realtime=false sends as fast as synthesis allows.
|
|
|
|
| 475 |
if sleep_s > 0:
|
| 476 |
await asyncio.sleep(min(sleep_s, 0.25))
|
| 477 |
|
| 478 |
+
# Properly clean up the control_task to avoid
|
| 479 |
+
# concurrent recv race with the outer message loop.
|
| 480 |
+
pending_command = None
|
| 481 |
if control_task is not None:
|
| 482 |
+
if control_task.done():
|
| 483 |
+
try:
|
| 484 |
+
pending_command = control_task.result()
|
| 485 |
+
except Exception:
|
| 486 |
+
pass
|
| 487 |
+
else:
|
| 488 |
+
control_task.cancel()
|
| 489 |
+
try:
|
| 490 |
+
await control_task
|
| 491 |
+
except (asyncio.CancelledError, Exception):
|
| 492 |
+
pass
|
| 493 |
+
control_task = None
|
| 494 |
+
|
| 495 |
+
# For downloads, encode accumulated PCM as FLAC and send.
|
| 496 |
+
if not realtime and download_pcm_chunks and not cancel_event.is_set():
|
| 497 |
+
try:
|
| 498 |
+
all_pcm = b"".join(download_pcm_chunks)
|
| 499 |
+
flac_data = app.state.tts.encode_pcm16_to_flac(
|
| 500 |
+
all_pcm, sample_rate=sample_rate
|
| 501 |
+
)
|
| 502 |
+
is_flac = flac_data[:4] == b"fLaC"
|
| 503 |
+
await websocket.send_json({
|
| 504 |
+
"type": "flac_data",
|
| 505 |
+
"encoding": "flac" if is_flac else "pcm_s16le",
|
| 506 |
+
"size": len(flac_data),
|
| 507 |
+
"sample_rate": sample_rate,
|
| 508 |
+
})
|
| 509 |
+
await websocket.send_bytes(flac_data)
|
| 510 |
+
except Exception as e:
|
| 511 |
+
logger.warning(f"FLAC encoding failed, downloads saved as PCM: {e}")
|
| 512 |
+
finally:
|
| 513 |
+
download_pcm_chunks.clear()
|
| 514 |
+
|
| 515 |
+
try:
|
| 516 |
+
await websocket.send_json(
|
| 517 |
+
{
|
| 518 |
+
"type": "chapter_complete",
|
| 519 |
+
"next_url": chapter.get("next_url"),
|
| 520 |
+
"prev_url": chapter.get("prev_url"),
|
| 521 |
+
}
|
| 522 |
+
)
|
| 523 |
+
except Exception:
|
| 524 |
+
pass # Client already disconnected
|
| 525 |
+
|
| 526 |
+
# If the client sent a new command while streaming,
|
| 527 |
+
# it will be picked up by the outer while-loop.
|
| 528 |
+
if pending_command is not None:
|
| 529 |
+
try:
|
| 530 |
+
json.loads(pending_command) # validate
|
| 531 |
+
except Exception:
|
| 532 |
+
pending_command = None
|
| 533 |
+
|
| 534 |
except Exception as e:
|
| 535 |
logger.error(f"Play stream error: {e}")
|
| 536 |
try:
|
backend/tts.py
CHANGED
|
@@ -6,11 +6,15 @@ from kokoro_onnx import Kokoro
|
|
| 6 |
import asyncio
|
| 7 |
import json
|
| 8 |
import inspect
|
|
|
|
|
|
|
| 9 |
from typing import AsyncIterator, Iterable, List, Optional
|
| 10 |
import contextlib
|
| 11 |
from pathlib import Path
|
| 12 |
import zipfile
|
| 13 |
|
|
|
|
|
|
|
| 14 |
class TTSEngine:
|
| 15 |
def __init__(
|
| 16 |
self,
|
|
@@ -67,21 +71,71 @@ class TTSEngine:
|
|
| 67 |
sess_options = None
|
| 68 |
|
| 69 |
# kokoro_onnx API varies by version; try passing providers if supported.
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
if "providers" in
|
| 73 |
-
|
| 74 |
# Newer versions may support passing ORT session options.
|
| 75 |
if sess_options is not None:
|
| 76 |
for k in ("sess_options", "session_options", "ort_session_options"):
|
| 77 |
-
if k in
|
| 78 |
-
|
| 79 |
break
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
def list_voices(self) -> List[str]:
|
| 87 |
if self._voices_cache is not None:
|
|
@@ -238,42 +292,52 @@ class TTSEngine:
|
|
| 238 |
for i in range(0, len(pcm16), frame_bytes):
|
| 239 |
yield pcm16[i : i + frame_bytes]
|
| 240 |
|
| 241 |
-
def
|
| 242 |
-
"""Apply a
|
| 243 |
|
| 244 |
-
|
| 245 |
-
|
|
|
|
| 246 |
"""
|
| 247 |
-
if
|
| 248 |
-
return
|
| 249 |
-
|
| 250 |
-
samples = np.frombuffer(pcm16, dtype=np.int16)
|
| 251 |
-
n = int(samples.shape[0])
|
| 252 |
-
if n < 8:
|
| 253 |
-
return pcm16
|
| 254 |
|
| 255 |
fade_samples = int(self.sample_rate * (float(fade_ms) / 1000.0))
|
| 256 |
-
fade_samples = max(0, min(fade_samples,
|
| 257 |
if fade_samples < 2:
|
| 258 |
-
return
|
| 259 |
-
|
| 260 |
-
#
|
| 261 |
-
|
| 262 |
-
ramp =
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
async def synthesize_sentence_pcm16(self, sentence: str, voice: str, speed: float) -> bytes:
|
| 269 |
-
|
| 270 |
-
audio
|
| 271 |
-
|
| 272 |
-
return audio_int16.tobytes()
|
| 273 |
|
| 274 |
async def synthesize_sentence_pcm16_smoothed(self, sentence: str, voice: str, speed: float) -> bytes:
|
| 275 |
-
|
| 276 |
-
|
|
|
|
| 277 |
|
| 278 |
async def generate_audio_stream(
|
| 279 |
self,
|
|
@@ -429,7 +493,7 @@ class TTSEngine:
|
|
| 429 |
"""
|
| 430 |
|
| 431 |
segments = self.split_paragraphs_with_offsets(paragraphs)
|
| 432 |
-
queue: asyncio.Queue[Optional[tuple[int, int, str, bytes, int, int
|
| 433 |
maxsize=max(1, prefetch_sentences)
|
| 434 |
)
|
| 435 |
|
|
@@ -453,11 +517,20 @@ class TTSEngine:
|
|
| 453 |
break
|
| 454 |
if not s:
|
| 455 |
continue
|
| 456 |
-
|
|
|
|
| 457 |
if fade_ms and fade_ms > 0:
|
| 458 |
-
|
| 459 |
pause_ms = pause_ms_for(s, is_last)
|
| 460 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
finally:
|
| 462 |
await queue.put(None)
|
| 463 |
|
|
@@ -467,22 +540,37 @@ class TTSEngine:
|
|
| 467 |
item = await queue.get()
|
| 468 |
if item is None:
|
| 469 |
break
|
| 470 |
-
p_idx, s_idx, sentence, pcm16,
|
| 471 |
if cancel_event is not None and cancel_event.is_set():
|
| 472 |
return
|
| 473 |
|
| 474 |
-
|
| 475 |
-
silence_samples = int(self.sample_rate * (pause_ms / 1000.0))
|
| 476 |
-
silence_bytes = silence_samples * 2
|
| 477 |
-
chunk = pcm16 + (b"\x00" * silence_bytes)
|
| 478 |
-
else:
|
| 479 |
-
chunk = pcm16
|
| 480 |
-
yield (p_idx, s_idx, sentence, chunk, cs, ce)
|
| 481 |
finally:
|
| 482 |
producer_task.cancel()
|
| 483 |
with contextlib.suppress(Exception):
|
| 484 |
await producer_task
|
| 485 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
if __name__ == "__main__":
|
| 487 |
# Test
|
| 488 |
async def test():
|
|
|
|
| 6 |
import asyncio
|
| 7 |
import json
|
| 8 |
import inspect
|
| 9 |
+
import logging
|
| 10 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 11 |
from typing import AsyncIterator, Iterable, List, Optional
|
| 12 |
import contextlib
|
| 13 |
from pathlib import Path
|
| 14 |
import zipfile
|
| 15 |
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
class TTSEngine:
|
| 19 |
def __init__(
|
| 20 |
self,
|
|
|
|
| 71 |
sess_options = None
|
| 72 |
|
| 73 |
# kokoro_onnx API varies by version; try passing providers if supported.
|
| 74 |
+
self._kokoro_sig = inspect.signature(Kokoro)
|
| 75 |
+
self._kokoro_kwargs: dict = {}
|
| 76 |
+
if "providers" in self._kokoro_sig.parameters:
|
| 77 |
+
self._kokoro_kwargs["providers"] = self.providers
|
| 78 |
# Newer versions may support passing ORT session options.
|
| 79 |
if sess_options is not None:
|
| 80 |
for k in ("sess_options", "session_options", "ort_session_options"):
|
| 81 |
+
if k in self._kokoro_sig.parameters:
|
| 82 |
+
self._kokoro_kwargs[k] = sess_options
|
| 83 |
break
|
| 84 |
|
| 85 |
+
self.kokoro = self._create_kokoro_instance()
|
| 86 |
+
|
| 87 |
+
# Periodic session recycling: after this many sentences the ONNX
|
| 88 |
+
# session is recreated to avoid accumulated internal state that
|
| 89 |
+
# can introduce subtle audio artifacts (crackling / static).
|
| 90 |
+
self._session_recycle_interval = int(
|
| 91 |
+
os.getenv("TTS_SESSION_RECYCLE_SENTENCES", "20")
|
| 92 |
+
)
|
| 93 |
+
self._sentences_since_recycle = 0
|
| 94 |
+
# Future holding a pre-created Kokoro instance for seamless swap.
|
| 95 |
+
self._pending_kokoro: Optional[asyncio.Future] = None
|
| 96 |
+
|
| 97 |
+
# Dedicated thread-pool for ONNX inference so synthesis doesn't
|
| 98 |
+
# compete with asyncio I/O tasks on the default executor.
|
| 99 |
+
self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="tts")
|
| 100 |
+
# Separate thread-pool for background session creation so it
|
| 101 |
+
# doesn't block ongoing synthesis in _executor.
|
| 102 |
+
self._recycle_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="tts-recycle")
|
| 103 |
+
|
| 104 |
+
def _create_kokoro_instance(self) -> Kokoro:
|
| 105 |
+
"""Create a fresh Kokoro instance (rebuilds the ONNX session)."""
|
| 106 |
+
if self._kokoro_kwargs:
|
| 107 |
+
return Kokoro(self.model_path, self.voices_path, **self._kokoro_kwargs)
|
| 108 |
+
return Kokoro(self.model_path, self.voices_path)
|
| 109 |
+
|
| 110 |
+
def _maybe_recycle_session(self) -> None:
|
| 111 |
+
"""Recreate the ONNX session if the sentence threshold is reached.
|
| 112 |
+
|
| 113 |
+
Uses async overlap: starts building the new session in a background
|
| 114 |
+
thread while current synthesis continues using the old session.
|
| 115 |
+
When the new session is ready, swaps it in atomically.
|
| 116 |
+
"""
|
| 117 |
+
self._sentences_since_recycle += 1
|
| 118 |
+
if self._sentences_since_recycle >= self._session_recycle_interval:
|
| 119 |
+
if self._pending_kokoro is not None and self._pending_kokoro.done():
|
| 120 |
+
# New session is ready — swap it in.
|
| 121 |
+
try:
|
| 122 |
+
new_kokoro = self._pending_kokoro.result()
|
| 123 |
+
self.kokoro = new_kokoro
|
| 124 |
+
logger.info("Swapped in pre-built ONNX session")
|
| 125 |
+
except Exception as e:
|
| 126 |
+
logger.warning("Background session creation failed, rebuilding synchronously: %s", e)
|
| 127 |
+
self.kokoro = self._create_kokoro_instance()
|
| 128 |
+
self._pending_kokoro = None
|
| 129 |
+
self._sentences_since_recycle = 0
|
| 130 |
+
elif self._pending_kokoro is None:
|
| 131 |
+
# Start building new session in background.
|
| 132 |
+
logger.info("Scheduling background ONNX session recycle after %d sentences", self._sentences_since_recycle)
|
| 133 |
+
loop = asyncio.get_event_loop()
|
| 134 |
+
self._pending_kokoro = loop.run_in_executor(
|
| 135 |
+
self._recycle_executor, self._create_kokoro_instance
|
| 136 |
+
)
|
| 137 |
+
self._sentences_since_recycle = 0
|
| 138 |
+
# else: pending_kokoro is still building — keep using current session
|
| 139 |
|
| 140 |
def list_voices(self) -> List[str]:
|
| 141 |
if self._voices_cache is not None:
|
|
|
|
| 292 |
for i in range(0, len(pcm16), frame_bytes):
|
| 293 |
yield pcm16[i : i + frame_bytes]
|
| 294 |
|
| 295 |
+
def _apply_cosine_fade_f32(self, audio: np.ndarray, *, fade_ms: int = 6) -> np.ndarray:
|
| 296 |
+
"""Apply a raised-cosine fade-in/out on float32 audio.
|
| 297 |
|
| 298 |
+
Operates entirely in float32 to avoid quantization round-trips.
|
| 299 |
+
A cosine curve is smoother than linear and eliminates audible clicks
|
| 300 |
+
at sentence boundaries.
|
| 301 |
"""
|
| 302 |
+
if audio.size < 8 or fade_ms <= 0:
|
| 303 |
+
return audio
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
|
| 305 |
fade_samples = int(self.sample_rate * (float(fade_ms) / 1000.0))
|
| 306 |
+
fade_samples = max(0, min(fade_samples, audio.size // 2))
|
| 307 |
if fade_samples < 2:
|
| 308 |
+
return audio
|
| 309 |
+
|
| 310 |
+
# Raised-cosine: 0.5 * (1 - cos(pi * t)) for t in [0, 1]
|
| 311 |
+
t = np.linspace(0.0, 1.0, fade_samples, endpoint=False, dtype=np.float32)
|
| 312 |
+
ramp = 0.5 * (1.0 - np.cos(np.pi * t))
|
| 313 |
+
audio = audio.copy()
|
| 314 |
+
audio[:fade_samples] *= ramp
|
| 315 |
+
audio[-fade_samples:] *= ramp[::-1]
|
| 316 |
+
return audio
|
| 317 |
+
|
| 318 |
+
@staticmethod
|
| 319 |
+
def _float32_to_pcm16_bytes(audio: np.ndarray) -> bytes:
|
| 320 |
+
"""Single float32 -> int16 conversion. Called once at the end of the pipeline."""
|
| 321 |
+
return (np.clip(audio, -1.0, 1.0) * 32767).astype(np.int16).tobytes()
|
| 322 |
+
|
| 323 |
+
async def synthesize_sentence_f32(self, sentence: str, voice: str, speed: float) -> np.ndarray:
|
| 324 |
+
"""Synthesize a sentence and return float32 audio (no quantization yet)."""
|
| 325 |
+
loop = asyncio.get_running_loop()
|
| 326 |
+
audio, _ = await loop.run_in_executor(
|
| 327 |
+
self._executor, self.kokoro.create, sentence, voice, speed
|
| 328 |
+
)
|
| 329 |
+
self._maybe_recycle_session()
|
| 330 |
+
return np.asarray(audio, dtype=np.float32)
|
| 331 |
|
| 332 |
async def synthesize_sentence_pcm16(self, sentence: str, voice: str, speed: float) -> bytes:
|
| 333 |
+
"""Backward-compatible: returns PCM16 bytes."""
|
| 334 |
+
audio = await self.synthesize_sentence_f32(sentence, voice=voice, speed=speed)
|
| 335 |
+
return self._float32_to_pcm16_bytes(audio)
|
|
|
|
| 336 |
|
| 337 |
async def synthesize_sentence_pcm16_smoothed(self, sentence: str, voice: str, speed: float) -> bytes:
|
| 338 |
+
audio = await self.synthesize_sentence_f32(sentence, voice=voice, speed=speed)
|
| 339 |
+
audio = self._apply_cosine_fade_f32(audio)
|
| 340 |
+
return self._float32_to_pcm16_bytes(audio)
|
| 341 |
|
| 342 |
async def generate_audio_stream(
|
| 343 |
self,
|
|
|
|
| 493 |
"""
|
| 494 |
|
| 495 |
segments = self.split_paragraphs_with_offsets(paragraphs)
|
| 496 |
+
queue: asyncio.Queue[Optional[tuple[int, int, str, bytes, int, int]]] = asyncio.Queue(
|
| 497 |
maxsize=max(1, prefetch_sentences)
|
| 498 |
)
|
| 499 |
|
|
|
|
| 517 |
break
|
| 518 |
if not s:
|
| 519 |
continue
|
| 520 |
+
# Stay in float32 for all processing; convert once at the end.
|
| 521 |
+
audio_f32 = await self.synthesize_sentence_f32(s, voice=voice, speed=speed)
|
| 522 |
if fade_ms and fade_ms > 0:
|
| 523 |
+
audio_f32 = self._apply_cosine_fade_f32(audio_f32, fade_ms=int(fade_ms))
|
| 524 |
pause_ms = pause_ms_for(s, is_last)
|
| 525 |
+
|
| 526 |
+
# Append silence in float32 then convert the whole chunk once.
|
| 527 |
+
if pause_ms > 0:
|
| 528 |
+
silence_samples = int(self.sample_rate * (pause_ms / 1000.0))
|
| 529 |
+
silence = np.zeros(silence_samples, dtype=np.float32)
|
| 530 |
+
audio_f32 = np.concatenate([audio_f32, silence])
|
| 531 |
+
|
| 532 |
+
pcm16 = self._float32_to_pcm16_bytes(audio_f32)
|
| 533 |
+
await queue.put((p_idx, s_idx, s, pcm16, int(cs), int(ce)))
|
| 534 |
finally:
|
| 535 |
await queue.put(None)
|
| 536 |
|
|
|
|
| 540 |
item = await queue.get()
|
| 541 |
if item is None:
|
| 542 |
break
|
| 543 |
+
p_idx, s_idx, sentence, pcm16, cs, ce = item
|
| 544 |
if cancel_event is not None and cancel_event.is_set():
|
| 545 |
return
|
| 546 |
|
| 547 |
+
yield (p_idx, s_idx, sentence, pcm16, cs, ce)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
finally:
|
| 549 |
producer_task.cancel()
|
| 550 |
with contextlib.suppress(Exception):
|
| 551 |
await producer_task
|
| 552 |
|
| 553 |
+
@staticmethod
|
| 554 |
+
def encode_pcm16_to_flac(pcm16_bytes: bytes, sample_rate: int = 24000) -> bytes:
|
| 555 |
+
"""Encode raw PCM16 mono bytes to FLAC (lossless compression).
|
| 556 |
+
|
| 557 |
+
Uses soundfile for maximum portability (pip-installable, no external
|
| 558 |
+
binary deps). Falls back to returning the original PCM if soundfile
|
| 559 |
+
is not available.
|
| 560 |
+
"""
|
| 561 |
+
try:
|
| 562 |
+
import soundfile as sf
|
| 563 |
+
import io
|
| 564 |
+
|
| 565 |
+
samples = np.frombuffer(pcm16_bytes, dtype=np.int16)
|
| 566 |
+
# soundfile expects float or int data; int16 is supported natively.
|
| 567 |
+
buf = io.BytesIO()
|
| 568 |
+
sf.write(buf, samples, sample_rate, format="FLAC", subtype="PCM_16")
|
| 569 |
+
return buf.getvalue()
|
| 570 |
+
except ImportError:
|
| 571 |
+
logger.warning("soundfile not installed; returning raw PCM instead of FLAC")
|
| 572 |
+
return pcm16_bytes
|
| 573 |
+
|
| 574 |
if __name__ == "__main__":
|
| 575 |
# Test
|
| 576 |
async def test():
|
backend/uv.lock
CHANGED
|
@@ -178,6 +178,54 @@ wheels = [
|
|
| 178 |
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
| 179 |
]
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
[[package]]
|
| 182 |
name = "charset-normalizer"
|
| 183 |
version = "3.4.4"
|
|
@@ -523,6 +571,8 @@ dependencies = [
|
|
| 523 |
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
| 524 |
{ name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
| 525 |
{ name = "onnxruntime" },
|
|
|
|
|
|
|
| 526 |
{ name = "uvicorn", extra = ["standard"] },
|
| 527 |
]
|
| 528 |
|
|
@@ -535,6 +585,8 @@ requires-dist = [
|
|
| 535 |
{ name = "lxml", specifier = ">=5.2.2" },
|
| 536 |
{ name = "numpy", specifier = ">=1.26.0" },
|
| 537 |
{ name = "onnxruntime", specifier = ">=1.20.0" },
|
|
|
|
|
|
|
| 538 |
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" },
|
| 539 |
]
|
| 540 |
|
|
@@ -885,6 +937,15 @@ wheels = [
|
|
| 885 |
{ url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" },
|
| 886 |
]
|
| 887 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 888 |
[[package]]
|
| 889 |
name = "pydantic"
|
| 890 |
version = "2.12.5"
|
|
@@ -1236,6 +1297,26 @@ wheels = [
|
|
| 1236 |
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
| 1237 |
]
|
| 1238 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1239 |
[[package]]
|
| 1240 |
name = "soupsieve"
|
| 1241 |
version = "2.8.3"
|
|
|
|
| 178 |
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
| 179 |
]
|
| 180 |
|
| 181 |
+
[[package]]
|
| 182 |
+
name = "cffi"
|
| 183 |
+
version = "2.0.0"
|
| 184 |
+
source = { registry = "https://pypi.org/simple" }
|
| 185 |
+
dependencies = [
|
| 186 |
+
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
| 187 |
+
]
|
| 188 |
+
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
| 189 |
+
wheels = [
|
| 190 |
+
{ url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
|
| 191 |
+
{ url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
|
| 192 |
+
{ url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
|
| 193 |
+
{ url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
|
| 194 |
+
{ url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
|
| 195 |
+
{ url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
|
| 196 |
+
{ url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
|
| 197 |
+
{ url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
|
| 198 |
+
{ url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
|
| 199 |
+
{ url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
|
| 200 |
+
{ url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
|
| 201 |
+
{ url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
|
| 202 |
+
{ url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
|
| 203 |
+
{ url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
|
| 204 |
+
{ url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
|
| 205 |
+
{ url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
|
| 206 |
+
{ url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
|
| 207 |
+
{ url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
|
| 208 |
+
{ url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
|
| 209 |
+
{ url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
|
| 210 |
+
{ url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
|
| 211 |
+
{ url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
|
| 212 |
+
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
|
| 213 |
+
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
|
| 214 |
+
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
|
| 215 |
+
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
|
| 216 |
+
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
|
| 217 |
+
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
|
| 218 |
+
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
|
| 219 |
+
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
|
| 220 |
+
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
|
| 221 |
+
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
|
| 222 |
+
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
|
| 223 |
+
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
|
| 224 |
+
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
|
| 225 |
+
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
|
| 226 |
+
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
|
| 227 |
+
]
|
| 228 |
+
|
| 229 |
[[package]]
|
| 230 |
name = "charset-normalizer"
|
| 231 |
version = "3.4.4"
|
|
|
|
| 571 |
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
| 572 |
{ name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
| 573 |
{ name = "onnxruntime" },
|
| 574 |
+
{ name = "requests" },
|
| 575 |
+
{ name = "soundfile" },
|
| 576 |
{ name = "uvicorn", extra = ["standard"] },
|
| 577 |
]
|
| 578 |
|
|
|
|
| 585 |
{ name = "lxml", specifier = ">=5.2.2" },
|
| 586 |
{ name = "numpy", specifier = ">=1.26.0" },
|
| 587 |
{ name = "onnxruntime", specifier = ">=1.20.0" },
|
| 588 |
+
{ name = "requests", specifier = ">=2.32.0" },
|
| 589 |
+
{ name = "soundfile", specifier = ">=0.12.0" },
|
| 590 |
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" },
|
| 591 |
]
|
| 592 |
|
|
|
|
| 937 |
{ url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" },
|
| 938 |
]
|
| 939 |
|
| 940 |
+
[[package]]
|
| 941 |
+
name = "pycparser"
|
| 942 |
+
version = "3.0"
|
| 943 |
+
source = { registry = "https://pypi.org/simple" }
|
| 944 |
+
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
| 945 |
+
wheels = [
|
| 946 |
+
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
| 947 |
+
]
|
| 948 |
+
|
| 949 |
[[package]]
|
| 950 |
name = "pydantic"
|
| 951 |
version = "2.12.5"
|
|
|
|
| 1297 |
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
| 1298 |
]
|
| 1299 |
|
| 1300 |
+
[[package]]
|
| 1301 |
+
name = "soundfile"
|
| 1302 |
+
version = "0.13.1"
|
| 1303 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1304 |
+
dependencies = [
|
| 1305 |
+
{ name = "cffi" },
|
| 1306 |
+
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
| 1307 |
+
{ name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
| 1308 |
+
]
|
| 1309 |
+
sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" }
|
| 1310 |
+
wheels = [
|
| 1311 |
+
{ url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" },
|
| 1312 |
+
{ url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" },
|
| 1313 |
+
{ url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" },
|
| 1314 |
+
{ url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" },
|
| 1315 |
+
{ url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" },
|
| 1316 |
+
{ url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" },
|
| 1317 |
+
{ url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" },
|
| 1318 |
+
]
|
| 1319 |
+
|
| 1320 |
[[package]]
|
| 1321 |
name = "soupsieve"
|
| 1322 |
version = "2.8.3"
|