From 6c4cf7ba2f328c32591c57727810a213a3c8da7c Mon Sep 17 00:00:00 2001 From: SlavaVlad Date: Wed, 25 Feb 2026 23:48:01 +0300 Subject: [PATCH] init --- .env.example | 15 ++ README.md | 118 +++++++++++ api/Dockerfile | 32 +++ api/main.py | 148 +++++++++++++ api/requirements.txt | 6 + docker-compose.yml | 51 +++++ ui/Dockerfile | 27 +++ ui/app.py | 494 +++++++++++++++++++++++++++++++++++++++++++ ui/requirements.txt | 3 + 9 files changed, 894 insertions(+) create mode 100644 .env.example create mode 100644 README.md create mode 100644 api/Dockerfile create mode 100644 api/main.py create mode 100644 api/requirements.txt create mode 100644 docker-compose.yml create mode 100644 ui/Dockerfile create mode 100644 ui/app.py create mode 100644 ui/requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9f724b8 --- /dev/null +++ b/.env.example @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ba06ac --- /dev/null +++ b/README.md @@ -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 diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..701129a --- /dev/null +++ b/api/Dockerfile @@ -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"] diff --git a/api/main.py b/api/main.py new file mode 100644 index 0000000..e7953d6 --- /dev/null +++ b/api/main.py @@ -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) diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 0000000..cc9a8d3 --- /dev/null +++ b/api/requirements.txt @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..49f1fdc --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/ui/Dockerfile b/ui/Dockerfile new file mode 100644 index 0000000..006fcb8 --- /dev/null +++ b/ui/Dockerfile @@ -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"] diff --git a/ui/app.py b/ui/app.py new file mode 100644 index 0000000..9e6e08c --- /dev/null +++ b/ui/app.py @@ -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(""" +
+

🎙️ Whisper Speed Transcribe

+

Транскрибация и перевод аудио/видео с изменением скорости

+
+ """) + + # Статус 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("
") + + # Основной интерфейс + 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("
") + + # История задач + 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 + ) diff --git a/ui/requirements.txt b/ui/requirements.txt new file mode 100644 index 0000000..2f5a1bf --- /dev/null +++ b/ui/requirements.txt @@ -0,0 +1,3 @@ +gradio==4.19.2 +requests==2.31.0 +ffmpeg-python==0.2.0