- поработал с переменными среды
- добавил ограничение максимального размера аудиофайлы (по умолчанию 50мб что дофига) - поправил docker-compose.yml, теперь можно одной командой развернуться - написал большую инструкцию по деплою через docker на debian
This commit is contained in:
25
.env.example
Normal file
25
.env.example
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Device settings
|
||||||
|
ASR_DEVICE=rocm
|
||||||
|
ASR_MODELS_ROOT=/app/models
|
||||||
|
|
||||||
|
# Server settings
|
||||||
|
PORT=9854
|
||||||
|
HOST=0.0.0.0
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Model settings
|
||||||
|
DEFAULT_MODEL=turbo
|
||||||
|
DEFAULT_LANGUAGE=ru
|
||||||
|
|
||||||
|
# Processing settings
|
||||||
|
AUDIO_SPEEDUP=1.25
|
||||||
|
MAX_UPLOAD_SIZE_MB=50 # 50MB in bytes
|
||||||
|
|
||||||
|
# Cache settings
|
||||||
|
CACHE_DIR=/app/cache
|
||||||
|
ENABLE_CACHE=true
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
CORS_ORIGINS=*
|
||||||
|
MAX_REQUESTS_PER_MINUTE=60
|
||||||
|
|
||||||
113
README.MD
Normal file
113
README.MD
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# ASR service на базе Whisper + AMD ROCm
|
||||||
|
|
||||||
|
Сервис для распознавания речи с использованием GPU AMD через ROCm.
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- GPU AMD с поддержкой ROCm (RX5000, RX6000 +)
|
||||||
|
- Установленный ROCm
|
||||||
|
- Docker + Compose plugin
|
||||||
|
- Debian based linux (для инструкции)
|
||||||
|
- Минимум 8GB RAM
|
||||||
|
- Минимум 2GB свободного места + место для ROCm + Модели (turbo ~3.5GB)
|
||||||
|
|
||||||
|
## Установка и запуск
|
||||||
|
|
||||||
|
### 1. Клонирование репозитория
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/SlavaVlad/simple-asr-server.git ./asr
|
||||||
|
cd asr
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Создание Docker-образа
|
||||||
|
|
||||||
|
Перед запуском в файле docker-compose.yml можно изменить всякие переменные окружения, например модель по умолчанию.
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
Тут он сам всё соберёт и запустится.
|
||||||
|
|
||||||
|
## Управление API-ключами
|
||||||
|
|
||||||
|
При первом запуске сервиса автоматически создается файл `keys.txt` со случайно сгенерированным API-ключом.
|
||||||
|
|
||||||
|
### Добавление новых ключей
|
||||||
|
1. Откройте файл `keys.txt`
|
||||||
|
2. Добавьте новые ключи, каждый с новой строки
|
||||||
|
3. Перезапустите сервис
|
||||||
|
|
||||||
|
Пример содержимого `keys.txt`:
|
||||||
|
```
|
||||||
|
f7a9c2b3d4e5a6b7c8d9e0f1a2b3c4d5
|
||||||
|
e8b9a2c3d4f5g6h7i8j9k0l1m2n3o4p5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование API
|
||||||
|
|
||||||
|
### Распознавание речи
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:9854/transcribe" \
|
||||||
|
-H "x-api-key: ВАШ_КЛЮЧ" \
|
||||||
|
-F "file=@путь_к_аудио_файлу" \
|
||||||
|
-F "model_name=turbo" # (необязательно, по умолчанию turbo)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ответ API
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Метрики производительности
|
||||||
|
|
||||||
|
API возвращает следующие метрики:
|
||||||
|
- `processing_time_seconds`: время обработки в секундах
|
||||||
|
- `characters_per_second`: скорость обработки (символов в секунду)
|
||||||
|
- `audio_realtime_ratio`: отношение длительности аудио к времени обработки
|
||||||
|
- `audio_duration`: длительность аудио в секундах
|
||||||
|
- `text_length`: количество символов в распознанном тексте
|
||||||
|
|
||||||
|
## Поддерживаемые форматы
|
||||||
|
|
||||||
|
Поддерживаются все аудиоформаты, которые может обработать FFmpeg. Файлы автоматически конвертируются в нужный формат.
|
||||||
|
|
||||||
|
## Решение проблем
|
||||||
|
|
||||||
|
### Проверка статуса сервиса
|
||||||
|
```bash
|
||||||
|
curl http://localhost:9854/docs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Частые проблемы
|
||||||
|
|
||||||
|
1. **Ошибка доступа к GPU**
|
||||||
|
```bash
|
||||||
|
sudo usermod -a -G video,render $USER
|
||||||
|
sudo reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Ошибка 403**
|
||||||
|
- Проверьте правильность API-ключа
|
||||||
|
- Убедитесь, что ключ добавлен в файл keys.txt
|
||||||
|
- Перезапустите сервис
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
104
README.md
104
README.md
@@ -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"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
93
app.py
93
app.py
@@ -105,6 +105,74 @@ def get_audio_duration(file_path: str) -> float:
|
|||||||
return 0.0
|
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:
|
||||||
|
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
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Transcription failed: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=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)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/transcribe")
|
@app.post("/transcribe")
|
||||||
async def transcribe_audio(
|
async def transcribe_audio(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
@@ -117,6 +185,10 @@ async def transcribe_audio(
|
|||||||
raise HTTPException(status_code=403, detail="Forbidden")
|
raise HTTPException(status_code=403, detail="Forbidden")
|
||||||
|
|
||||||
logger.info(f"Processing file: {file.filename} with model: {model_name}")
|
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')
|
||||||
|
|
||||||
metrics = TranscriptionMetrics()
|
metrics = TranscriptionMetrics()
|
||||||
|
|
||||||
# Save uploaded file
|
# Save uploaded file
|
||||||
@@ -139,6 +211,24 @@ async def transcribe_audio(
|
|||||||
logger.info("Starting transcription")
|
logger.info("Starting transcription")
|
||||||
if original_duration > 30:
|
if original_duration > 30:
|
||||||
logger.info("Audio duration > 30 seconds, using transcribe_longform")
|
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(
|
transcription_result = model.transcribe_longform(
|
||||||
temp_output_path
|
temp_output_path
|
||||||
)
|
)
|
||||||
@@ -182,7 +272,8 @@ async def transcribe_audio(
|
|||||||
def main():
|
def main():
|
||||||
import uvicorn
|
import uvicorn
|
||||||
get_keys()
|
get_keys()
|
||||||
uvicorn.run(app, host="0.0.0.0", port=9854, log_level="debug")
|
uvicorn.run(app, host="0.0.0.0", port=9854, log_level=os.getenv("LOG_LEVEL", "info"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,6 +1,27 @@
|
|||||||
services:
|
services:
|
||||||
whisper-app:
|
asr-server:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "9854:9854"
|
- "${PORT:-9854}:9854"
|
||||||
command: ["python", "app.py"]
|
environment:
|
||||||
|
- HOST=${HOST:-0.0.0.0}
|
||||||
|
- PORT=${PORT:-9854}
|
||||||
|
- DEFAULT_MODEL=${DEFAULT_MODEL:-turbo}
|
||||||
|
- MODEL_DOWNLOAD_ROOT=${MODEL_DOWNLOAD_ROOT:-/app/models}
|
||||||
|
- KEYS_FILE=${KEYS_FILE:-/app/data/keys.txt}
|
||||||
|
- HSA_OVERRIDE_GFX_VERSION=${HSA_OVERRIDE_GFX_VERSION:-10.3.0}
|
||||||
|
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||||
|
- AUDIO_SPEEDUP=${AUDIO_SPEEDUP:-1.25}
|
||||||
|
volumes:
|
||||||
|
- ./models:/app/models
|
||||||
|
- ./data:/app/data
|
||||||
|
devices:
|
||||||
|
- /dev/kfd:/dev/kfd
|
||||||
|
- /dev/dri:/dev/dri
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:9854/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
Reference in New Issue
Block a user