Compare commits

10 Commits

Author SHA1 Message Date
b212d83161 доработка для запуска со скрипта на хосте на ROCm 2025-09-07 16:58:55 +03:00
50fcb2d025 fix: неверный базовый образ для ROCm 2025-09-07 12:23:31 +03:00
1f13bc4f86 feat: обновление конфигурации ASR сервера и поддержка GPU NVIDIA/AMD 2025-09-07 12:01:57 +03:00
fcae47cad1 - рефакторинг приложения
- снова изменения в Readme
- работа над валидацией параметров
- большая гибкость и конфигурироемость
2025-09-06 21:05:56 +03:00
3f97810f89 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	.env.example
#	README.md
#	app.py
#	docker-compose.yml
2025-09-06 16:39:07 +03:00
d8c27b1cbb - поработал с переменными среды
- добавил ограничение максимального размера аудиофайлы (по умолчанию 50мб что дофига)
- поправил docker-compose.yml, теперь можно одной командой развернуться
- написал большую инструкцию по деплою через docker на debian
2025-09-06 16:37:02 +03:00
red
d70f2e7089 Набор всяких штук для деплоя.
Предполагается что ROCM стоит на хосте!
2025-09-03 10:50:44 +03:00
red
ce41cf4a09 - Добавлены параметры модели в гет эндпоинт 2025-08-20 23:25:05 +09:00
red
228f67d07f - Поменял всё снова на Whisper
- Добавил предзагрузку модели по-умолчанию
- Убрал метрики
- Добавил скрипты для старта
- Для отчаянных Dockerfile для сборки контейнера на 70ГБ
2025-08-20 23:18:02 +09:00
4fd0f18dd1 Merge pull request #1 from SlavaVlad/Whisper-Based
Whisper based is main now
2025-08-17 23:28:16 +09:00
18 changed files with 771 additions and 263 deletions

25
.env.example Normal file
View File

@@ -0,0 +1,25 @@
# Simple ASR Server Configuration
# Этот файл содержит переменные окружения для сервера ASR
# Сервер
HOST=0.0.0.0
PORT=9854
# Модель Whisper
DEFAULT_MODEL=turbo
MODEL_DEVICE=cuda
MODEL_DOWNLOAD_ROOT=./models
# Файлы и директории
KEYS_FILE=./data/keys.txt
LOG_LEVEL=info
# AMD GPU настройки (для ROCm)
HSA_OVERRIDE_GFX_VERSION=10.3.0
# Дополнительные настройки
AUDIO_SPEEDUP=1.25
# Настройки для GPU (по умолчанию CUDA)
# Для использования CPU установите MODEL_DEVICE=cpu
# Для AMD GPU убедитесь что установлен ROCm и используется подходящая версия PyTorch

5
.idea/.gitignore generated vendored
View File

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

7
.idea/dictionaries/project.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>конфигурироемость</w>
</words>
</dictionary>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

12
.idea/material_theme_project_new.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-6077f146:198b84bb7ea:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>

7
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.13 (simple-asr-server)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (whisper)" project-jdk-type="Python SDK" />
</project>

7
.idea/simple-asr-server.iml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

7
.idea/whisper.iml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

View File

