- рефакторинг приложения

- снова изменения в Readme
- работа над валидацией параметров
- большая гибкость и конфигурироемость
This commit is contained in:
2025-09-06 21:05:56 +03:00
parent 3f97810f89
commit fcae47cad1
8 changed files with 475 additions and 334 deletions

5
.idea/.gitignore generated vendored
View File

@@ -1,5 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@@ -4,11 +4,12 @@ FROM python:3.10-slim
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
# Install system dependencies # Install system dependencies including openssl
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
ffmpeg \ ffmpeg \
git \ git \
curl \ curl \
openssl \
python3-pip \ python3-pip \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
@@ -24,13 +25,17 @@ RUN pip install --no-cache-dir -r requirements.txt
# Copy application code # Copy application code
COPY app.py . COPY app.py .
# Create directory for models and keys # Copy startup script for key generation
COPY docker-entrypoint.sh .
RUN chmod +x docker-entrypoint.sh
# Create directory for models and data
RUN mkdir -p /app/models /app/data RUN mkdir -p /app/models /app/data
# Set environment variables # Set environment variables
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
ENV MODEL_DOWNLOAD_ROOT=/app/models ENV HOST=0.0.0.0
ENV KEYS_FILE=/app/data/keys.txt ENV PORT=9854
# Expose port # Expose port
EXPOSE 9854 EXPOSE 9854
@@ -40,4 +45,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:9854/health || exit 1 CMD curl -f http://localhost:9854/health || exit 1
# Run the application # Run the application
CMD ["python", "app.py"] ENTRYPOINT ["./docker-entrypoint.sh"]

298
README.MD
View File

