feat: Add presence heartbeat for Matrix online status

- matrix-gateway: POST /internal/matrix/presence/online endpoint
- usePresenceHeartbeat hook with activity tracking
- Auto away after 5 min inactivity
- Offline on page close/visibility change
- Integrated in MatrixChatRoom component
This commit is contained in:
Apple
2025-11-27 00:19:40 -08:00
parent 5bed515852
commit 3de3c8cb36
6371 changed files with 1317450 additions and 932 deletions

View File

@@ -1,26 +1,25 @@
FROM python:3.11-slim
# Встановити системні залежності
RUN apt-get update && apt-get install -y \
ffmpeg \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Встановлюємо системні залежності
# qwen3_asr_toolkit може потребувати ffmpeg для обробки деяких форматів
RUN apt-get update && apt-get install -y \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*
# Копіюємо requirements та встановлюємо залежності
# Копіювати requirements
COPY requirements.txt .
# Встановити Python залежності
RUN pip install --no-cache-dir -r requirements.txt
# Копіюємо код
COPY . .
# Копіювати код
COPY app/ ./app/
# Створюємо тимчасову директорію
RUN mkdir -p /tmp/stt
# Whisper модель завантажиться автоматично при першому запиті
# Це уникає проблем з мережею під час build
# Відкриваємо порт
EXPOSE 9000
# Запускаємо додаток
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "9000"]
EXPOSE 8895
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8895"]

View File

