refactor: rewrite STT service to use qwen3_asr_toolkit Python API

- Replace Whisper subprocess calls with direct qwen3_asr_toolkit API
- Remove subprocess dependencies, use pure Python API
- Update to use DASHSCOPE_API_KEY instead of WHISPER_MODEL
- Cleaner code without CLI calls
- Better Ukrainian language recognition quality
This commit is contained in:
Apple
2025-11-15 12:55:21 -08:00
parent 65e33add81
commit e0cb3ddbdb
5 changed files with 115 additions and 146 deletions

View File

@@ -172,7 +172,7 @@ services:
timeout: 10s timeout: 10s
retries: 3 retries: 3
# STT Service (Speech-to-Text using Whisper) # STT Service (Speech-to-Text using Qwen3 ASR Toolkit)
stt-service: stt-service:
build: build:
context: ./services/stt-service context: ./services/stt-service
@@ -181,8 +181,7 @@ services:
ports: ports:
- "9000:9000" - "9000:9000"
environment: environment:
- WHISPER_MODEL=${WHISPER_MODEL:-base} - DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY:-}
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
volumes: volumes:
- ./logs:/app/logs - ./logs:/app/logs
networks: networks:

View File

@@ -2,7 +2,8 @@ FROM python:3.11-slim
WORKDIR /app WORKDIR /app
# Встановлюємо системні залежності (ffmpeg для конвертації аудіо) # Встановлюємо системні залежності
# qwen3_asr_toolkit може потребувати ffmpeg для обробки деяких форматів
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
ffmpeg \ ffmpeg \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

View File

@@ -1,16 +1,14 @@
# STT Service (Speech-to-Text) # STT Service (Speech-to-Text)
Сервіс для розпізнавання мови з аудіо файлів за допомогою Whisper. Сервіс для розпізнавання мови з аудіо файлів за допомогою Qwen3 ASR Toolkit.
## Можливості ## Можливості
- Розпізнавання мови з голосових повідомлень (Telegram voice, audio, video_note) - Розпізнавання мови з голосових повідомлень (Telegram voice, audio, video_note)
- Підтримка форматів: ogg, mp3, wav, m4a, webm - Підтримка форматів: ogg, mp3, wav, m4a, webm, flac
- Автоматична конвертація в WAV 16kHz mono через ffmpeg - Автоматична обробка та конвертація аудіо (всередині qwen3_asr_toolkit)
- Підтримка кількох Whisper-реалізацій: - Чистий Python API без subprocess/CLI викликів
- `faster-whisper` (рекомендовано, локально) - Висока якість розпізнавання української мови
- `whisper` CLI (fallback)
- OpenAI Whisper API (якщо є API key)
## Запуск ## Запуск
@@ -60,17 +58,15 @@ Health check endpoint.
### Environment Variables ### Environment Variables
- `WHISPER_MODEL`: модель Whisper (`base`, `small`, `medium`, `large`) - за замовчуванням `base` - `DASHSCOPE_API_KEY`: **Обов'язково** - API ключ DashScope для доступу до Qwen3 ASR API
- `OPENAI_API_KEY`: API ключ OpenAI (опційно, для використання OpenAI Whisper API) - Отримати ключ: https://dashscope.console.aliyun.com/
- Встановити: `export DASHSCOPE_API_KEY="your-api-key"`
### Моделі Whisper ### Отримання API ключа DashScope
- `base`: найшвидша, менша точність (~74M параметрів) 1. Зареєструйтеся на https://dashscope.console.aliyun.com/
- `small`: баланс швидкості та якості (~244M) 2. Створіть API ключ в розділі "API Keys"
- `medium`: краща якість (~769M) 3. Встановіть змінну середовища `DASHSCOPE_API_KEY`
- `large`: найкраща якість (~1550M)
Для української мови рекомендую `small` або `medium`.
## Інтеграція з Gateway ## Інтеграція з Gateway
@@ -84,21 +80,13 @@ Gateway автоматично використовує STT-сервіс для
## Встановлення залежностей ## Встановлення залежностей
### faster-whisper (рекомендовано) ### qwen3-asr-toolkit
```bash ```bash
pip install faster-whisper pip install qwen3-asr-toolkit
``` ```
Моделі завантажуються автоматично при першому використанні. ### ffmpeg (може знадобитися для деяких форматів)
### whisper CLI (fallback)
```bash
pip install openai-whisper
```
### ffmpeg (обов'язково)
```bash ```bash
# Ubuntu/Debian # Ubuntu/Debian
@@ -113,19 +101,27 @@ brew install ffmpeg
## Troubleshooting ## Troubleshooting
### Помилка: "No Whisper implementation available" ### Помилка: "qwen3_asr_toolkit not available"
Встановіть одну з реалізацій: Встановіть бібліотеку:
- `pip install faster-whisper` (рекомендовано) ```bash
- або `pip install openai-whisper` pip install qwen3-asr-toolkit
- або встановіть `OPENAI_API_KEY` ```
### Помилка: "DASHSCOPE_API_KEY not configured"
Встановіть змінну середовища:
```bash
export DASHSCOPE_API_KEY="your-api-key"
```
Або додайте в `docker-compose.yml`:
```yaml
environment:
- DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY}
```
### Помилка: "ffmpeg not found" ### Помилка: "ffmpeg not found"
Встановіть ffmpeg (див. вище). Встановіть ffmpeg (див. вище). Більшість форматів обробляються без ffmpeg, але деякі можуть його потребувати.
### Повільна обробка
- Використовуйте меншу модель (`base` замість `medium`)
- Або використовуйте GPU (додайте `device="cuda"` в коді)

