137 lines
5.7 KiB
Python
137 lines
5.7 KiB
Python
"""Тест STT-модуля (Модуль 1).
|
||
|
||
Запуск:
|
||
python modules/stt/test_stt.py --make-sample # сгенерировать sample.mp3 (edge-tts) и распознать
|
||
python modules/stt/test_stt.py modules/stt/samples/sample.mp3
|
||
python modules/stt/test_stt.py --record 5 # 5 секунд с микрофона → распознать
|
||
python modules/stt/test_stt.py --model base <file> # лёгкая модель для слабого ПК
|
||
python modules/stt/test_stt.py --repeat 3 <file> # замерить скорость несколькими прогонами
|
||
|
||
Критерий приёмки: фраза распознана верно, обработка <= 3 c на фразе ~5 c.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import asyncio
|
||
import sys
|
||
import time
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
import numpy as np
|
||
|
||
try:
|
||
from modules.stt import get_provider
|
||
except ImportError: # запуск как обычного скрипта: добавляем корень проекта в путь
|
||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||
from modules.stt import get_provider
|
||
|
||
SAMPLES_DIR = Path(__file__).resolve().parent / "samples"
|
||
SAMPLE_PHRASE = (
|
||
"Привет! Это тестовый голосовой ассистент. "
|
||
"Сегодня хорошая погода, и я хочу выпить чаю."
|
||
)
|
||
DEFAULT_VOICE = "ru-RU-SvetlanaNeural"
|
||
|
||
|
||
def make_sample(path: Path, voice: str) -> None:
|
||
"""Синтез русской фразы через edge-tts — удобный источник тестового аудио."""
|
||
import edge_tts
|
||
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
async def _save() -> None:
|
||
await edge_tts.Communicate(SAMPLE_PHRASE, voice).save(str(path))
|
||
|
||
print(f"Генерирую {path.name} (голос {voice})...")
|
||
asyncio.run(_save())
|
||
|
||
|
||
def record(seconds: int, sample_rate: int = 16000) -> np.ndarray:
|
||
"""Запись с микрофона по умолчанию → float32 mono, 16 кГц."""
|
||
import sounddevice as sd
|
||
import soundfile as sf
|
||
|
||
print(f"\nЗапись {seconds} с. Приготовьтесь:")
|
||
for i in (3, 2, 1):
|
||
print(f" {i}...")
|
||
time.sleep(0.6)
|
||
print(" >>> ГОВОРИТЕ <<<")
|
||
raw = sd.rec(int(seconds * sample_rate), samplerate=sample_rate, channels=1, dtype="int16")
|
||
sd.wait()
|
||
audio = raw.astype(np.float32) / 32768.0
|
||
audio = audio.mean(axis=1) # (frames, 1) → mono
|
||
wav = SAMPLES_DIR / "recording.wav"
|
||
sf.write(str(wav), audio, sample_rate)
|
||
print(f"Сохранено: {wav}")
|
||
return audio
|
||
|
||
|
||
def parse_args() -> argparse.Namespace:
|
||
p = argparse.ArgumentParser(description="Тест STT-модуля (faster-whisper)")
|
||
p.add_argument("audio", nargs="?", help="аудио-файл (wav/mp3/flac)")
|
||
p.add_argument("--make-sample", action="store_true",
|
||
help="сгенерировать sample.mp3 (edge-tts) и распознать его")
|
||
p.add_argument("--voice", default=DEFAULT_VOICE, help="голос edge-tts для --make-sample")
|
||
p.add_argument("--record", type=int, default=0, metavar="SEC",
|
||
help="записать SEC секунд с микрофона и распознать")
|
||
p.add_argument("--model", default="small", help="tiny | base | small | medium")
|
||
p.add_argument("--device", default="cpu", help="cpu | cuda")
|
||
p.add_argument("--compute", default="int8", help="int8 | float16 | float32")
|
||
p.add_argument("--beam", type=int, default=1, help="beam size (1 = быстрее)")
|
||
p.add_argument("--no-vad", action="store_true", help="не отрезать тишину")
|
||
p.add_argument("--repeat", type=int, default=1, help="число прогонов для замера")
|
||
return p.parse_args()
|
||
|
||
|
||
def main() -> int:
|
||
args = parse_args()
|
||
|
||
audio_array: Optional[np.ndarray] = None
|
||
target: Optional[Path] = None
|
||
|
||
if args.record:
|
||
audio_array = record(args.record)
|
||
elif args.make_sample:
|
||
target = SAMPLES_DIR / "sample.mp3"
|
||
if not target.exists():
|
||
make_sample(target, args.voice)
|
||
else:
|
||
print(f"Использую существующий {target}")
|
||
elif args.audio:
|
||
target = Path(args.audio)
|
||
else:
|
||
print(__doc__)
|
||
return 1
|
||
|
||
print(f"\nЗагружаю модель {args.model!r} ({args.compute}, {args.device})...")
|
||
print("Первый запуск скачивает модель с HuggingFace (small ≈ 460 МБ), это нормально.")
|
||
stt = get_provider(
|
||
"faster-whisper",
|
||
model_size=args.model,
|
||
device=args.device,
|
||
compute_type=args.compute,
|
||
beam_size=args.beam,
|
||
vad_filter=not args.no_vad,
|
||
)
|
||
print(f"Модель готова за {stt.load_sec:.1f} с.\n")
|
||
|
||
results = []
|
||
for i in range(1, args.repeat + 1):
|
||
source = audio_array if audio_array is not None else target
|
||
res = stt.transcribe(source)
|
||
results.append(res)
|
||
print(f"[прогон {i}/{args.repeat}] {res}")
|
||
|
||
best = min(results, key=lambda r: r.processing_sec)
|
||
print("\n=== ИТОГ ===")
|
||
print(f"Текст: {best.text or '(пусто — нечего распознавать)'}")
|
||
print(f"Аудио {best.duration_sec:.1f} c → обработка {best.processing_sec:.2f} c "
|
||
f"(RTF {best.realtime_factor:.2f})")
|
||
print(f"Язык: {best.language} (уверенность {best.language_probability:.2f})")
|
||
passed = bool(best.text.strip())
|
||
print("ПРИЁМКА:", "ПРОЙДЕНА ✅" if passed else "ПРОВАЛЕНА ❌ (пустой текст)")
|
||
return 0 if passed else 2
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main()) |