@@ -1,127 +1,279 @@
# STT Service (Speech-to-Text)
# STT Service - Speech-to-Text для DAARION
Сервіс для розпізнавання мови з аудіо файлів за допомогою Qwen3 ASR Toolkit.
Сервіс конвертації аудіо в текст використовуючи OpenAI Whisper AI.
## Можливості
- Розпізнавання мови з голосових повідомлень (Telegram voice, audio, video_note)
- Підтримка форматів: ogg, mp3, wav, m4a, webm, flac
- Автоматична обробка та конвертація аудіо (всередині qwen3_asr_toolkit)
- Чистий Python API без subprocess/CLI викликів
- Висока якість розпізнавання української мови
- 🎤 **Розпізнавання мови**: Whisper AI (base model)
- 🌍 **Мультимовність**: Підтримка української та інших мов
- 📊 **Формати аудіо**: webm, mp3, wav, m4a, ogg
- 🚀 **Швидкість**: ~5-10 секунд для 1 хвилини аудіо
- 🔒 **Безпека**: Локальна обробка, без відправки на зовнішні сервери
## Запуск
## Встановлення
### Локально (development)
### Docker (рекомендовано)
```bash
cd services/stt-service
docker-compose up -d
```
### Локально
```bash
cd services/stt-service
pip install -r requirements.txt
uvicorn main:app --reload --host 0.0.0.0 --port 9000
python -m app.main
```
### Docker
## API Endpoints
```bash
docker-compose up stt-service
```
### 1. POST /api/stt
## API
### POST /stt
Розпізнати мову з аудіо файлу.
Конвертує base64 аудіо в текст.
**Request:**
- `file`: аудіо файл (multipart/form-data)
```json
POST http://localhost:8895/api/stt
Content-Type: application/json
{
"audio": "data:audio/webm;base64,GkXfo59ChoEBQveBAULygQRC...",
"language": "uk",
"model": "base"
}
```
**Response:**
```json
{
"text": "розпізнаний текст",
"text": "Привіт, це тестове повідомлення",
"language": "uk",
"duration": 5.2
"duration": 2.5,
"model": "base",
"confidence": 0.95
}
```
**Приклад:**
---
### 2. POST /api/stt/upload
Конвертує завантажений аудіо файл в текст.
**Request:**
```bash
curl -X POST http://localhost:9000/stt \
-F "file=@voice.ogg"
curl -X POST http://localhost:8895/api/stt/upload \
-F "file=@recording.webm"
```
### GET /health
**Response:**
```json
{
"text": "Привіт, це тестове повідомлення",
"filename": "recording.webm",
"language": "uk",
"model": "base"
}
```
---
### 3. GET /health
Health check endpoint.
**Response:**
```json
{
"status": "healthy",
"whisper": "available",
"model": "base"
}
```
## Інтеграція з Frontend
### 1. Оновити Enhanced Chat
**Файл:** `src/components/microdao/MicroDaoOrchestratorChatEnhanced.tsx`
```typescript
// Після запису аудіо
const handleVoiceStop = async () => {
if (mediaRecorderRef.current) {
mediaRecorderRef.current.stop();
mediaRecorderRef.current.onstop = async () => {
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
// Конвертувати в base64
const reader = new FileReader();
reader.onloadend = async () => {
const base64Audio = reader.result as string;
// Відправити на STT Service
try {
const response = await fetch('http://localhost:8895/api/stt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
audio: base64Audio,
language: 'uk',
model: 'base'
})
});
const data = await response.json();
// Додати розшифрований текст в input
setInput((prev) => prev + (prev ? ' ' : '') + data.text);
console.log('✅ STT:', data.text);
} catch (error) {
console.error('❌ STT error:', error);
// Fallback - показати що аудіо записано
setInput((prev) => prev + ' 🎤 [Голосове повідомлення]');
}
};
reader.readAsDataURL(audioBlob);
};
}
setIsRecording(false);
};
```
## Моделі Whisper
| Модель | Розмір | VRAM | Швидкість | Точність |
|--------|--------|------|-----------|----------|
| tiny | 39 MB | ~1 GB | Дуже швидко | Низька |
| base | 74 MB | ~1 GB | Швидко | Середня |
| small | 244 MB | ~2 GB | Середньо | Хороша |
| medium | 769 MB | ~5 GB | Повільно | Висока |
| large | 1550 MB | ~10 GB | Дуже повільно | Найвища |
**Рекомендація для НОДА2:** `base` (баланс швидкості та точності)
## Підтримувані мови
- 🇺🇦 Українська (uk)
- 🇬🇧 Англійська (en)
- 🇷🇺 Російська (ru)
- 🇵🇱 Польська (pl)
- 🇩🇪 Німецька (de)
- 🇫🇷 Французька (fr)
- ... і ще 90+ мов
## Тестування
### 1. cURL (base64)
```bash
# Записати аудіо
ffmpeg -f avfoundation -i ":0" -t 5 test.webm
# Конвертувати в base64
BASE64_AUDIO=$(base64 -i test.webm)
# Відправити на STT
curl -X POST http://localhost:8895/api/stt \
-H "Content-Type: application/json" \
-d "{\"audio\":\"data:audio/webm;base64,$BASE64_AUDIO\",\"language\":\"uk\"}"
```
### 2. cURL (file upload)
```bash
curl -X POST http://localhost:8895/api/stt/upload \
-F "file=@test.webm"
```
### 3. Python
```python
import requests
import base64
# Прочитати аудіо файл
with open('test.webm', 'rb') as f:
audio_bytes = f.read()
# Конвертувати в base64
audio_base64 = base64.b64encode(audio_bytes).decode()
# Відправити на STT
response = requests.post('http://localhost:8895/api/stt', json={
'audio': f'data:audio/webm;base64,{audio_base64}',
'language': 'uk',
'model': 'base'
})
print(response.json())
```
## Конфігурація
### Environment Variables
- `DASHSCOPE_API_KEY`: **Обов'язково** - API ключ DashScope для доступу до Qwen3 ASR API
- Отримати ключ: https://dashscope.console.aliyun.com/
- Встановити: `export DASHSCOPE_API_KEY="your-api-key"`
### Отримання API ключа DashScope
1. Зареєструйтеся на https://dashscope.console.aliyun.com/
2. Створіть API ключ в розділі "API Keys"
3. Встановіть змінну середовища `DASHSCOPE_API_KEY`
## Інтеграція з Gateway
Gateway автоматично використовує STT-сервіс для обробки голосових повідомлень з Telegram:
1. Користувач надсилає voice/audio/video_note
2. Gateway завантажує файл з Telegram
3. Gateway відправляє файл в STT-сервіс
4. STT повертає розпізнаний текст
5. Текст відправляється в DAGI Router як звичайне текстове повідомлення
## Встановлення залежностей
### qwen3-asr-toolkit
```bash
pip install qwen3-asr-toolkit
# .env файл
WHISPER_MODEL=base # tiny, base, small, medium, large
WHISPER_LANGUAGE=uk # uk, en, ru, pl, de, fr
```
### ffmpeg (може знадобитися для деяких форматів)
### Docker Compose
```bash
# Ubuntu/Debian
sudo apt-get install ffmpeg
# macOS
brew install ffmpeg
# Docker
Вже включено в Dockerfile
```yaml
environment:
- WHISPER_MODEL=base
- WHISPER_LANGUAGE=uk
- LOG_LEVEL=INFO
```
## Troubleshooting
### Помилка: "qwen3_asr_toolkit not available"
Встановіть бібліотеку:
```bash
pip install qwen3-asr-toolkit
```
### Помилка: "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 (див. вище). Більшість форматів обробляються без ffmpeg, але деякі можуть його потребувати.
```bash
# Ubuntu/Debian
apt-get install ffmpeg
# macOS
brew install ffmpeg
```
### Помилка: "torch not compatible"
```bash
# Переінсталювати PyTorch
pip uninstall torch torchaudio
pip install torch==2.1.0 torchaudio==2.1.0
```
### Помилка: "Whisper model not found"
```bash
# Завантажити модель вручну
python -c "import whisper; whisper.load_model('base')"
```
## Метрики
- **Endpoint:** `http://localhost:8895/metrics` (TODO)
- **Prometheus:** Інтеграція заплановано
- **Grafana:** Dashboard заплановано
## Статус
- ✅ Базова функціональність
- ✅ Docker підтримка
- ✅ Whisper AI інтеграція
- ⚠️ Потребує тестування
- 🔄 Frontend інтеграція (наступний крок)
## Автор
DAARION Team - 2025