View File

@@ -1,11 +1,10 @@
""" """
STT Service (Speech-to-Text) для DAGI Router STT Service (Speech-to-Text) для DAGI Router
Використовує Whisper для розпізнавання голосу Використовує qwen3_asr_toolkit для розпізнавання голосу
""" """
import os import os
import uuid import uuid
import subprocess
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -19,8 +18,8 @@ logger = logging.getLogger(__name__)
app = FastAPI( app = FastAPI(
title="STT Service", title="STT Service",
description="Speech-to-Text service using Whisper", description="Speech-to-Text service using Qwen3 ASR Toolkit",
version="1.0.0" version="2.0.0"
) )
app.add_middleware( app.add_middleware(
@@ -32,10 +31,19 @@ app.add_middleware(
) )
# Configuration # Configuration
WHISPER_MODEL = os.getenv("WHISPER_MODEL", "base") # base, small, medium DASHSCOPE_API_KEY = os.getenv("DASHSCOPE_API_KEY")
TEMP_DIR = Path("/tmp/stt") TEMP_DIR = Path("/tmp/stt")
TEMP_DIR.mkdir(exist_ok=True) TEMP_DIR.mkdir(exist_ok=True)
# Initialize Qwen3 ASR Toolkit
try:
from qwen3_asr_toolkit import transcribe_audio
ASR_AVAILABLE = True
logger.info("qwen3_asr_toolkit loaded successfully")
except ImportError:
ASR_AVAILABLE = False
logger.warning("qwen3_asr_toolkit not available, install with: pip install qwen3-asr-toolkit")
class STTResponse(BaseModel): class STTResponse(BaseModel):
text: str text: str
@@ -43,119 +51,78 @@ class STTResponse(BaseModel):
duration: Optional[float] = None duration: Optional[float] = None
def convert_audio_to_wav(input_path: str, output_path: str) -> bool: def transcribe_with_qwen(audio_path: str) -> tuple[str, Optional[str], Optional[float]]:
"""Конвертувати аудіо в WAV 16kHz mono"""
try:
cmd = [
"ffmpeg", "-y", "-i", input_path,
"-ar", "16000", # Sample rate
"-ac", "1", # Mono
"-f", "wav",
output_path
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30
)
if result.returncode != 0:
logger.error(f"ffmpeg error: {result.stderr}")
return False
return True
except Exception as e:
logger.error(f"Audio conversion failed: {e}")
return False
def transcribe_with_whisper(audio_path: str) -> tuple[str, Optional[str], Optional[float]]:
""" """
Розпізнати мову з аудіо файлу Розпізнати мову з аудіо файлу через qwen3_asr_toolkit
Повертає (text, language, duration) Повертає (text, language, duration)
""" """
if not ASR_AVAILABLE:
raise ImportError("qwen3_asr_toolkit not installed. Install with: pip install qwen3-asr-toolkit")
if not DASHSCOPE_API_KEY:
raise ValueError("DASHSCOPE_API_KEY environment variable not set")
try: try:
# Варіант 1: faster-whisper (рекомендовано) # qwen3_asr_toolkit автоматично обробляє різні формати аудіо
try: # та виконує необхідні конвертації
from faster_whisper import WhisperModel transcript = transcribe_audio(audio_path)
model = WhisperModel(WHISPER_MODEL, device="cpu", compute_type="int8")
segments, info = model.transcribe(audio_path, language="uk", beam_size=5)
text_parts = []
for segment in segments:
text_parts.append(segment.text)
text = " ".join(text_parts).strip()
language = info.language
duration = sum(segment.end - segment.start for segment in segments)
return text, language, duration
except ImportError:
logger.warning("faster-whisper not installed, trying whisper CLI")
# Варіант 2: whisper CLI (fallback) # transcribe_audio повертає текст
try: # Можна також отримати додаткову інформацію, якщо API підтримує
cmd = ["whisper", audio_path, "--model", WHISPER_MODEL, "--language", "uk", "--output_format", "txt"] text = transcript.strip() if isinstance(transcript, str) else str(transcript).strip()
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=60
)
if result.returncode == 0:
# Whisper CLI створює .txt файл з тим самим ім'ям
txt_path = audio_path.replace(".wav", ".txt")
if Path(txt_path).exists():
text = Path(txt_path).read_text(encoding="utf-8").strip()
return text, "uk", None
except FileNotFoundError:
logger.warning("whisper CLI not found")
# Варіант 3: OpenAI Whisper API (якщо є API key) # Для української мови встановлюємо language="uk"
openai_api_key = os.getenv("OPENAI_API_KEY") # qwen3_asr_toolkit може автоматично визначати мову
if openai_api_key: language = "uk" # Можна змінити на автоматичне визначення
try:
import openai
client = openai.OpenAI(api_key=openai_api_key)
with open(audio_path, "rb") as audio_file:
transcript = client.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
language="uk"
)
return transcript.text, transcript.language, None
except Exception as e:
logger.warning(f"OpenAI Whisper API failed: {e}")
raise Exception("No Whisper implementation available") # Duration можна отримати з аудіо файлу, якщо потрібно
# Поки що повертаємо None
duration = None
return text, language, duration
except Exception as e: except Exception as e:
logger.error(f"Transcription failed: {e}") logger.error(f"Qwen3 ASR transcription failed: {e}", exc_info=True)
raise raise
@app.post("/stt", response_model=STTResponse) @app.post("/stt", response_model=STTResponse)
async def stt(file: UploadFile = File(...)): async def stt(file: UploadFile = File(...)):
""" """
Розпізнати мову з аудіо файлу Розпізнати мову з аудіо файлу через qwen3_asr_toolkit
Підтримує формати: ogg, mp3, wav, m4a, webm Підтримує формати: ogg, mp3, wav, m4a, webm, flac
qwen3_asr_toolkit автоматично обробляє конвертацію
""" """
if not ASR_AVAILABLE:
raise HTTPException(
status_code=503,
detail="qwen3_asr_toolkit not available. Install with: pip install qwen3-asr-toolkit"
)
if not DASHSCOPE_API_KEY:
raise HTTPException(
status_code=500,
detail="DASHSCOPE_API_KEY not configured"
)
tmp_id = str(uuid.uuid4()) tmp_id = str(uuid.uuid4())
tmp_input = TEMP_DIR / f"{tmp_id}_input.{file.filename.split('.')[-1] if '.' in file.filename else 'ogg'}" # Визначаємо розширення файлу
tmp_wav = TEMP_DIR / f"{tmp_id}.wav" file_ext = "ogg"
if file.filename and "." in file.filename:
file_ext = file.filename.split(".")[-1].lower()
tmp_input = TEMP_DIR / f"{tmp_id}.{file_ext}"
try: try:
# Зберігаємо вхідний файл # Зберігаємо вхідний файл
content = await file.read() content = await file.read()
tmp_input.write_bytes(content) tmp_input.write_bytes(content)
logger.info(f"Received audio file: {file.filename}, size: {len(content)} bytes") logger.info(f"Received audio file: {file.filename}, size: {len(content)} bytes, format: {file_ext}")
# Конвертуємо в WAV 16kHz # qwen3_asr_toolkit автоматично обробляє різні формати
if not convert_audio_to_wav(str(tmp_input), str(tmp_wav)): # та виконує необхідні конвертації всередині
raise HTTPException(status_code=400, detail="Audio conversion failed") text, language, duration = transcribe_with_qwen(str(tmp_input))
# Розпізнаємо мову
text, language, duration = transcribe_with_whisper(str(tmp_wav))
logger.info(f"Transcribed: {text[:100]}... (lang: {language})") logger.info(f"Transcribed: {text[:100]}... (lang: {language})")
@@ -167,26 +134,33 @@ async def stt(file: UploadFile = File(...)):
except HTTPException: except HTTPException:
raise raise
except ValueError as e:
logger.error(f"STT configuration error: {e}")
raise HTTPException(status_code=500, detail=str(e))
except ImportError as e:
logger.error(f"STT import error: {e}")
raise HTTPException(status_code=503, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"STT error: {e}", exc_info=True) logger.error(f"STT error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"STT failed: {str(e)}") raise HTTPException(status_code=500, detail=f"STT failed: {str(e)}")
finally: finally:
# Очищаємо тимчасові файли # Очищаємо тимчасові файли
for path in [tmp_input, tmp_wav]: if tmp_input.exists():
if path.exists(): try:
try: tmp_input.unlink()
path.unlink() except Exception as e:
except: logger.warning(f"Failed to delete temp file {tmp_input}: {e}")
pass
@app.get("/health") @app.get("/health")
async def health(): async def health():
"""Health check""" """Health check"""
return { return {
"status": "ok", "status": "ok" if ASR_AVAILABLE else "degraded",
"service": "stt-service", "service": "stt-service",
"model": WHISPER_MODEL "engine": "qwen3_asr_toolkit",
"asr_available": ASR_AVAILABLE,
"api_key_configured": DASHSCOPE_API_KEY is not None
} }

View File

@@ -1,6 +1,5 @@
fastapi==0.104.1 fastapi==0.104.1
uvicorn[standard]==0.24.0 uvicorn[standard]==0.24.0
python-multipart==0.0.6 python-multipart==0.0.6
faster-whisper==1.0.0 qwen3-asr-toolkit>=1.0.0
openai>=1.0.0