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

- снова изменения в 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
WORKDIR /app
# Install system dependencies
# Install system dependencies including openssl
RUN apt-get update && apt-get install -y \
ffmpeg \
git \
curl \
openssl \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
@@ -24,13 +25,17 @@ RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
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
# Set environment variables
ENV PYTHONUNBUFFERED=1
ENV MODEL_DOWNLOAD_ROOT=/app/models
ENV KEYS_FILE=/app/data/keys.txt
ENV HOST=0.0.0.0
ENV PORT=9854
# Expose port
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
# 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 +)
- Установленный ROCm
- Docker + Compose plugin
- Debian based linux (для инструкции)
- Минимум 8GB RAM
- Минимум 2GB свободного места + место для ROCm + Модели (turbo ~3.5GB)
- Python 3.8+
- FFmpeg (для обработки аудио)
- Минимум 4GB RAM
- Свободное место для моделей (turbo ~1GB, large ~3GB)
## Установка и запуск
Для Docker дополнительно:
- Docker + Docker Compose
- GPU AMD с поддержкой ROCm (опционально)
### 1. Клонирование репозитория
## Быстрый старт
### Запуск через Docker
```bash
git clone https://github.com/SlavaVlad/simple-asr-server.git ./asr
cd asr
```
### 2. Создание Docker-образа
Перед запуском в файле docker-compose.yml можно изменить всякие переменные окружения, например модель по умолчанию.
```bash
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-ключами
При первом запуске сервиса автоматически создается файл `keys.txt` со случайно сгенерированным API-ключом.
### Создание ключей
### Добавление новых ключей
1. Откройте файл `keys.txt`
2. Добавьте новые ключи, каждый с новой строки
3. Перезапустите сервис
При первом запуске автоматически создается файл `data/keys.txt` с демонстрационным ключом.
Пример содержимого `keys.txt`:
```
f7a9c2b3d4e5a6b7c8d9e0f1a2b3c4d5
e8b9a2c3d4f5g6h7i8j9k0l1m2n3o4p5
```
### Добавление/удаление ключей
## Использование API
### Распознавание речи
1. Отредактируйте файл `data/keys.txt` (один ключ на строку)
2. Вызовите эндпоинт перезагрузки:
```bash
curl -X POST "http://localhost:9854/transcribe" \
-H "x-api-key: ВАШ_КЛЮЧ" \
-F "file=@путь_к_аудиоайлу" \
-F "model_name=turbo" # (необязательно, по умолчанию turbo)
curl -X POST "http://localhost:9854/keys/reload" \
-H "X-API-Key: your-api-key"
```
### Ответ 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
{
"transcription": [...],
"text": "распознанный текст",
"metrics": {
"processing_time_seconds": 1.23,
"characters_per_second": 45.6,
"audio_realtime_ratio": 2.34,
"audio_duration": 5.67,
"text_length": 89
"text": "Привет, как дела?",
"segments": [
{
"start": 0.0,
"end": 2.5,
"text": "Привет, как дела?",
"words": [...]
}
],
"language": "ru"
}
```
```text
**simple (только текст):**
```json
{
"text": "Привет, как дела?"
}
```
## Метрики производительности
**text (plain text):**
```
Привет, как дела?
```
API возвращает следующие метрики:
- `processing_time_seconds`: время обработки в секундах
- `characters_per_second`: скорость обработки (символов в секунду)
- `audio_realtime_ratio`: отношение длительности аудио к времени обработки
- `audio_duration`: длительность аудио в секундах
- `text_length`: количество символов в распознанном тексте
### GET /health
## Поддерживаемые форматы
Проверка состояния сервиса:
Поддерживаются все аудиоформаты, которые может обработать FFmpeg. Файлы автоматически конвертируются в нужный формат.
## Решение проблем
### Проверка статуса сервиса
```bash
curl http://localhost:9854/docs
curl "http://localhost:9854/health"
```
### Частые проблемы
Ответ:
```json
{
"status": "healthy",
"model_loaded": true,
"model_name": "turbo"
}
```
1. **Ошибка доступа к GPU**
```bash
sudo usermod -a -G video,render $USER
sudo reboot
```
### POST /keys/reload
2. **Ошибка 403**
- Проверьте правильность API-ключа
- Убедитесь, что ключ добавлен в файл keys.txt
- Перезапустите сервис
Перезагрузка API-ключей без перезапуска:
## Лицензия
```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 os
import subprocess
import time
from os import getenv
from typing import Dict
import json
import whisper
from pathlib import Path
from typing import Dict, Optional, Set, Literal, List, Union
from threading import Lock
import gigaam
from fastapi import FastAPI, Depends, HTTPException, UploadFile, File
from fastapi import FastAPI, Depends, HTTPException, UploadFile, File, Request, Form
from fastapi.security import APIKeyHeader
from fastapi.responses import PlainTextResponse
from pydantic import BaseModel, Field
import uvicorn
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Настройка логирования
logging.basicConfig(level=logging.INFO)
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(): # не бейте меня за это
keys_file = "keys.txt"
if not os.path.exists(keys_file):
# 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
"""
def load_api_keys():
"""Загружает API ключи из файла"""
global api_keys
try:
command = [
'ffmpeg', '-i', input_path,
'-filter:a', f'atempo={speed}',
'-ar', '16000',
'-ac', '1',
'-c:a', 'pcm_s16le',
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
)
if os.path.exists(keys_file_path):
with open(keys_file_path, 'r') as f:
keys = [line.strip() for line in f.readlines() if line.strip()]
with keys_lock:
api_keys = set(keys)
logger.info(f"Загружено {len(api_keys)} API ключей")
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 = full_text
return result
logger.warning(f"Файл ключей {keys_file_path} не найден")
except Exception as e:
logger.error(f"Transcription failed: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
logger.error(f"Ошибка загрузки ключей: {e}")
finally:
# Cleanup temporary files
if os.path.exists(temp_input_path):
os.remove(temp_input_path)
if os.path.exists(temp_output_path):
os.remove(temp_output_path)
def load_model():
"""Загружает модель Whisper"""
global model
model_name = os.getenv("DEFAULT_MODEL", "turbo")
download_root = os.getenv("MODEL_DOWNLOAD_ROOT", "./models")
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")
async def transcribe_audio(
file: UploadFile = File(...),
token: str = Depends(api_key_header),
model_name: str = "turbo"
audio_file: UploadFile = File(...),
params: TranscribeParams = Depends(),
api_key: str = Depends(verify_api_key)
):
# Token validation
if token not in get_keys():
logger.warning(f"Invalid token attempt: {token}")
raise HTTPException(status_code=403, detail="Forbidden")
"""Транскрибирует аудиофайл"""
if model is None:
raise HTTPException(status_code=500, detail="Модель не загружена")
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')
metrics = TranscriptionMetrics()
# Save uploaded file
temp_input_path = f"/tmp/input_{file.filename}"
temp_output_path = f"/tmp/converted_{file.filename}.wav"
# Формат ответа
response_format = params.format
temp_file_path = None
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")
if not convert_audio(temp_input_path, temp_output_path):
raise HTTPException(status_code=400, detail="Audio conversion failed")
# Транскрибируем
logger.info(f"Транскрибация файла: {audio_file.filename} с параметрами: {whisper_params}")
result = model.transcribe(temp_file_path, **whisper_params)
# 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 original_duration > 30:
logger.info("Audio duration > 30 seconds, using transcribe_longform")
cmd = [
'ffmpeg', '-i', temp_input_path,
'-filter:a', f'atempo={os.getenv("AUDIO_SPEEDUP", 1.25)}',
'-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
# Возвращаем результат в нужном формате
if response_format == 'text':
return PlainTextResponse(content=result['text'])
elif response_format == 'simple':
return {"text": result['text']}
else: # json - полный ответ по умолчанию
return result
except Exception as e:
logger.error(f"Transcription failed: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
logger.error(f"Ошибка транскрибации: {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:
# Cleanup temporary files
if os.path.exists(temp_input_path):
os.remove(temp_input_path)
if os.path.exists(temp_output_path):
os.remove(temp_output_path)
def main():
import uvicorn
get_keys()
uvicorn.run(app, host="0.0.0.0", port=9854, log_level=os.getenv("LOG_LEVEL", "info"))
@app.post("/keys/reload")
async def reload_keys(api_key: str = Depends(verify_api_key)):
"""Перезагружает ключи из файла"""
load_api_keys()
with keys_lock:
return {"message": f"Перезагружено {len(api_keys)} ключей"}
@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__":
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
openai-whisper
python-dotenv
pydantic

View File

@@ -9,6 +9,15 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
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
if [ -f "${APP_DIR}/.env" ]; then
echo "Loading environment variables from ${APP_DIR}/.env"
@@ -25,13 +34,35 @@ 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_DIR}/models"}
export KEYS_FILE=${KEYS_FILE:-"${APP_DIR}/keys.txt"}
export LOG_LEVEL=${LOG_LEVEL:-"INFO"}
export KEYS_FILE=${KEYS_FILE:-"${APP_DIR}/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 "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
VENV_DIR="${APP_DIR}/venv"
if [ ! -d "${VENV_DIR}" ]; then
@@ -59,6 +90,5 @@ echo "Model Download Root: ${MODEL_DOWNLOAD_ROOT}"
echo "Keys File: ${KEYS_FILE}"
echo "Log Level: ${LOG_LEVEL}"
# Start the application
exec python3 app.py
# Start the server
exec python app.py