View File

@@ -0,0 +1,254 @@
"""
STT Service - Speech-to-Text для DAARION
Конвертує аудіо файли в текст використовуючи Whisper AI
"""
from fastapi import FastAPI, HTTPException, UploadFile, File
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import logging
import os
import tempfile
import base64
from typing import Optional
import subprocess
import json
# Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(
title="STT Service",
description="Speech-to-Text Service для DAARION (Whisper AI)",
version="1.0.0"
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Конфігурація
WHISPER_MODEL = os.getenv("WHISPER_MODEL", "base") # tiny, base, small, medium, large
LANGUAGE = os.getenv("WHISPER_LANGUAGE", "uk") # ukrainian
class STTRequest(BaseModel):
audio: str # base64 encoded audio
language: Optional[str] = "uk"
model: Optional[str] = "base"
class STTResponse(BaseModel):
text: str
language: str
duration: float
model: str
confidence: Optional[float] = None
@app.get("/")
async def root():
"""Health check"""
return {
"service": "STT Service",
"status": "running",
"model": WHISPER_MODEL,
"language": LANGUAGE,
"version": "1.0.0"
}
@app.get("/health")
async def health():
"""Health check endpoint"""
try:
# Перевірити чи Whisper доступний
result = subprocess.run(
["whisper", "--help"],
capture_output=True,
text=True,
timeout=5
)
whisper_available = result.returncode == 0
return {
"status": "healthy" if whisper_available else "degraded",
"whisper": "available" if whisper_available else "unavailable",
"model": WHISPER_MODEL
}
except Exception as e:
logger.error(f"Health check failed: {e}")
return {
"status": "unhealthy",
"error": str(e)
}
@app.post("/api/stt", response_model=STTResponse)
async def speech_to_text(request: STTRequest):
"""
Конвертує аудіо в текст
Body:
{
"audio": "data:audio/webm;base64,...",
"language": "uk",
"model": "base"
}
"""
try:
logger.info("📥 Received STT request")
# Декодувати base64 audio
audio_data = request.audio
if ',' in audio_data:
audio_data = audio_data.split(',')[1]
audio_bytes = base64.b64decode(audio_data)
logger.info(f"📊 Audio size: {len(audio_bytes)} bytes")
# Зберегти у тимчасовий файл
with tempfile.NamedTemporaryFile(suffix='.webm', delete=False) as temp_audio:
temp_audio.write(audio_bytes)
audio_path = temp_audio.name
try:
# Запустити Whisper
model = request.model or WHISPER_MODEL
language = request.language or LANGUAGE
logger.info(f"🎤 Running Whisper (model={model}, language={language})")
# Whisper CLI команда
cmd = [
"whisper",
audio_path,
"--model", model,
"--language", language,
"--output_format", "json",
"--output_dir", tempfile.gettempdir()
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=60
)
if result.returncode != 0:
error_msg = result.stderr or "Whisper failed"
logger.error(f"❌ Whisper error: {error_msg}")
raise HTTPException(status_code=500, detail=f"Whisper error: {error_msg}")
# Прочитати результат
json_path = audio_path.replace('.webm', '.json')
with open(json_path, 'r', encoding='utf-8') as f:
whisper_result = json.load(f)
text = whisper_result.get('text', '').strip()
# Очистити тимчасові файли
os.unlink(audio_path)
if os.path.exists(json_path):
os.unlink(json_path)
logger.info(f"✅ Transcribed: '{text[:50]}...'")
return STTResponse(
text=text,
language=language,
duration=0.0, # TODO: отримати з Whisper
model=model,
confidence=None
)
except subprocess.TimeoutExpired:
os.unlink(audio_path)
raise HTTPException(status_code=408, detail="Whisper timeout")
except Exception as e:
if os.path.exists(audio_path):
os.unlink(audio_path)
raise
except Exception as e:
logger.error(f"❌ STT error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/stt/upload")
async def stt_upload(file: UploadFile = File(...)):
"""
Конвертує завантажений аудіо файл в текст
Form-data:
- file: audio file (webm, mp3, wav, m4a)
"""
try:
logger.info(f"📥 Received file upload: {file.filename}")
# Зберегти у тимчасовий файл
with tempfile.NamedTemporaryFile(suffix=os.path.splitext(file.filename)[1], delete=False) as temp_audio:
content = await file.read()
temp_audio.write(content)
audio_path = temp_audio.name
logger.info(f"📊 File size: {len(content)} bytes")
try:
# Запустити Whisper
cmd = [
"whisper",
audio_path,
"--model", WHISPER_MODEL,
"--language", LANGUAGE,
"--output_format", "json",
"--output_dir", tempfile.gettempdir()
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=60
)
if result.returncode != 0:
error_msg = result.stderr or "Whisper failed"
logger.error(f"❌ Whisper error: {error_msg}")
raise HTTPException(status_code=500, detail=f"Whisper error: {error_msg}")
# Прочитати результат
json_path = audio_path.replace(os.path.splitext(audio_path)[1], '.json')
with open(json_path, 'r', encoding='utf-8') as f:
whisper_result = json.load(f)
text = whisper_result.get('text', '').strip()
# Очистити тимчасові файли
os.unlink(audio_path)
if os.path.exists(json_path):
os.unlink(json_path)
logger.info(f"✅ Transcribed: '{text[:50]}...'")
return {
"text": text,
"filename": file.filename,
"language": LANGUAGE,
"model": WHISPER_MODEL
}
except Exception as e:
if os.path.exists(audio_path):
os.unlink(audio_path)
raise
except Exception as e:
logger.error(f"❌ Upload STT error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8895)

View File

@@ -0,0 +1,20 @@
version: '3.8'
services:
stt-service:
build: .
container_name: dagi-stt-service
ports:
- "8895:8895"
environment:
- WHISPER_MODEL=base
- WHISPER_LANGUAGE=uk
volumes:
- ./app:/app/app
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8895/health"]
interval: 30s
timeout: 10s
retries: 3

View File

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