@@ -1,22 +1,51 @@
FROM rocm/pytorch:rocm6.4.1_ubuntu22.04_py3.10_pytorch_release_2.6.0 # NVIDIA
#FROM python:3.10-slim
# AMD
FROM rocm/pytorch:latest
# Set working directory
WORKDIR /app WORKDIR /app
# Install system dependencies including openssl
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
ffmpeg \ ffmpeg \
git \
curl \
openssl \
python3-pip \ python3-pip \
python3-venv \ && rm -rf /var/lib/apt/lists/*
# Update pip
RUN pip install --upgrade pip
# Copy requirements first for better caching
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir --default-timeout=100 -r requirements.txt
COPY . . # Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app.py .
# 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 HOST=0.0.0.0
ENV PORT=9854
# Expose port
EXPOSE 9854 EXPOSE 9854
# Устанавливаем переменные окружения для ROCm # Health check
ENV HSA_OVERRIDE_GFX_VERSION=10.3.0 HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
ENV PYTORCH_ROCM_ARCH=gfx1030 CMD curl -f http://localhost:9854/health || exit 1
# Команда для запуска приложения # Run the application
CMD ["python3", "app.py"] ENTRYPOINT ["./docker-entrypoint.sh"]

282
README.MD Normal file
View File

@@ -0,0 +1,282 @@
# Simple ASR Service на базе Whisper
Простой сервис для распознавания речи с использованием OpenAI Whisper. Поддерживает различные форматы ответов, управление API-ключами без перезапуска и все параметры модели Whisper.
## Особенности
- 🎯 Эндпоинт `/transcribe` для распознавания речи
- 🔑 Управление API-ключами без перезапуска сервиса
- 📊 Три формата ответа: `json`, `simple`, `text/plain`
- ⚙️ Поддержка всех параметров `whisper.transcribe()`
- 🐳 Docker и native запуск
- 🏥 Health check эндпоинт
- 🔄 Горячая перезагрузка API-ключей
- 🚀 GPU ускорение по умолчанию (NVIDIA/AMD)
- ⚙️ Централизованная конфигурация через .env файл
## Требования
- Python 3.8+
- FFmpeg (для обработки аудио)
- Минимум 4GB RAM
- Свободное место для моделей (turbo ~1GB, large ~3GB)
- **GPU с поддержкой CUDA или ROCm (рекомендуется)**
Для Docker дополнительно:
- Docker + Docker Compose
- NVIDIA Docker runtime (для NVIDIA GPU)
- ROCm (для AMD GPU)
## Быстрый старт
### Запуск через Docker
```bash
git clone https://github.com/SlavaVlad/simple-asr-server.git ./asr
cd asr
# Для AMD GPU оставьте как есть
# Для NVIDIA GPU раскомментируйте соответствующую секцию в docker-compose.yml
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
```
### Переменные окружения
| Переменная | По умолчанию | Описание |
|------------|--------------|----------|
| `HOST` | `0.0.0.0` | IP адрес для привязки |
| `PORT` | `9854` | Порт сервера |
| `DEFAULT_MODEL` | `turbo` | Модель Whisper для загрузки |
| `MODEL_DEVICE` | `cuda` | Устройство: `cuda`, `cpu`, или `auto` |
| `MODEL_DOWNLOAD_ROOT` | `./models` | Директория для моделей |
| `KEYS_FILE` | `./data/keys.txt` | Файл с API ключами |
| `LOG_LEVEL` | `info` | Уровень логирования |
| `HSA_OVERRIDE_GFX_VERSION` | `10.3.0` | Версия GPU для AMD ROCm |
| `AUDIO_SPEEDUP` | `1.25` | Ускорение обработки аудио |
### Настройка GPU
**По умолчанию сервис настроен для работы с NVIDIA GPU.**
**Для использования CPU:**
```env
MODEL_DEVICE=cpu
```
### Доступные модели Whisper
- `tiny` - самая быстрая, наименее точная (~40MB)
- `base` - баланс скорости и качества (~150MB)
- `small` - хорошее качество (~500MB)
- `medium` - лучшее качество (~1.5GB)
- `large` - максимальное качество (~3GB)
- `turbo` - оптимизированная версия (~800MB, рекомендуется)
## Управление API-ключами
### Автоматическое создание ключей
При первом запуске:
1. Если установлен `openssl` - генерируется безопасный 64-символьный ключ
2. Если `openssl` отсутствует - создается пустой файл ключей
### Добавление/удаление ключей
1. Отредактируйте файл `data/keys.txt` (один ключ на строку, 64 hex символа)
2. Вызовите эндпоинт перезагрузки:
```bash
curl -X POST "http://localhost:9854/keys/reload" \
-H "X-API-Key: your-api-key"
```
Пример `data/keys.txt`:
```
key1
key2
```
### Генерация новых ключей
```bash
# Генерация нового ключа
openssl rand -hex 32
# Добавление в файл ключей
echo "$(openssl rand -hex 32)" >> data/keys.txt
```
## 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
{
"text": "Привет, как дела?",
"segments": [
{
"start": 0.0,
"end": 2.5,
"text": "Привет, как дела?",
"words": [...]
}
],
"language": "ru"
}
```
**simple (только текст):**
```json
{
"text": "Привет, как дела?"
}
```
**text (plain text):**
```
Привет, как дела?
```
### GET /health
Проверка состояния сервиса:
```bash
curl "http://localhost:9854/health"
```
Ответ:
```json
{
"status": "healthy",
"model_loaded": true,
"model_name": "turbo"
}
```
## Коды ошибок
- `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 секунд

104
README.md
View File

@@ -1,104 +0,0 @@
BASED ON https://github.com/salute-developers/GigaAM
# Simple ASR Server
This project provides a RESTful API for audio transcription using a Whisper model. The API is built with FastAPI and runs in a Docker container.
## Prerequisites
Before you begin, ensure you have the following installed:
* [Docker](https://docs.docker.com/get-docker/)
* [Docker Compose](https://docs.docker.com/compose/install/)
## Project Structure
```
.
├── app.py # Main application file with FastAPI endpoint
├── docker-compose.yml # Docker Compose configuration
├── Dockerfile # Dockerfile for building the application image
├── model/ # Directory for Whisper model files
└── requirements.txt # Python dependencies
```
## Setup
1. **Clone the repository:**
```bash
git clone https://github.com/SlavaVlad/simple-asr-server
cd simple-asr-server
```
3. **Add API keys:**
Create a `keys.txt` file in the root of the project and add your API keys, one per line.
## Building and Running the Project
You can build and run the project using Docker Compose.
1. **Build the Docker image:**
```bash
docker-compose build
```
2. **Run the container:**
```bash
docker-compose up
```
The application will be available at `http://0.0.0.0:9854`.
## API Endpoint
### POST /transcribe
This endpoint accepts an audio file and returns the transcription.
* **URL:** `/transcribe`
* **Method:** `POST`
* **Headers:**
* `X-API-Key`: Your API key.
* **Form Data:**
* `file`: The audio file to be transcribed.
**Example using `curl`:**
```bash
curl -X POST "http://localhost:9854/transcribe" \
-H "X-API-Key: YOUR_API_KEY" \
-F "file=@/path/to/your/audio.wav"
```
**Successful Response (200 OK):**
```json
{
"transcription": [
{
"start_time": 0.0,
"end_time": 2.5,
"transcription": "Hello world."
}
],
"text": "Hello world. ",
"metrics": {
"processing_time": 5.2,
"rtf": 0.5,
"word_rate": 2.0
}
}
```
**Error Response (401 Unauthorized):**
If the API key is missing or invalid.
```json
{
"detail": "Invalid API Key"
}
```

280
app.py
View File

@@ -1,170 +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="Формат ответа")
# API key header # Глобальные переменные для модели и ключей
api_key_header = APIKeyHeader(name="x-api-key") model = None
api_keys: Set[str] = set()
keys_lock = Lock()
keys_file_path = os.getenv("KEYS_FILE", "keys.txt")
# Схема безопасности
api_key_header = APIKeyHeader(name="X-API-Key")
def get_keys(): # не бейте меня за это app = FastAPI(title="Whisper ASR Service", version="1.0.0")
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 load_api_keys():
def convert_audio(input_path: str, output_path: str, speed: float = 1.0): """Загружает API ключи из файла"""
""" global api_keys
Convert audio to compatible format and speed up if needed.
"""
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, else:
'-y' logger.warning(f"Файл ключей {keys_file_path} не найден")
] except Exception as e:
logger.debug(f"Running FFmpeg command: {' '.join(command)}") logger.error(f"Ошибка загрузки ключей: {e}")
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
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")
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: try:
output = subprocess.check_output(cmd).decode().strip() logger.info(f"Загрузка модели Whisper: {model_name}")
return float(output) model = whisper.load_model(model_name, device=device, download_root=download_root)
except: logger.info("Модель успешно загружена")
return 0.0 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: str = "turbo", api_key: str = Depends(verify_api_key)
verbose: Optional[bool] = None,
temperature: Union[float, Tuple[float, ...]] = (0.0, 0.2, 0.4, 0.6, 0.8, 1.0),
compression_ratio_threshold: Optional[float] = 2.4,
speed_up: Optional[float] = 1.25,
logprob_threshold: Optional[float] = -1.0,
no_speech_threshold: Optional[float] = 0.6,
condition_on_previous_text: bool = True,
initial_prompt: Optional[str] = None,
word_timestamps: bool = False,
prepend_punctuations: str = "\"'\"¿([{-",
append_punctuations: str = "\"\'.。,!?:\")]}、",
clip_timestamps: Union[str, List[float]] = "0",
hallucination_silence_threshold: Optional[float] = None
): ):
# 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")
model = whisper.load_model(model) # Load the Whisper model # Готовим параметры для whisper.transcribe()
whisper_params = {}
for field_name, field_value in params.dict(exclude_none=True, exclude={'format'}).items():
whisper_params[field_name] = field_value
logger.info(f"Processing file: {file.filename} with model: {model}") # Формат ответа
response_format = params.format
# 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, speed_up): 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':
transcription_result = model.transcribe_longform( return {"text": result['text']}
temp_output_path else: # json - полный ответ по умолчанию
) return result
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
}
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)} ключей"}
@app.get("/keys/count")
def main(): async def get_keys_count(api_key: str = Depends(verify_api_key)):
import uvicorn """Возвращает количество активных ключей"""
get_keys() with keys_lock:
uvicorn.run(app, host="0.0.0.0", port=9854, log_level="debug") 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
)

20
asr.service Normal file
View File

@@ -0,0 +1,20 @@
[Unit]
Description=Whisper ASR Server (ROCM)
After=network.target
Wants=network.target
[Service]
Type=exec
User=asr
Group=asr
WorkingDirectory=/opt/asr
ExecStart=/opt/asr/start_server.sh
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=asr
[Install]
WantedBy=multi-user.target

View File

@@ -1,6 +1,40 @@
services: services:
whisper-app: asr-server:
build: . build: .
ports: ports:
- "9854:9854" - "${PORT:-9854}:${PORT:-9854}"
command: ["python", "app.py"] env_file:
- .env
environment:
- HOST=${HOST}
- PORT=${PORT}
- DEFAULT_MODEL=${DEFAULT_MODEL}
- MODEL_DEVICE=${MODEL_DEVICE}
- MODEL_DOWNLOAD_ROOT=/app/models
- KEYS_FILE=/app/data/keys.txt
- HSA_OVERRIDE_GFX_VERSION=${HSA_OVERRIDE_GFX_VERSION}
- LOG_LEVEL=${LOG_LEVEL}
- AUDIO_SPEEDUP=${AUDIO_SPEEDUP}
volumes:
- ./models:/app/models
- ./data:/app/data
# GPU support - раскомментируйте нужную секцию
# Для NVIDIA GPU:
# deploy:
# resources:
# reservations:
# devices:
# - driver: nvidia
# count: all
# capabilities: [gpu]
# Для AMD GPU (ROCm):
devices:
- /dev/kfd:/dev/kfd
- /dev/dri:/dev/dri
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:${PORT:-9854}/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s

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

@@ -1,8 +1,12 @@
# PyTorch с поддержкой ROCm
--index-url https://download.pytorch.org/whl/rocm6.0
torch
torchaudio
# Остальные зависимости
fastapi fastapi
uvicorn[standard] uvicorn[standard]
python-multipart python-multipart
gigaam openai-whisper
gigaam[longform] python-dotenv
ffmpeg-python pydantic
PyYAML
numpy<2.0.0

106
start_server.sh Executable file
View File

@@ -0,0 +1,106 @@
#!/bin/bash
# Simple ASR Server startup script for systemd
# This script loads environment variables from .env file and starts the server
set -e
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DIR="${SCRIPT_DIR}"
# Set ROCm environment variables if ROCm is available
if [ -d "/opt/rocm" ]; then
export ROCM_PATH=${ROCM_PATH:-"/opt/rocm"}
export PATH="${ROCM_PATH}/bin:${PATH}"
export LD_LIBRARY_PATH="${ROCM_PATH}/lib:${LD_LIBRARY_PATH:-}"
# Set HIP_VISIBLE_DEVICES to use all available GPUs
export HIP_VISIBLE_DEVICES=${HIP_VISIBLE_DEVICES:-"0"}
echo "ROCm detected, configured environment variables"
fi
# 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"
set -a # automatically export all variables
source "${APP_DIR}/.env"
set +a
else
echo "Warning: .env file not found at ${APP_DIR}/.env"
echo "Using default environment variables"
fi
# Set default values if not provided in .env
export HOST=${HOST:-"0.0.0.0"}
export PORT=${PORT:-9854}
export DEFAULT_MODEL=${DEFAULT_MODEL:-"turbo"}
export MODEL_DEVICE=${MODEL_DEVICE:-"cuda"}
export MODEL_DOWNLOAD_ROOT=${MODEL_DOWNLOAD_ROOT:-"${APP_DIR}/models"}
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
echo "Creating virtual environment..."
python3 -m venv "${VENV_DIR}"
fi
# Activate virtual environment
echo "Activating virtual environment..."
source "${VENV_DIR}/bin/activate"
# Install/upgrade dependencies
echo "Installing/upgrading dependencies..."
pip install --upgrade pip
pip install -r "${APP_DIR}/requirements.txt"
# Change to app directory
cd "${APP_DIR}"
echo "Starting Simple ASR Server..."
echo "Host: ${HOST}"
echo "Port: ${PORT}"
echo "Default Model: ${DEFAULT_MODEL}"
echo "Model Device: ${MODEL_DEVICE}"
echo "Model Download Root: ${MODEL_DOWNLOAD_ROOT}"
echo "Keys File: ${KEYS_FILE}"
echo "Log Level: ${LOG_LEVEL}"
# Start the server
exec python app.py