first commit
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
"""Тест 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())
|
||||
Reference in New Issue
Block a user