init
This commit is contained in:
15
.env.example
Normal file
15
.env.example
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Настройки Whisper модели
|
||||||
|
MODEL_SIZE=base
|
||||||
|
DEVICE=cuda
|
||||||
|
|
||||||
|
# ROCm настройки (AMD GPU)
|
||||||
|
# Версия GFX для обхода совместимости (10.3.0 = RDNA2/RX 6000 series)
|
||||||
|
HSA_OVERRIDE_GFX_VERSION=10.3.0
|
||||||
|
# Индекс GPU (обычно 0)
|
||||||
|
ROCM_VISIBLE_DEVICES=0
|
||||||
|
|
||||||
|
# URL API транскрибации (для UI)
|
||||||
|
WHISPER_API_URL=http://whisper-api:8080/transcribe
|
||||||
|
|
||||||
|
# Время хранения задач в часах
|
||||||
|
DB_RETENTION_HOURS=6
|
||||||
118
README.md
Normal file
118
README.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# Whisper Speed Transcribe
|
||||||
|
|
||||||
|
Полнофункциональный Web UI для транскрибации аудио/видео с использованием Whisper.
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- 📁 Загрузка видео (MP4, MKV, AVI, MOV, WebM) и аудио (MP3, WAV, M4A, FLAC, OGG)
|
||||||
|
- ⚡ Изменение скорости аудио (0.5x - 2.0x) без изменения высоты тона
|
||||||
|
- 🎙️ Транскрибация или перевод на английский язык
|
||||||
|
- 🔊 Прослушивание обработанного аудио после конвертации
|
||||||
|
- 📋 История задач с автоудалением через 6 часов
|
||||||
|
- 🐳 Полная поддержка Docker и Docker Compose
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
### Требования
|
||||||
|
|
||||||
|
- Docker
|
||||||
|
- Docker Compose
|
||||||
|
|
||||||
|
### Запуск
|
||||||
|
|
||||||
|
1. Клонируйте репозиторий и перейдите в директорию проекта:
|
||||||
|
```bash
|
||||||
|
cd whisper-webui
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Создайте файл `.env` на основе `.env.example`:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Запустите все сервисы:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Откройте в браузере:
|
||||||
|
- **Gradio UI**: http://localhost:7860
|
||||||
|
- **API**: http://localhost:8080
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### Через Web UI
|
||||||
|
|
||||||
|
1. Загрузите видео или аудио файл
|
||||||
|
2. Настройте скорость воспроизведения (по умолчанию 1.25x)
|
||||||
|
3. Выберите задачу: транскрибация или перевод
|
||||||
|
4. При необходимости укажите язык
|
||||||
|
5. Нажмите "Обработать файл"
|
||||||
|
6. Прослушайте обработанное аудио и скопируйте текст
|
||||||
|
|
||||||
|
### Через curl (прямой вызов API)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Транскрибация
|
||||||
|
curl -X POST -F "file=@test.m4a" http://localhost:8080/transcribe
|
||||||
|
|
||||||
|
# Транскрибация с указанием языка
|
||||||
|
curl -X POST -F "file=@test.m4a" -F "language=ru" http://localhost:8080/transcribe
|
||||||
|
|
||||||
|
# Перевод на английский
|
||||||
|
curl -X POST -F "file=@test.m4a" -F "task=translate" http://localhost:8080/transcribe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Настройки
|
||||||
|
|
||||||
|
### Переменные окружения
|
||||||
|
|
||||||
|
| Переменная | По умолчанию | Описание |
|
||||||
|
|------------|--------------|----------|
|
||||||
|
| `MODEL_SIZE` | `base` | Размер модели Whisper (tiny, base, small, medium, large) |
|
||||||
|
| `DEVICE` | `cpu` | Устройство для инференса (cpu, cuda) |
|
||||||
|
| `WHISPER_API_URL` | `http://whisper-api:8080/transcribe` | URL API транскрибации |
|
||||||
|
| `DB_RETENTION_HOURS` | `6` | Время хранения задач в часах |
|
||||||
|
|
||||||
|
### Доступные модели Whisper
|
||||||
|
|
||||||
|
| Модель | Размер | Скорость | Качество |
|
||||||
|
|--------|--------|----------|----------|
|
||||||
|
| tiny | ~39 MB | Очень быстро | Низкое |
|
||||||
|
| base | ~74 MB | Быстро | Среднее |
|
||||||
|
| small | ~244 MB | Средне | Хорошее |
|
||||||
|
| medium | ~769 MB | Медленно | Очень хорошее |
|
||||||
|
| large | ~1550 MB | Медленно | Лучшее |
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
whisper-webui/
|
||||||
|
├── docker-compose.yml # Конфигурация Docker Compose
|
||||||
|
├── .env.example # Пример переменных окружения
|
||||||
|
├── README.md # Документация
|
||||||
|
├── api/ # API сервис (Whisper)
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ ├── main.py
|
||||||
|
│ └── requirements.txt
|
||||||
|
└── ui/ # Gradio интерфейс
|
||||||
|
├── Dockerfile
|
||||||
|
├── app.py
|
||||||
|
└── requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Остановка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Примечание
|
||||||
|
|
||||||
|
- Аудио файлы не сохраняются - только метаданные и результаты транскрибации
|
||||||
|
- База данных SQLite автоматически очищает записи старше 6 часов
|
||||||
|
- Для GPU ускорения установите `DEVICE=cuda` и используйте соответствующий образ
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
MIT
|
||||||
32
api/Dockerfile
Normal file
32
api/Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# ROCm-совместимый образ с PyTorch
|
||||||
|
FROM rocm/pytorch:rocm5.7.3_ubuntu22.04_py3.10_pytorch2.1.2
|
||||||
|
|
||||||
|
# Установка системных зависимостей
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ffmpeg \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Создание рабочей директории
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Установка переменных окружения для Python
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV HSA_OVERRIDE_GFX_VERSION=10.3.0
|
||||||
|
|
||||||
|
# Установка Python зависимостей (без torch/torchaudio - уже в базовом образе)
|
||||||
|
RUN pip install --no-cache-dir \
|
||||||
|
fastapi==0.109.0 \
|
||||||
|
uvicorn[standard]==0.27.0 \
|
||||||
|
python-multipart==0.0.6 \
|
||||||
|
openai-whisper==20231117
|
||||||
|
|
||||||
|
# Копирование исходного кода
|
||||||
|
COPY main.py .
|
||||||
|
|
||||||
|
# Экспорт порта
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Запуск приложения
|
||||||
|
CMD ["python", "main.py"]
|
||||||
148
api/main.py
Normal file
148
api/main.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
"""
|
||||||
|
Whisper API - FastAPI сервис для транскрибации аудио
|
||||||
|
Поддержка ROCm (AMD GPU)
|
||||||
|
Эндпоинт: POST /transcribe
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import whisper
|
||||||
|
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
import torch
|
||||||
|
|
||||||
|
app = FastAPI(title="Whisper Transcription API")
|
||||||
|
|
||||||
|
# Загрузка модели при старте
|
||||||
|
MODEL_SIZE = os.getenv("MODEL_SIZE", "base")
|
||||||
|
DEVICE_ENV = os.getenv("DEVICE", "cuda")
|
||||||
|
|
||||||
|
# Определение устройства для инференса
|
||||||
|
# PyTorch на ROCm определяет AMD GPU как CUDA-совместимое устройство
|
||||||
|
def get_device():
|
||||||
|
"""Определение доступного устройства для инференса"""
|
||||||
|
if DEVICE_ENV == "cuda" and torch.cuda.is_available():
|
||||||
|
device_name = torch.cuda.get_device_name(0)
|
||||||
|
print(f"Используем GPU: {device_name}")
|
||||||
|
return "cuda"
|
||||||
|
else:
|
||||||
|
print("Используем CPU")
|
||||||
|
return "cpu"
|
||||||
|
|
||||||
|
DEVICE = get_device()
|
||||||
|
|
||||||
|
print(f"Загрузка модели Whisper {MODEL_SIZE} на устройство: {DEVICE}")
|
||||||
|
|
||||||
|
# Информация о GPU
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
gpu_name = torch.cuda.get_device_name(0)
|
||||||
|
gpu_memory = torch.cuda.get_device_properties(0).total_memory / (1024**3)
|
||||||
|
print(f"GPU: {gpu_name}, память: {gpu_memory:.1f} GB")
|
||||||
|
|
||||||
|
try:
|
||||||
|
model = whisper.load_model(MODEL_SIZE, device=DEVICE)
|
||||||
|
print("Модель успешно загружена")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка загрузки модели: {e}")
|
||||||
|
model = None
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""Проверка здоровья сервиса"""
|
||||||
|
gpu_info = None
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
gpu_info = {
|
||||||
|
"name": torch.cuda.get_device_name(0),
|
||||||
|
"available": True
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"status": "healthy",
|
||||||
|
"model_loaded": model is not None,
|
||||||
|
"device": DEVICE,
|
||||||
|
"gpu": gpu_info
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""Корневой эндпоинт с информацией об API"""
|
||||||
|
return {
|
||||||
|
"service": "Whisper API (ROCm/AMD GPU)",
|
||||||
|
"model": MODEL_SIZE,
|
||||||
|
"device": DEVICE,
|
||||||
|
"gpu_available": torch.cuda.is_available(),
|
||||||
|
"endpoints": {
|
||||||
|
"transcribe": "POST /transcribe",
|
||||||
|
"health": "GET /health"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/transcribe")
|
||||||
|
async def transcribe(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
task: str = Form("transcribe"),
|
||||||
|
language: str = Form(None)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Транскрибация или перевод аудио файла
|
||||||
|
|
||||||
|
Параметры:
|
||||||
|
- file: аудио/видео файл
|
||||||
|
- task: "transcribe" или "translate"
|
||||||
|
- language: код языка (опционально, например "ru", "en")
|
||||||
|
|
||||||
|
Пример curl:
|
||||||
|
curl -X POST -F "file=@test.m4a" -F "task=transcribe" http://localhost:8080/transcribe
|
||||||
|
"""
|
||||||
|
if model is None:
|
||||||
|
raise HTTPException(status_code=503, detail="Модель Whisper не загружена")
|
||||||
|
|
||||||
|
# Проверка типа задачи
|
||||||
|
if task not in ["transcribe", "translate"]:
|
||||||
|
raise HTTPException(status_code=400, detail="Недопустимое значение task. Используйте 'transcribe' или 'translate'")
|
||||||
|
|
||||||
|
# Сохранение загруженного файла во временный файл
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file.filename)[1]) as tmp_input:
|
||||||
|
content = await file.read()
|
||||||
|
tmp_input.write(content)
|
||||||
|
tmp_input_path = tmp_input.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Параметры транскрибации
|
||||||
|
transcribe_options = {
|
||||||
|
"task": task,
|
||||||
|
"verbose": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Добавляем язык если указан
|
||||||
|
if language:
|
||||||
|
transcribe_options["language"] = language
|
||||||
|
|
||||||
|
# Выполнение транскрибации
|
||||||
|
result = model.transcribe(tmp_input_path, **transcribe_options)
|
||||||
|
|
||||||
|
# Возвращаем результат
|
||||||
|
return JSONResponse(content={
|
||||||
|
"text": result["text"],
|
||||||
|
"segments": result.get("segments", []),
|
||||||
|
"language": result.get("language", language),
|
||||||
|
"task": task,
|
||||||
|
"filename": file.filename,
|
||||||
|
"device_used": DEVICE
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Ошибка при транскрибации: {str(e)}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Удаление временного файла
|
||||||
|
try:
|
||||||
|
os.unlink(tmp_input_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8080)
|
||||||
6
api/requirements.txt
Normal file
6
api/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
fastapi==0.109.0
|
||||||
|
uvicorn[standard]==0.27.0
|
||||||
|
python-multipart==0.0.6
|
||||||
|
openai-whisper==20231117
|
||||||
|
torch==2.1.2
|
||||||
|
torchaudio==2.1.2
|
||||||
51
docker-compose.yml
Normal file
51
docker-compose.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
services:
|
||||||
|
whisper-api:
|
||||||
|
build:
|
||||||
|
context: ./api
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: whisper-api
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- MODEL_SIZE=${MODEL_SIZE:-turbo}
|
||||||
|
- DEVICE=${DEVICE:-cuda}
|
||||||
|
- HSA_OVERRIDE_GFX_VERSION=${HSA_OVERRIDE_GFX_VERSION:-10.3.0}
|
||||||
|
- ROCM_VISIBLE_DEVICES=${ROCM_VISIBLE_DEVICES:-0}
|
||||||
|
volumes:
|
||||||
|
- whisper-cache:/root/.cache/whisper
|
||||||
|
devices:
|
||||||
|
- /dev/kfd:/dev/kfd
|
||||||
|
- /dev/dri:/dev/dri
|
||||||
|
# Настройки для GPU
|
||||||
|
shm_size: '10gb'
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
group_add:
|
||||||
|
- video
|
||||||
|
- render
|
||||||
|
security_opt:
|
||||||
|
- seccomp:unconfined
|
||||||
|
|
||||||
|
gradio-ui:
|
||||||
|
build:
|
||||||
|
context: ./ui
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: gradio-ui
|
||||||
|
ports:
|
||||||
|
- "7860:7860"
|
||||||
|
environment:
|
||||||
|
- WHISPER_API_URL=${WHISPER_API_URL:-http://whisper-api:8080/transcribe}
|
||||||
|
- DB_RETENTION_HOURS=${DB_RETENTION_HOURS:-6}
|
||||||
|
volumes:
|
||||||
|
- ./ui/db:/app/db
|
||||||
|
depends_on:
|
||||||
|
whisper-api:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
whisper-cache:
|
||||||
27
ui/Dockerfile
Normal file
27
ui/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
FROM rocm/pytorch:latest
|
||||||
|
|
||||||
|
# Установка системных зависимостей
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ffmpeg \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Создание рабочей директории
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Создание директории для базы данных
|
||||||
|
RUN mkdir -p /app/db
|
||||||
|
|
||||||
|
# Копирование зависимостей
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Установка Python зависимостей
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Копирование исходного кода
|
||||||
|
COPY app.py .
|
||||||
|
|
||||||
|
# Экспорт порта
|
||||||
|
EXPOSE 7860
|
||||||
|
|
||||||
|
# Запуск приложения
|
||||||
|
CMD ["python", "app.py"]
|
||||||
494
ui/app.py
Normal file
494
ui/app.py
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
"""
|
||||||
|
Gradio Web UI для Whisper транскрибации
|
||||||
|
- Загрузка видео/аудио файлов
|
||||||
|
- Обработка FFmpeg (извлечение аудио, изменение скорости)
|
||||||
|
- Вызов API транскрибации
|
||||||
|
- Сохранение результатов в SQLite с автоудалением через 6 часов
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import uuid
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
import requests
|
||||||
|
import gradio as gr
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Конфигурация из переменных окружения
|
||||||
|
WHISPER_API_URL = os.getenv("WHISPER_API_URL", "http://whisper-api:8080/transcribe")
|
||||||
|
DB_RETENTION_HOURS = int(os.getenv("DB_RETENTION_HOURS", "6"))
|
||||||
|
DB_PATH = "/app/db/jobs.db"
|
||||||
|
|
||||||
|
# Поддерживаемые языки
|
||||||
|
LANGUAGES = [
|
||||||
|
"Автоопределение", "Английский", "Русский", "Испанский", "Французский",
|
||||||
|
"Немецкий", "Итальянский", "Португальский", "Польский", "Турецкий",
|
||||||
|
"Нидерландский", "Японский", "Корейский", "Китайский"
|
||||||
|
]
|
||||||
|
|
||||||
|
LANGUAGE_CODES = {
|
||||||
|
"Автоопределение": None,
|
||||||
|
"Английский": "en",
|
||||||
|
"Русский": "ru",
|
||||||
|
"Испанский": "es",
|
||||||
|
"Французский": "fr",
|
||||||
|
"Немецкий": "de",
|
||||||
|
"Итальянский": "it",
|
||||||
|
"Португальский": "pt",
|
||||||
|
"Польский": "pl",
|
||||||
|
"Турецкий": "tr",
|
||||||
|
"Нидерландский": "nl",
|
||||||
|
"Японский": "ja",
|
||||||
|
"Корейский": "ko",
|
||||||
|
"Китайский": "zh"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def init_database():
|
||||||
|
"""Инициализация SQLite базы данных"""
|
||||||
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||||
|
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS jobs (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
task_type TEXT NOT NULL,
|
||||||
|
language TEXT,
|
||||||
|
speed REAL NOT NULL,
|
||||||
|
transcript_text TEXT NOT NULL,
|
||||||
|
api_url TEXT NOT NULL
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Удаление старых записей при запуске
|
||||||
|
cleanup_old_jobs()
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_old_jobs():
|
||||||
|
"""Удаление записей старше DB_RETENTION_HOURS часов"""
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cutoff_time = datetime.now() - timedelta(hours=DB_RETENTION_HOURS)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM jobs WHERE created_at < ?",
|
||||||
|
(cutoff_time.strftime("%Y-%m-%d %H:%M:%S"),)
|
||||||
|
)
|
||||||
|
|
||||||
|
deleted = cursor.rowcount
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if deleted > 0:
|
||||||
|
print(f"Удалено {deleted} старых записей из базы данных")
|
||||||
|
|
||||||
|
|
||||||
|
def save_job(job_id: str, filename: str, task_type: str, language: str,
|
||||||
|
speed: float, transcript_text: str):
|
||||||
|
"""Сохранение задачи в базу данных"""
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO jobs (id, created_at, filename, task_type, language, speed, transcript_text, api_url)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
''', (
|
||||||
|
job_id,
|
||||||
|
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
filename,
|
||||||
|
task_type,
|
||||||
|
language,
|
||||||
|
speed,
|
||||||
|
transcript_text,
|
||||||
|
WHISPER_API_URL
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_history():
|
||||||
|
"""Получение истории задач"""
|
||||||
|
cleanup_old_jobs() # Очистка при каждом запросе истории
|
||||||
|
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT id, created_at, filename, task_type, language, speed,
|
||||||
|
substr(transcript_text, 1, 50) as preview
|
||||||
|
FROM jobs
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 50
|
||||||
|
''')
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Форматирование для DataFrame
|
||||||
|
data = []
|
||||||
|
for row in rows:
|
||||||
|
data.append({
|
||||||
|
"ID": row[0][:8],
|
||||||
|
"Дата": row[1],
|
||||||
|
"Файл": row[2],
|
||||||
|
"Задача": row[3],
|
||||||
|
"Язык": row[4] or "Auto",
|
||||||
|
"Скорость": f"{row[5]}x",
|
||||||
|
"Текст": row[6] + "..." if len(row[6]) >= 50 else row[6]
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def check_api_health():
|
||||||
|
"""Проверка доступности API"""
|
||||||
|
try:
|
||||||
|
api_base = WHISPER_API_URL.split("/transcribe")[0]
|
||||||
|
response = requests.get(f"{api_base}/health", timeout=5)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return True, "Подключено"
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Ошибка: {str(e)}"
|
||||||
|
return False, "Недоступно"
|
||||||
|
|
||||||
|
|
||||||
|
def process_audio(file_obj, speed, task_type, language, progress=gr.Progress()):
|
||||||
|
"""
|
||||||
|
Обработка аудио/видео файла:
|
||||||
|
1. Извлечение аудио из видео (если нужно)
|
||||||
|
2. Изменение скорости аудио
|
||||||
|
3. Отправка на API транскрибации
|
||||||
|
4. Сохранение результата в БД
|
||||||
|
"""
|
||||||
|
if file_obj is None:
|
||||||
|
raise gr.Error("Пожалуйста, загрузите файл")
|
||||||
|
|
||||||
|
progress(0, desc="Инициализация...")
|
||||||
|
|
||||||
|
# Генерация уникального ID задачи
|
||||||
|
job_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Определение расширения файла
|
||||||
|
original_filename = os.path.basename(file_obj.name)
|
||||||
|
file_ext = os.path.splitext(original_filename)[1].lower()
|
||||||
|
|
||||||
|
# Создание временных файлов
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
input_path = os.path.join(temp_dir, f"input{file_ext}")
|
||||||
|
output_path = os.path.join(temp_dir, "processed.mp3")
|
||||||
|
|
||||||
|
# Копирование загруженного файла
|
||||||
|
with open(input_path, "wb") as f:
|
||||||
|
with open(file_obj.name, "rb") as original:
|
||||||
|
f.write(original.read())
|
||||||
|
|
||||||
|
progress(10, desc="Обработка аудио (FFmpeg)...")
|
||||||
|
|
||||||
|
# Определение кодека для извлечения аудио
|
||||||
|
audio_ext = file_ext.lstrip('.')
|
||||||
|
video_exts = ['.mp4', '.mkv', '.avi', '.mov', '.webm', '.flv']
|
||||||
|
|
||||||
|
# Если это видео - извлекаем аудио
|
||||||
|
if file_ext in video_exts:
|
||||||
|
# Извлечение аудио из видео
|
||||||
|
cmd_extract = [
|
||||||
|
"ffmpeg", "-y", "-i", input_path,
|
||||||
|
"-vn", "-acodec", "libmp3lame", "-q:a", "2",
|
||||||
|
output_path
|
||||||
|
]
|
||||||
|
subprocess.run(cmd_extract, capture_output=True, check=True)
|
||||||
|
input_path = output_path
|
||||||
|
|
||||||
|
# Применение изменения скорости (atempo)
|
||||||
|
# FFmpeg atempo поддерживает диапазон 0.5-2.0
|
||||||
|
speed = float(speed)
|
||||||
|
if speed != 1.0:
|
||||||
|
# Если скорость вне диапазона atempo, используем несколько проходов
|
||||||
|
atempo_values = []
|
||||||
|
remaining_speed = speed
|
||||||
|
|
||||||
|
while remaining_speed > 2.0:
|
||||||
|
atempo_values.append("2.0")
|
||||||
|
remaining_speed /= 2.0
|
||||||
|
while remaining_speed < 0.5:
|
||||||
|
atempo_values.append("0.5")
|
||||||
|
remaining_speed /= 0.5
|
||||||
|
|
||||||
|
atempo_values.append(str(round(remaining_speed, 2)))
|
||||||
|
|
||||||
|
# Применяем atempo фильтр
|
||||||
|
temp_output = os.path.join(temp_dir, "sped.mp3")
|
||||||
|
cmd_speed = [
|
||||||
|
"ffmpeg", "-y", "-i", input_path,
|
||||||
|
"-filter:a", f"atempo={','.join(atempo_values)}",
|
||||||
|
"-acodec", "libmp3lame", "-q:a", "2",
|
||||||
|
temp_output
|
||||||
|
]
|
||||||
|
subprocess.run(cmd_speed, capture_output=True, check=True)
|
||||||
|
output_path = temp_output
|
||||||
|
else:
|
||||||
|
# Просто конвертируем в mp3 если это аудио
|
||||||
|
if file_ext not in video_exts:
|
||||||
|
cmd_convert = [
|
||||||
|
"ffmpeg", "-y", "-i", input_path,
|
||||||
|
"-acodec", "libmp3lame", "-q:a", "2",
|
||||||
|
output_path
|
||||||
|
]
|
||||||
|
subprocess.run(cmd_convert, capture_output=True, check=True)
|
||||||
|
|
||||||
|
progress(50, desc="Отправка на сервер транскрибации...")
|
||||||
|
|
||||||
|
# Чтение обработанного аудио
|
||||||
|
with open(output_path, "rb") as audio_file:
|
||||||
|
audio_data = audio_file.read()
|
||||||
|
|
||||||
|
# Подготовка данных для API
|
||||||
|
files = {
|
||||||
|
"file": ("audio.mp3", audio_data, "audio/mpeg")
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"task": task_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Добавляем язык если выбран
|
||||||
|
if language and language != "Автоопределение":
|
||||||
|
data["language"] = LANGUAGE_CODES.get(language)
|
||||||
|
|
||||||
|
progress(70, desc="Транскрибация...")
|
||||||
|
|
||||||
|
# Отправка запроса к API
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
WHISPER_API_URL,
|
||||||
|
files=files,
|
||||||
|
data=data,
|
||||||
|
timeout=600 # 10 минут таймаут
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
error_msg = response.text
|
||||||
|
raise gr.Error(f"Ошибка API: {response.status_code} - {error_msg}")
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
raise gr.Error(f"Не удалось подключиться к API: {WHISPER_API_URL}")
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
raise gr.Error("Превышен таймаут ожидания ответа от API")
|
||||||
|
|
||||||
|
progress(90, desc="Сохранение результатов...")
|
||||||
|
|
||||||
|
# Сохранение в базу данных
|
||||||
|
transcript_text = result.get("text", "")
|
||||||
|
detected_language = result.get("language", language)
|
||||||
|
|
||||||
|
save_job(
|
||||||
|
job_id=job_id,
|
||||||
|
filename=original_filename,
|
||||||
|
task_type=task_type,
|
||||||
|
language=detected_language,
|
||||||
|
speed=speed,
|
||||||
|
transcript_text=transcript_text
|
||||||
|
)
|
||||||
|
|
||||||
|
progress(100, desc="Готово!")
|
||||||
|
|
||||||
|
# Возвращаем результаты
|
||||||
|
# Читаем аудио для проигрывания
|
||||||
|
with open(output_path, "rb") as f:
|
||||||
|
audio_bytes = f.read()
|
||||||
|
|
||||||
|
return (
|
||||||
|
output_path, # Audio для проигрывания
|
||||||
|
transcript_text, # Текст транскрибации
|
||||||
|
json.dumps(result.get("segments", []), ensure_ascii=False, indent=2), # Сегменты
|
||||||
|
f"Готово! Язык: {detected_language}, Задача: {task_type}" # Статус
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_ui():
|
||||||
|
"""Создание Gradio интерфейса"""
|
||||||
|
|
||||||
|
# Инициализация БД
|
||||||
|
init_database()
|
||||||
|
|
||||||
|
# Проверка статуса API
|
||||||
|
api_ok, api_status = check_api_health()
|
||||||
|
|
||||||
|
with gr.Blocks(
|
||||||
|
title="Whisper Speed Transcribe",
|
||||||
|
theme=gr.themes.Base(
|
||||||
|
primary_hue="orange",
|
||||||
|
secondary_hue="gray",
|
||||||
|
),
|
||||||
|
css="""
|
||||||
|
.main-header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(90deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.status-connected {
|
||||||
|
color: #10b981;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.status-disconnected {
|
||||||
|
color: #ef4444;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
) as app:
|
||||||
|
|
||||||
|
# Заголовок
|
||||||
|
gr.HTML("""
|
||||||
|
<div class="main-header">
|
||||||
|
<h1>🎙️ Whisper Speed Transcribe</h1>
|
||||||
|
<p>Транскрибация и перевод аудио/видео с изменением скорости</p>
|
||||||
|
</div>
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Статус API
|
||||||
|
with gr.Row():
|
||||||
|
with gr.Column(scale=3):
|
||||||
|
gr.Markdown(f"**Статус API:** {'🟢 Подключено' if api_ok else '🔴 Не подключено'}")
|
||||||
|
with gr.Column(scale=1):
|
||||||
|
gr.Markdown(f"**URL API:** `{WHISPER_API_URL}`")
|
||||||
|
|
||||||
|
gr.HTML("<hr>")
|
||||||
|
|
||||||
|
# Основной интерфейс
|
||||||
|
with gr.Row():
|
||||||
|
# Левая колонка - настройки
|
||||||
|
with gr.Column(scale=1):
|
||||||
|
gr.Markdown("### 📁 Загрузка файла")
|
||||||
|
file_input = gr.File(
|
||||||
|
label="Видео или аудио файл",
|
||||||
|
file_count="single",
|
||||||
|
file_types=[".mp4", ".mkv", ".avi", ".mov", ".webm", ".mp3", ".wav", ".m4a", ".flac", ".ogg"]
|
||||||
|
)
|
||||||
|
|
||||||
|
gr.Markdown("### ⚙️ Настройки обработки")
|
||||||
|
|
||||||
|
with gr.Accordion("Настройки аудио", open=True):
|
||||||
|
speed_slider = gr.Slider(
|
||||||
|
label="Скорость воспроизведения",
|
||||||
|
minimum=0.5,
|
||||||
|
maximum=2.0,
|
||||||
|
step=0.05,
|
||||||
|
value=1.25,
|
||||||
|
info="Ускоряет аудио без изменения высоты тона"
|
||||||
|
)
|
||||||
|
|
||||||
|
with gr.Accordion("Настройки Whisper", open=True):
|
||||||
|
task_radio = gr.Radio(
|
||||||
|
label="Задача",
|
||||||
|
choices=["transcribe", "translate"],
|
||||||
|
value="transcribe",
|
||||||
|
info="transcribe - расшифровка, translate - перевод на английский"
|
||||||
|
)
|
||||||
|
|
||||||
|
language_dropdown = gr.Dropdown(
|
||||||
|
label="Язык (опционально)",
|
||||||
|
choices=LANGUAGES,
|
||||||
|
value="Автоопределение",
|
||||||
|
info="Если не выбрано - определяется автоматически"
|
||||||
|
)
|
||||||
|
|
||||||
|
process_btn = gr.Button(
|
||||||
|
"🚀 Обработать файл",
|
||||||
|
variant="primary",
|
||||||
|
size="lg"
|
||||||
|
)
|
||||||
|
|
||||||
|
status_output = gr.Textbox(
|
||||||
|
label="Статус",
|
||||||
|
interactive=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Правая колонка - результаты
|
||||||
|
with gr.Column(scale=1):
|
||||||
|
gr.Markdown("### 🔊 Обработанное аудио")
|
||||||
|
audio_output = gr.Audio(
|
||||||
|
label="Аудио после обработки",
|
||||||
|
interactive=False
|
||||||
|
)
|
||||||
|
|
||||||
|
gr.Markdown("### 📝 Результат транскрибации")
|
||||||
|
text_output = gr.Textbox(
|
||||||
|
label="Текст",
|
||||||
|
lines=10,
|
||||||
|
interactive=False
|
||||||
|
)
|
||||||
|
|
||||||
|
with gr.Accordion("Сегменты (JSON)", open=False):
|
||||||
|
segments_output = gr.JSON(
|
||||||
|
label="Детальные сегменты"
|
||||||
|
)
|
||||||
|
|
||||||
|
gr.HTML("<hr>")
|
||||||
|
|
||||||
|
# История задач
|
||||||
|
gr.Markdown("### 📋 История задач")
|
||||||
|
|
||||||
|
with gr.Row():
|
||||||
|
refresh_btn = gr.Button("🔄 Обновить историю")
|
||||||
|
|
||||||
|
history_table = gr.Dataframe(
|
||||||
|
headers=["ID", "Дата", "Файл", "Задача", "Язык", "Скорость", "Текст"],
|
||||||
|
datatype=["str", "str", "str", "str", "str", "str", "str"],
|
||||||
|
label="Последние задачи",
|
||||||
|
max_height=300
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_history():
|
||||||
|
data = get_history()
|
||||||
|
if data:
|
||||||
|
return data
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Загрузка истории при запуске
|
||||||
|
history_table.value = load_history()
|
||||||
|
|
||||||
|
# Обработчики событий
|
||||||
|
process_btn.click(
|
||||||
|
fn=process_audio,
|
||||||
|
inputs=[file_input, speed_slider, task_radio, language_dropdown],
|
||||||
|
outputs=[audio_output, text_output, segments_output, status_output]
|
||||||
|
)
|
||||||
|
|
||||||
|
refresh_btn.click(
|
||||||
|
fn=load_history,
|
||||||
|
outputs=[history_table]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Автообновление истории каждые 30 секунд
|
||||||
|
app.load(fn=load_history, outputs=[history_table])
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(f"Запуск Gradio UI...")
|
||||||
|
print(f"API URL: {WHISPER_API_URL}")
|
||||||
|
print(f"Хранение задач: {DB_RETENTION_HOURS} часов")
|
||||||
|
|
||||||
|
app = create_ui()
|
||||||
|
app.launch(
|
||||||
|
server_name="0.0.0.0",
|
||||||
|
server_port=7860,
|
||||||
|
share=False
|
||||||
|
)
|
||||||
3
ui/requirements.txt
Normal file
3
ui/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
gradio==4.19.2
|
||||||
|
requests==2.31.0
|
||||||
|
ffmpeg-python==0.2.0
|
||||||
Reference in New Issue
Block a user