init
This commit is contained in:
@@ -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"]
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
gradio==4.19.2
|
||||
requests==2.31.0
|
||||
ffmpeg-python==0.2.0
|
||||
Reference in New Issue
Block a user