@@ -1,113 +1,269 @@
# ASR service на базе Whisper + AMD ROCm # Simple ASR Service на базе Whisper
Сервис для распознавания речи с использованием GPU AMD через ROCm. Простой сервис для распознавания речи с использованием OpenAI Whisper. Поддерживает различные форматы ответов, управление API-ключами без перезапуска и все параметры модели Whisper.
## Особенности
- 🎯 Эндпоинт `/transcribe` для распознавания речи
- 🔑 Управление API-ключами без перезапуска сервиса
- 📊 Три формата ответа: `json`, `simple`, `text`
- ⚙️ Поддержка всех параметров `whisper.transcribe()`
- 🐳 Docker и native запуск
- 🏥 Health check эндпоинт
- 🔄 Горячая перезагрузка API-ключей
## Требования ## Требования
- GPU AMD с поддержкой ROCm (RX5000, RX6000 +) - Python 3.8+
- Установленный ROCm - FFmpeg (для обработки аудио)
- Docker + Compose plugin - Минимум 4GB RAM
- Debian based linux (для инструкции) - Свободное место для моделей (turbo ~1GB, large ~3GB)
- Минимум 8GB RAM
- Минимум 2GB свободного места + место для ROCm + Модели (turbo ~3.5GB)
## Установка и запуск Для Docker дополнительно:
- Docker + Docker Compose
- GPU AMD с поддержкой ROCm (опционально)
### 1. Клонирование репозитория ## Быстрый старт
### Запуск через Docker
```bash ```bash
git clone https://github.com/SlavaVlad/simple-asr-server.git ./asr git clone https://github.com/SlavaVlad/simple-asr-server.git ./asr
cd asr cd asr
```
### 2. Создание Docker-образа
Перед запуском в файле docker-compose.yml можно изменить всякие переменные окружения, например модель по умолчанию.
```bash
docker compose up -d docker compose up -d
``` ```
Тут он сам всё соберёт и запустится.
### Нативный запуск
```bash
git clone https://github.com/SlavaVlad/simple-asr-server.git ./asr
cd asr
chmod +x start_server.sh
./start_server.sh
```
## Конфигурация
Создайте файл `.env` для настройки переменных окружения:
```env
HOST=0.0.0.0
PORT=9854
DEFAULT_MODEL=turbo
MODEL_DOWNLOAD_ROOT=./models
KEYS_FILE=./data/keys.txt
LOG_LEVEL=info
```
### Доступные модели Whisper
- `tiny` - самая быстрая, наименее точная
- `base` - баланс скорости и качества
- `small` - хорошее качество
- `medium` - лучшее качество
- `large` - максимальное качество
- `turbo` - оптимизированная версия (рекомендуется)
## Управление API-ключами ## Управление API-ключами
При первом запуске сервиса автоматически создается файл `keys.txt` со случайно сгенерированным API-ключом. ### Создание ключей
### Добавление новых ключей При первом запуске автоматически создается файл `data/keys.txt` с демонстрационным ключом.
1. Откройте файл `keys.txt`
2. Добавьте новые ключи, каждый с новой строки
3. Перезапустите сервис
Пример содержимого `keys.txt`: ### Добавление/удаление ключей
```
f7a9c2b3d4e5a6b7c8d9e0f1a2b3c4d5
e8b9a2c3d4f5g6h7i8j9k0l1m2n3o4p5
```
## Использование API 1. Отредактируйте файл `data/keys.txt` (один ключ на строку)
2. Вызовите эндпоинт перезагрузки:
### Распознавание речи
```bash ```bash
curl -X POST "http://localhost:9854/transcribe" \ curl -X POST "http://localhost:9854/keys/reload" \
-H "x-api-key: ВАШ_КЛЮЧ" \ -H "X-API-Key: your-api-key"
-F "file=@путь_к_аудиоайлу" \
-F "model_name=turbo" # (необязательно, по умолчанию turbo)
``` ```
### Ответ API Пример `data/keys.txt`:
```
key1
key2
```
## API Документация
### POST /transcribe
Основной эндпоинт для распознавания речи.
#### Параметры
**Обязательные:**
- `audio_file` - аудиофайл (form-data)
**Опциональные:**
- `format` - формат ответа: `json` (по умолчанию), `simple`, `text`
- Все параметры `whisper.transcribe()`:
- `language` - язык аудио (auto-detect по умолчанию)
- `task` - `transcribe` или `translate`
- `temperature` - температура для генерации (0.0-1.0)
- `beam_size` - размер луча для поиска
- `best_of` - количество кандидатов для выбора лучшего
- `compression_ratio_threshold` - порог сжатия для фильтрации
- `logprob_threshold` - порог логарифмической вероятности
- `no_speech_threshold` - порог детекции отсутствия речи
- `condition_on_previous_text` - использовать предыдущий текст как контекст
- `initial_prompt` - начальная подсказка для модели
- `word_timestamps` - временные метки слов (true/false)
- `prepend_punctuations` - знаки препинания для добавления в начало
- `append_punctuations` - знаки препинания для добавления в конец
- `clip_timestamps` - временные метки для обрезки аудио
- `hallucination_silence_threshold` - порог тишины для детекции галлюцинаций
#### Примеры запросов
**Простая транскрибация:**
```bash
curl -X POST "http://localhost:9854/transcribe" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{}' \
--form "audio_file=@audio.wav"
```
**С параметрами:**
```bash
curl -X POST "http://localhost:9854/transcribe" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"language": "ru",
"format": "simple",
"word_timestamps": true,
"temperature": 0.2
}' \
--form "audio_file=@audio.wav"
```
**Только текст:**
```bash
curl -X POST "http://localhost:9854/transcribe" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"format": "text"}' \
--form "audio_file=@audio.wav"
```
**Расширенные параметры:**
```bash
curl -X POST "http://localhost:9854/transcribe" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"language": "en",
"task": "translate",
"temperature": 0.1,
"beam_size": 5,
"word_timestamps": true,
"initial_prompt": "This is a technical presentation about AI",
"format": "json"
}' \
--form "audio_file=@audio.wav"
```
#### Форматы ответов
**json (полный ответ от Whisper):**
```json ```json
{ {
"transcription": [...], "text": "Привет, как дела?",
"text": "распознанный текст", "segments": [
"metrics": { {
"processing_time_seconds": 1.23, "start": 0.0,
"characters_per_second": 45.6, "end": 2.5,
"audio_realtime_ratio": 2.34, "text": "Привет, как дела?",
"audio_duration": 5.67, "words": [...]
"text_length": 89
} }
],
"language": "ru"
} }
``` ```
```text **simple (только текст):**
```json
{
"text": "Привет, как дела?"
}
``` ```
## Метрики производительности **text (plain text):**
```
Привет, как дела?
```
API возвращает следующие метрики: ### GET /health
- `processing_time_seconds`: время обработки в секундах
- `characters_per_second`: скорость обработки (символов в секунду)
- `audio_realtime_ratio`: отношение длительности аудио к времени обработки
- `audio_duration`: длительность аудио в секундах
- `text_length`: количество символов в распознанном тексте
## Поддерживаемые форматы Проверка состояния сервиса:
Поддерживаются все аудиоформаты, которые может обработать FFmpeg. Файлы автоматически конвертируются в нужный формат.
## Решение проблем
### Проверка статуса сервиса
```bash ```bash
curl http://localhost:9854/docs curl "http://localhost:9854/health"
``` ```
### Частые проблемы Ответ:
```json
{
"status": "healthy",
"model_loaded": true,
"model_name": "turbo"
}
```
1. **Ошибка доступа к GPU** ### POST /keys/reload
```bash
sudo usermod -a -G video,render $USER
sudo reboot
```
2. **Ошибка 403** Перезагрузка API-ключей без перезапуска:
- Проверьте правильность API-ключа
- Убедитесь, что ключ добавлен в файл keys.txt
- Перезапустите сервис
## Лицензия ```bash
curl -X POST "http://localhost:9854/keys/reload" \
-H "X-API-Key: your-api-key"
```
MIT License ### GET /keys/count
Количество активных ключей:
```bash
curl "http://localhost:9854/keys/count" \
-H "X-API-Key: your-api-key"
```
## Коды ошибок
- `401` - API ключ не предоставлен
- `403` - Неверный API ключ
- `422` - Неверные параметры запроса
- `500` - Ошибка сервера/модели
## Systemd сервис
Для автоматического запуска создайте systemd сервис:
```bash
sudo cp asr.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable asr
sudo systemctl start asr
```
## Поддерживаемые форматы аудио
Все форматы, поддерживаемые FFmpeg:
- WAV, MP3, FLAC, M4A, OGG
- Видео форматы (извлекается аудио): MP4, AVI, MKV
## Производительность
Время обработки зависит от:
- Выбранной модели
- Длительности аудио
- Доступных ресурсов (CPU/GPU)
Примерные времена для 1 минуты аудио:
- `tiny`: ~2-5 секунд
- `turbo`: ~5-10 секунд
- `large`: ~15-30 секунд

389
app.py
View File

@@ -1,279 +1,176 @@
import logging import logging
import os import os
import subprocess import json
import time import whisper
from os import getenv from pathlib import Path
from typing import Dict from typing import Dict, Optional, Set, Literal, List, Union
from threading import Lock
import gigaam from fastapi import FastAPI, Depends, HTTPException, UploadFile, File, Request, Form
from fastapi import FastAPI, Depends, HTTPException, UploadFile, File
from fastapi.security import APIKeyHeader from fastapi.security import APIKeyHeader
from fastapi.responses import PlainTextResponse
from pydantic import BaseModel, Field
import uvicorn
# Configure logging # Настройка логирования
logging.basicConfig( logging.basicConfig(level=logging.INFO)
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
app = FastAPI() # Pydantic модель для параметров транскрибации
class TranscribeParams(BaseModel):
language: Optional[str] = Field(None, description="Язык аудио (auto-detect по умолчанию)")
task: Optional[str] = Field("transcribe", description="transcribe или translate")
temperature: Optional[float] = Field(0.0, description="Температура для генерации (0.0-1.0)")
beam_size: Optional[int] = Field(None, description="Размер луча для поиска")
best_of: Optional[int] = Field(None, description="Количество кандидатов для выбора лучшего")
compression_ratio_threshold: Optional[float] = Field(None, description="Порог сжатия для фильтрации")
logprob_threshold: Optional[float] = Field(None, description="Порог логарифмической вероятности")
no_speech_threshold: Optional[float] = Field(None, description="Порог детекции отсутствия речи")
condition_on_previous_text: Optional[bool] = Field(True, description="Использовать предыдущий текст как контекст")
initial_prompt: Optional[str] = Field(None, description="Начальная подсказка для модели")
word_timestamps: Optional[bool] = Field(False, description="Временные метки слов")
prepend_punctuations: Optional[str] = Field(None, description="Знаки препинания для добавления в начало")
append_punctuations: Optional[str] = Field(None, description="Знаки препинания для добавления в конец")
clip_timestamps: Optional[List[float]] = Field(None, description="Временные метки для обрезки аудио")
hallucination_silence_threshold: Optional[float] = Field(None, description="Порог тишины для детекции галлюцинаций")
format: Optional[Literal["json", "simple", "text"]] = Field("json", description="Формат ответа")
model = gigaam.load_model("v2_ctc", device=getenv("ASR_DEVICE"), download_root=getenv("ASR_MODELS_ROOT")) # Глобальные переменные для модели и ключей
model = None
api_keys: Set[str] = set()
keys_lock = Lock()
keys_file_path = os.getenv("KEYS_FILE", "keys.txt")
# API key header # Схема безопасности
api_key_header = APIKeyHeader(name="x-api-key") api_key_header = APIKeyHeader(name="X-API-Key")
app = FastAPI(title="Whisper ASR Service", version="1.0.0")
def get_keys(): # не бейте меня за это def load_api_keys():
keys_file = "keys.txt" """Загружает API ключи из файла"""
if not os.path.exists(keys_file): global api_keys
# Create a new keys file with a default key
default_key = os.urandom(32).hex()
with open(keys_file, "w") as f:
f.write(default_key + "\n")
logger.info(f"Created new keys file with default key: {default_key}")
return [default_key]
else:
# Read keys from the existing file
with open(keys_file, "r") as f:
keys = [line.strip() for line in f if line.strip()]
logger.info(f"Loaded {len(keys)} keys from file")
logger.debug(f"Keys: {keys}")
if not keys:
raise ValueError("No keys found in keys.txt")
return keys
def convert_audio(input_path: str, output_path: str, speed: float = 1.25):
"""
Convert audio to compatible format and speed up
"""
try: try:
command = [ if os.path.exists(keys_file_path):
'ffmpeg', '-i', input_path, with open(keys_file_path, 'r') as f:
'-filter:a', f'atempo={speed}', keys = [line.strip() for line in f.readlines() if line.strip()]
'-ar', '16000', with keys_lock:
'-ac', '1', api_keys = set(keys)
'-c:a', 'pcm_s16le', logger.info(f"Загружено {len(api_keys)} API ключей")
output_path,
'-y'
]
logger.debug(f"Running FFmpeg command: {' '.join(command)}")
subprocess.run(command, check=True, capture_output=True)
return True
except subprocess.CalledProcessError as e:
logger.error(f"FFmpeg conversion failed: {e.stderr.decode()}")
return False
class TranscriptionMetrics:
def __init__(self):
self.start_time = time.time()
self.end_time = None
self.text_length = 0
self.audio_duration = 0
def stop(self, text: str, audio_duration: float):
self.end_time = time.time()
self.text_length = len(text)
self.audio_duration = audio_duration
def get_metrics(self) -> Dict[str, float]:
processing_time = self.end_time - self.start_time
return {
"processing_time_seconds": round(processing_time, 2),
"characters_per_second": round(self.text_length / processing_time, 2),
"audio_realtime_ratio": round(self.audio_duration / processing_time, 2),
"audio_duration": round(self.audio_duration, 2),
"text_length": self.text_length
}
def get_audio_duration(file_path: str) -> float:
"""Get audio duration using ffprobe"""
cmd = [
'ffprobe',
'-v', 'quiet',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
file_path
]
try:
output = subprocess.check_output(cmd).decode().strip()
return float(output)
except:
return 0.0
@app.post("/transcribe/simple")
async def transcribe_simple(
file: UploadFile = File(...),
token: str = Depends(api_key_header),
model_name: str = "turbo"
):
# Token validation
if token not in get_keys():
logger.warning(f"Invalid token attempt: {token}")
if token == "" or token is None:
raise HTTPException(status_code=401, detail="Forbidden. x-api-key header is missing or empty.")
raise HTTPException(status_code=403, detail="Forbidden. Invalid API key.")
logger.info(f"Processing file: {file.filename} with model: {model_name}")
if file.size > int(os.getenv("MAX_UPLOAD_SIZE_MB")) * 1024 * 1024:
raise HTTPException(status_code=400, detail=f'File size exceeds ${os.getenv("MAX_UPLOAD_SIZE_MB")}MB limit')
# Save uploaded file
temp_input_path = f"/tmp/input_{file.filename}"
temp_output_path = f"/tmp/converted_{file.filename}.wav"
try:
with open(temp_input_path, "wb") as f:
f.write(await file.read())
# Convert audio if needed
logger.debug("Converting audio file")
if not convert_audio(temp_input_path, temp_output_path):
raise HTTPException(status_code=400, detail="Audio conversion failed")
# Get audio duration before speed up
original_duration = get_audio_duration(temp_input_path)
# Transcribe
logger.info("Starting transcription")
if original_duration > 30:
logger.info("Audio duration > 30 seconds, using transcribe_longform")
transcription_result = model.transcribe_longform(
temp_output_path
)
else: else:
logger.info("Audio duration <= 30 seconds, using transcribe") logger.warning(f"Файл ключей {keys_file_path} не найден")
transcription_result = model.transcribe(
temp_output_path
)
full_text = ""
for part in transcription_result:
if part["transcription"].strip() != "":
full_text += part["transcription"].strip() + " "
result = full_text
return result
except Exception as e: except Exception as e:
logger.error(f"Transcription failed: {str(e)}") logger.error(f"Ошибка загрузки ключей: {e}")
raise HTTPException(status_code=500, detail=str(e))
finally: def load_model():
# Cleanup temporary files """Загружает модель Whisper"""
if os.path.exists(temp_input_path): global model
os.remove(temp_input_path) model_name = os.getenv("DEFAULT_MODEL", "turbo")
if os.path.exists(temp_output_path): download_root = os.getenv("MODEL_DOWNLOAD_ROOT", "./models")
os.remove(temp_output_path) device = os.getenv("MODEL_DEVICE", "cpu")
try:
logger.info(f"Загрузка модели Whisper: {model_name}")
model = whisper.load_model(model_name, device=device, download_root=download_root)
logger.info("Модель успешно загружена")
except Exception as e:
logger.error(f"Ошибка загрузки модели: {e}")
raise
def verify_api_key(api_key: str = Depends(api_key_header)) -> str:
"""Проверяет API ключ"""
if not api_key:
raise HTTPException(status_code=401, detail="API ключ не предоставлен")
# Перезагружаем ключи для проверки обновлений
load_api_keys()
with keys_lock:
if api_key not in api_keys:
raise HTTPException(status_code=403, detail="Неверный API ключ")
return api_key
@app.on_event("startup")
async def startup_event():
"""Инициализация при запуске"""
load_api_keys()
load_model()
@app.get("/health")
async def health_check():
"""Проверка здоровья сервиса"""
return {"status": "healthy", "model_loaded": model is not None, "current_model": str(model) if model else None}
@app.post("/transcribe") @app.post("/transcribe")
async def transcribe_audio( async def transcribe_audio(
file: UploadFile = File(...), audio_file: UploadFile = File(...),
token: str = Depends(api_key_header), params: TranscribeParams = Depends(),
model_name: str = "turbo" api_key: str = Depends(verify_api_key)
): ):
# Token validation """Транскрибирует аудиофайл"""
if token not in get_keys(): if model is None:
logger.warning(f"Invalid token attempt: {token}") raise HTTPException(status_code=500, detail="Модель не загружена")
raise HTTPException(status_code=403, detail="Forbidden")
logger.info(f"Processing file: {file.filename} with model: {model_name}") # Готовим параметры для whisper.transcribe()
whisper_params = {}
for field_name, field_value in params.dict(exclude_none=True, exclude={'format'}).items():
whisper_params[field_name] = field_value
if file.size > int(os.getenv("MAX_UPLOAD_SIZE_MB")) * 1024 * 1024: # Формат ответа
raise HTTPException(status_code=400, detail=f'File size exceeds ${os.getenv("MAX_UPLOAD_SIZE_MB")}MB limit') response_format = params.format
metrics = TranscriptionMetrics()
# Save uploaded file
temp_input_path = f"/tmp/input_{file.filename}"
temp_output_path = f"/tmp/converted_{file.filename}.wav"
temp_file_path = None
try: try:
with open(temp_input_path, "wb") as f: # Сохраняем временный файл
f.write(await file.read()) temp_file_path = f"/tmp/{audio_file.filename}"
with open(temp_file_path, "wb") as temp_file:
content = await audio_file.read()
temp_file.write(content)
# Convert audio if needed # Транскрибируем
logger.debug("Converting audio file") logger.info(f"Транскрибация файла: {audio_file.filename} с параметрами: {whisper_params}")
if not convert_audio(temp_input_path, temp_output_path): result = model.transcribe(temp_file_path, **whisper_params)
raise HTTPException(status_code=400, detail="Audio conversion failed")
# Get audio duration before speed up # Удаляем временный файл
original_duration = get_audio_duration(temp_input_path) os.unlink(temp_file_path)
# Transcribe # Возвращаем результат в нужном формате
logger.info("Starting transcription") if response_format == 'text':
if original_duration > 30: return PlainTextResponse(content=result['text'])
logger.info("Audio duration > 30 seconds, using transcribe_longform") elif response_format == 'simple':
cmd = [ return {"text": result['text']}
'ffmpeg', '-i', temp_input_path, else: # json - полный ответ по умолчанию
'-filter:a', f'atempo={os.getenv("AUDIO_SPEEDUP", 1.25)}', return result
'-ar', '16000',
'-ac', '1',
'-c:a', 'pcm_s16le',
temp_output_path,
'-y'
]
log = subprocess.run(cmd, check=True, capture_output=True)
logger.debug(f"Running FFmpeg command: {' '.join(cmd)}")
logger.info("Audio sped up for longform transcription")
if log.stderr:
logger.error(f"FFmpeg err log: {log.stderr.decode()}")
logger.debug(f"FFmpeg log: {log.stdout.decode()}")
else:
logger.debug(f"FFmpeg log: {log.stdout.decode()}")
transcription_result = model.transcribe_longform(
temp_output_path
)
else:
logger.info("Audio duration <= 30 seconds, using transcribe")
transcription_result = model.transcribe(
temp_output_path
)
full_text = ""
for part in transcription_result:
if part["transcription"].strip() != "":
full_text += part["transcription"].strip() + " "
result = {
"transcription": transcription_result,
"text": full_text
}
# Calculate metrics
metrics.stop(full_text, original_duration)
logger.info(f"Transcription metrics: {metrics.get_metrics()}")
# Add metrics to result
result["metrics"] = metrics.get_metrics()
return result
except Exception as e: except Exception as e:
logger.error(f"Transcription failed: {str(e)}") logger.error(f"Ошибка транскрибации: {e}")
raise HTTPException(status_code=500, detail=str(e)) # Удаляем временный файл в случае ошибки
if temp_file_path and os.path.exists(temp_file_path):
os.unlink(temp_file_path)
raise HTTPException(status_code=500, detail=f"Ошибка транскрибации: {str(e)}")
finally: @app.post("/keys/reload")
# Cleanup temporary files async def reload_keys(api_key: str = Depends(verify_api_key)):
if os.path.exists(temp_input_path): """Перезагружает ключи из файла"""
os.remove(temp_input_path) load_api_keys()
if os.path.exists(temp_output_path): with keys_lock:
os.remove(temp_output_path) return {"message": f"Перезагружено {len(api_keys)} ключей"}
def main():
import uvicorn
get_keys()
uvicorn.run(app, host="0.0.0.0", port=9854, log_level=os.getenv("LOG_LEVEL", "info"))
@app.get("/keys/count")
async def get_keys_count(api_key: str = Depends(verify_api_key)):
"""Возвращает количество активных ключей"""
with keys_lock:
return {"count": len(api_keys)}
if __name__ == "__main__": if __name__ == "__main__":
main() host = os.getenv("HOST", "0.0.0.0")
port = int(os.getenv("PORT", "9854"))
log_level = os.getenv("LOG_LEVEL", "info")
uvicorn.run(
"app:app",
host=host,
port=port,
log_level=log_level,
reload=False
)

59
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,59 @@
#!/bin/bash
# Docker entrypoint script for ASR service
# Generates API keys if needed and starts the server
set -e
# Function to generate a secure API key
generate_api_key() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32
else
echo ""
fi
}
# Set default values for environment variables
export HOST=${HOST:-"0.0.0.0"}
export PORT=${PORT:-9854}
export DEFAULT_MODEL=${DEFAULT_MODEL:-"turbo"}
export MODEL_DOWNLOAD_ROOT=${MODEL_DOWNLOAD_ROOT:-"/app/models"}
export KEYS_FILE=${KEYS_FILE:-"/app/data/keys.txt"}
export LOG_LEVEL=${LOG_LEVEL:-"info"}
# Create necessary directories
mkdir -p "${MODEL_DOWNLOAD_ROOT}"
mkdir -p "$(dirname "${KEYS_FILE}")"
# Check if keys file exists, create with generated key if not
if [ ! -f "${KEYS_FILE}" ]; then
echo "Creating default keys file..."
# Try to generate a secure key with openssl
GENERATED_KEY=$(generate_api_key)
if [ -n "${GENERATED_KEY}" ]; then
echo "${GENERATED_KEY}" > "${KEYS_FILE}"
echo "Generated secure API key using openssl: ${GENERATED_KEY}"
echo "Created keys file at: ${KEYS_FILE}"
else
echo "WARNING: openssl not found! Cannot generate secure API key."
echo "Please manually add API keys to ${KEYS_FILE}"
echo "Each key should be 64 hex characters (32 bytes) on a separate line."
echo ""
echo "Creating empty keys file - you must add keys manually before starting the server."
touch "${KEYS_FILE}"
fi
fi
echo "Starting Simple ASR Server in Docker..."
echo "Host: ${HOST}"
echo "Port: ${PORT}"
echo "Default Model: ${DEFAULT_MODEL}"
echo "Model Download Root: ${MODEL_DOWNLOAD_ROOT}"
echo "Keys File: ${KEYS_FILE}"
echo "Log Level: ${LOG_LEVEL}"
# Start the server
exec python app.py

View File

@@ -3,5 +3,4 @@ uvicorn[standard]
python-multipart python-multipart
openai-whisper openai-whisper
python-dotenv python-dotenv
pydantic

View File

@@ -9,6 +9,15 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DIR="${SCRIPT_DIR}" APP_DIR="${SCRIPT_DIR}"
# Function to generate a secure API key
generate_api_key() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32
else
echo ""
fi
}
# Load environment variables from .env file if it exists # Load environment variables from .env file if it exists
if [ -f "${APP_DIR}/.env" ]; then if [ -f "${APP_DIR}/.env" ]; then
echo "Loading environment variables from ${APP_DIR}/.env" echo "Loading environment variables from ${APP_DIR}/.env"
@@ -25,13 +34,35 @@ export HOST=${HOST:-"0.0.0.0"}
export PORT=${PORT:-9854} export PORT=${PORT:-9854}
export DEFAULT_MODEL=${DEFAULT_MODEL:-"turbo"} export DEFAULT_MODEL=${DEFAULT_MODEL:-"turbo"}
export MODEL_DOWNLOAD_ROOT=${MODEL_DOWNLOAD_ROOT:-"${APP_DIR}/models"} export MODEL_DOWNLOAD_ROOT=${MODEL_DOWNLOAD_ROOT:-"${APP_DIR}/models"}
export KEYS_FILE=${KEYS_FILE:-"${APP_DIR}/keys.txt"} export KEYS_FILE=${KEYS_FILE:-"${APP_DIR}/data/keys.txt"}
export LOG_LEVEL=${LOG_LEVEL:-"INFO"} export LOG_LEVEL=${LOG_LEVEL:-"info"}
# Create necessary directories # Create necessary directories
mkdir -p "${MODEL_DOWNLOAD_ROOT}" mkdir -p "${MODEL_DOWNLOAD_ROOT}"
mkdir -p "$(dirname "${KEYS_FILE}")" mkdir -p "$(dirname "${KEYS_FILE}")"
# Check if keys file exists, create with generated key if not
if [ ! -f "${KEYS_FILE}" ]; then
echo "Creating default keys file..."
# Try to generate a secure key with openssl
GENERATED_KEY=$(generate_api_key)
if [ -n "${GENERATED_KEY}" ]; then
echo "${GENERATED_KEY}" > "${KEYS_FILE}"
echo "Generated secure API key using openssl: ${GENERATED_KEY}"
echo "Created keys file at: ${KEYS_FILE}"
else
echo "WARNING: openssl not found! Cannot generate secure API key."
echo "Please manually add API keys to ${KEYS_FILE}"
echo "Each key should be 64 hex characters (32 bytes) on a separate line."
echo "Example key format: 0000000000000000000000000000000000000000000000000000000000000000"
echo ""
echo "Creating empty keys file - you must add keys manually before starting the server."
touch "${KEYS_FILE}"
fi
fi
# Check if virtual environment exists, create if not # Check if virtual environment exists, create if not
VENV_DIR="${APP_DIR}/venv" VENV_DIR="${APP_DIR}/venv"
if [ ! -d "${VENV_DIR}" ]; then if [ ! -d "${VENV_DIR}" ]; then
@@ -59,6 +90,5 @@ echo "Model Download Root: ${MODEL_DOWNLOAD_ROOT}"
echo "Keys File: ${KEYS_FILE}" echo "Keys File: ${KEYS_FILE}"
echo "Log Level: ${LOG_LEVEL}" echo "Log Level: ${LOG_LEVEL}"
# Start the application # Start the server
exec python3 app.py exec python app.py