lct-hack/backend/scripts/models.py
Ivan Gerasimov dcdec56779 lct-02: замер задержки голосового контура, make latency
Замер на машине разработки (Ryzen 7 8845HS, WSL2), не на демо-машине —
там make latency надо прогнать трижды, разброс между прогонами до 20%.

Распознавание — GigaAM v3 RNNT int8: фраза на 2 с за ~285 мс, WER 0%
на 99 словах живой речи Golos. CTC не быстрее ни в одном из двух прогонов
и ошибается чаще: время съедает общий энкодер, поэтому ступень деградации
«RNNT не успевает → CTC» по скорости ничего не даёт.

Синтез — Silero v5: в 10 раз быстрее реального времени, а не в 40, как
заложено в STACK.md; первая фраза 100–250 мс в зависимости от длины.
Частота синтеза на скорость не влияет, без профилирующего компилятора —
на 13% быстрее.

Сквозной бюджет: 600 мс endpointing + ~285 STT + ~150 TTS = ~1 с без LLM.
Цель ≤ 1.5 с держится, только если LLM отдаёт первое предложение быстрее
~465 мс. Вопрос, считается ли филлер «алло?..» первым звуком, записан
в LATENCY.md для людей.

make models качает GigaAM и Silero VAD с Hugging Face. Silero TTS лежит
на российском хосте, недоступном из-под VPN: скрипт не лезет на зеркала
(.pt грузится через pickle — это чужой код) и честно говорит, откуда
скачать вручную.
2026-09-17 14:44:02 +03:00

77 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""make models: веса моделей в backend/models/.
Эмбеддинги, распознавание (GigaAM v3, int8) и VAD качаются с Hugging Face.
Синтез (Silero TTS v5) лежит на models.silero.ai — российском хосте, который
не отвечает из-под VPN: его скрипт не качает, а проверяет и говорит, что делать.
Докачка продолжается с места обрыва: сеть на стенде бывает медленной.
"""
import sys
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MODELS = ROOT / "models"
E5 = "https://huggingface.co/Xenova/multilingual-e5-small/resolve/main/"
GIGAAM = "https://huggingface.co/istupakov/gigaam-v3-onnx/resolve/main/"
VAD = "https://huggingface.co/istupakov/silero-vad-onnx/resolve/main/"
FILES = {
"e5-small/config.json": E5 + "config.json",
"e5-small/tokenizer.json": E5 + "tokenizer.json",
"e5-small/model_quantized.onnx": E5 + "onnx/model_quantized.onnx",
# RNNT — основная модель, CTC — запасная на случай, если RNNT не загрузится
# (по скорости CTC не выигрывает — docs/LATENCY.md).
"gigaam-v3-onnx/config.json": GIGAAM + "config.json",
"gigaam-v3-onnx/v3_vocab.txt": GIGAAM + "v3_vocab.txt",
"gigaam-v3-onnx/v3_rnnt_encoder.int8.onnx": GIGAAM + "v3_rnnt_encoder.int8.onnx",
"gigaam-v3-onnx/v3_rnnt_decoder.int8.onnx": GIGAAM + "v3_rnnt_decoder.int8.onnx",
"gigaam-v3-onnx/v3_rnnt_joint.int8.onnx": GIGAAM + "v3_rnnt_joint.int8.onnx",
"gigaam-v3-onnx/v3_ctc.int8.onnx": GIGAAM + "v3_ctc.int8.onnx",
"silero-vad/config.json": VAD + "config.json",
"silero-vad/silero_vad.onnx": VAD + "silero_vad.onnx",
}
SILERO_TTS = "silero-tts/v5_ru.pt"
SILERO_TTS_URL = "https://models.silero.ai/models/tts/ru/v5_ru.pt"
def fetch(url: str, target: Path) -> None:
partial = target.with_suffix(target.suffix + ".part")
offset = partial.stat().st_size if partial.exists() else 0
request = urllib.request.Request(url, headers={"Range": f"bytes={offset}-"} if offset else {})
with urllib.request.urlopen(request, timeout=60) as response, partial.open("ab") as out:
total = offset + int(response.headers.get("Content-Length", 0))
while chunk := response.read(1 << 20):
out.write(chunk)
done = out.tell()
if total:
print(f"\r {target.name}: {done // (1 << 20)} из {total // (1 << 20)} МБ", end="", flush=True)
partial.rename(target)
print()
def main() -> None:
for relative, url in FILES.items():
target = MODELS / relative
if target.exists():
print(f" {relative}: уже есть")
continue
target.parent.mkdir(parents=True, exist_ok=True)
fetch(url, target)
tts = MODELS / SILERO_TTS
if tts.exists():
print(f" {SILERO_TTS}: уже есть")
print("все модели на месте")
return 0
# Не качаем с зеркал: .pt грузится через pickle, файл из непроверенного
# источника — это чужой код на стенде.
print(f"""
{SILERO_TTS}: НЕТ. Хост models.silero.ai не отвечает из-под VPN — скачайте вручную:
{SILERO_TTS_URL}
и положите в backend/models/{SILERO_TTS}""")
return 1
if __name__ == "__main__":
sys.exit(main())