mus / main.py
Starchik's picture
Update main.py
a6fde8a verified
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
main.py — одностраничный Flask сервер для онлайн-обработки аудио с множеством эффектов.
Запуск: gunicorn -b 0.0.0.0:7860 main:app
Требования (requirements.txt):
flask
pydub
gunicorn
python-multipart
ВАЖНО:
- ffmpeg должен быть установлен в контейнере (apt-get install -y ffmpeg перед сменой пользователя).
- Этот файл использует pydub и иногда вызывает ffmpeg через subprocess для некоторых эффектов (echo, pitch_shift, reverb).
"""
import os
import io
import uuid
import time
import threading
import subprocess
from pathlib import Path
from typing import List, Dict, Any
from flask import (
Flask, request, jsonify, url_for, send_file,
render_template_string, abort
)
from pydub import AudioSegment, effects, silence
# ----------------------------
# Конфигурация
# ----------------------------
TMP_DIR = Path("./tmp")
TMP_DIR.mkdir(exist_ok=True)
KEEP_FILES_SEC = 60 * 60 # храним файлы 1 час
MAX_UPLOAD_MB = 200
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = MAX_UPLOAD_MB * 1024 * 1024
# job state
job_logs: Dict[str, List[Dict[str, Any]]] = {}
job_files: Dict[str, Path] = {}
jobs_lock = threading.Lock()
# ----------------------------
# Утилиты
# ----------------------------
def random_filename(ext: str) -> Path:
return TMP_DIR / (str(uuid.uuid4()) + "." + ext)
def secure_extension_for_format(fmt: str) -> str:
fmt = (fmt or "").lower().strip()
if fmt in ("mp3", "mpeg"):
return "mp3"
if fmt in ("wav", "wave"):
return "wav"
if fmt in ("flac",):
return "flac"
if fmt in ("ogg", "vorbis"):
return "ogg"
if fmt in ("m4a", "mp4", "aac"):
return "m4a"
return "wav"
def cleanup_old_files():
now = time.time()
for p in list(TMP_DIR.iterdir()):
try:
if p.is_file() and (now - p.stat().st_mtime) > KEEP_FILES_SEC:
p.unlink()
except Exception:
pass
def load_audio_from_filestorage(filestorage):
data = filestorage.read()
buf = io.BytesIO(data)
filename = getattr(filestorage, "filename", "") or ""
ext = ""
if "." in filename:
ext = filename.rsplit(".", 1)[1].lower()
# Try extension-guessing first
if ext:
try:
buf.seek(0)
return AudioSegment.from_file(buf, format=ext)
except Exception:
pass
# Try common formats
for fmt in ("mp3", "wav", "ogg", "flac", "m4a", "aac"):
try:
buf.seek(0)
return AudioSegment.from_file(buf, format=fmt)
except Exception:
continue
buf.seek(0)
return AudioSegment.from_file(buf)
# ----------------------------
# Pydub-based эффекты
# ----------------------------
def apply_trim_silence(seg: AudioSegment, silence_thresh_db=-50, chunk_len_ms=10):
non_silence = silence.detect_nonsilent(seg, min_silence_len=chunk_len_ms, silence_thresh=silence_thresh_db)
if not non_silence:
return seg[:0]
start = non_silence[0][0]
end = non_silence[-1][1]
return seg[start:end]
def apply_lowpass(seg: AudioSegment, cutoff_hz: float):
if cutoff_hz and cutoff_hz > 0:
return seg.low_pass_filter(int(cutoff_hz))
return seg
def apply_highpass(seg: AudioSegment, cutoff_hz: float):
if cutoff_hz and cutoff_hz > 0:
return seg.high_pass_filter(int(cutoff_hz))
return seg
def apply_speed_change(seg: AudioSegment, speed: float):
if speed <= 0 or abs(speed - 1.0) < 1e-6:
return seg
try:
return effects.speedup(seg, playback_speed=speed)
except Exception:
new_frame_rate = int(seg.frame_rate * speed)
return seg._spawn(seg.raw_data, overrides={"frame_rate": new_frame_rate}).set_frame_rate(seg.frame_rate)
def apply_normalize(seg: AudioSegment):
return effects.normalize(seg)
def apply_gain(seg: AudioSegment, db: float):
try:
return seg.apply_gain(float(db))
except Exception:
return seg
def apply_fade(seg: AudioSegment, fade_in_ms: int, fade_out_ms: int):
if fade_in_ms and fade_in_ms > 0:
seg = seg.fade_in(int(fade_in_ms))
if fade_out_ms and fade_out_ms > 0:
seg = seg.fade_out(int(fade_out_ms))
return seg
def apply_reverse(seg: AudioSegment):
try:
return seg.reverse()
except Exception:
return seg
# ----------------------------
# FFmpeg-based helpers для echo/reverb/pitch shift
# ----------------------------
def ffmpeg_apply_filter_on_segment(seg: AudioSegment, filter_str: str) -> AudioSegment:
"""
Экспорт сегмента во временный WAV, применить ffmpeg -af filter_str, загрузить обратно.
"""
in_tmp = random_filename("wav")
out_tmp = random_filename("wav")
try:
seg.export(in_tmp.as_posix(), format="wav")
cmd = [
"ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
"-i", in_tmp.as_posix(),
"-af", filter_str,
out_tmp.as_posix()
]
subprocess.run(cmd, check=True)
result = AudioSegment.from_file(out_tmp.as_posix(), format="wav")
except subprocess.CalledProcessError as e:
raise RuntimeError(f"ffmpeg error: {e}")
finally:
try:
if in_tmp.exists():
in_tmp.unlink()
except Exception:
pass
# clean out_tmp later by caller (we'll try to remove now)
try:
if out_tmp.exists():
out_tmp.unlink()
except Exception:
pass
return result
def ffmpeg_apply_filter_files(input_path: Path, filter_str: str, out_path: Path):
cmd = [
"ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
"-i", input_path.as_posix(),
"-af", filter_str,
out_path.as_posix()
]
subprocess.run(cmd, check=True)
def apply_echo_with_ffmpeg(seg: AudioSegment, delay_ms: float = 500, decay: float = 0.5):
# aecho=in_gain:out_gain:delays:decays (delays in ms, decays 0..1)
delays = str(int(delay_ms))
decays = str(float(decay))
filter_str = f"aecho=0.8:0.9:{delays}:{decays}"
return ffmpeg_apply_filter_on_segment(seg, filter_str)
def apply_reverb_with_ffmpeg(seg: AudioSegment, reverb_level: float = 50.0):
# Emulate reverb using multiple echoes — simple approach
# Compose multiple delays and decays
d1 = 60; d2 = 120; d3 = 180; d4 = 240
dec1 = max(0.1, min(0.9, reverb_level / 100.0))
# build aecho with multiple delays/decays: delays separated by '|' and decays same length
filter_str = f"aecho=0.7:0.75:{d1}|{d2}|{d3}|{d4}:{dec1}|{dec1*0.6}|{dec1*0.4}|{dec1*0.2}"
return ffmpeg_apply_filter_on_segment(seg, filter_str)
def apply_pitch_shift_with_ffmpeg(seg: AudioSegment, semitones: float):
"""
Pitch shift preserving tempo using ffmpeg filters:
asetrate=sr*2^(semitones/12),aresample=sr,atempo=1/2^(semitones/12)
This approach preserves duration (attempt) but may produce artifacts for large shifts.
"""
# Export, run ffmpeg, reload
if abs(semitones) < 0.001:
return seg
factor = 2 ** (semitones / 12.0)
# Build filter string
# Increase sample rate by factor, then resample back, then adjust tempo by inverse factor
filter_str = f"asetrate=sample_rate*{factor},aresample=sample_rate,atempo={1.0/factor}"
return ffmpeg_apply_filter_on_segment(seg, filter_str)
# ----------------------------
# Логирование job
# ----------------------------
def post_log(job_id: str, item: dict):
with jobs_lock:
if job_id in job_logs:
job_logs[job_id].append(item)
# ----------------------------
# Рабочий поток обработки
# ----------------------------
def processing_worker(job_id: str, input_path: Path, original_filename: str, options: dict):
try:
post_log(job_id, {"type": "progress", "msg": "Получаю файл...", "percent": 2})
# load
try:
with open(input_path, "rb") as f:
class _FS:
def __init__(self, data, filename):
self._data = data
self.filename = filename
def read(self):
return self._data
fs_wrapper = _FS(f.read(), original_filename)
seg = load_audio_from_filestorage(fs_wrapper)
except Exception as e:
post_log(job_id, {"type": "error", "msg": f"Не удалось загрузить аудио: {e}"})
return
post_log(job_id, {"type": "progress", "msg": "Анализ аудио...", "percent": 8})
time.sleep(0.08)
# trim silence
if options.get("trim_silence"):
post_log(job_id, {"type": "progress", "msg": "Обрезаю тишину...", "percent": 18})
try:
seg = apply_trim_silence(seg, silence_thresh_db=int(options.get("sil_thresh", -50)))
except Exception as exc:
post_log(job_id, {"type": "error", "msg": f"Ошибка при обрезке тишины: {exc}"})
return
time.sleep(0.05)
# low/high pass
lp = float(options.get("lowpass", 0) or 0)
hp = float(options.get("highpass", 0) or 0)
if lp > 0:
post_log(job_id, {"type": "progress", "msg": f"Применяю low-pass {int(lp)} Hz...", "percent": 30})
try:
seg = apply_lowpass(seg, lp)
except Exception as exc:
post_log(job_id, {"type": "error", "msg": f"Ошибка low-pass: {exc}"})
return
time.sleep(0.04)
if hp > 0:
post_log(job_id, {"type": "progress", "msg": f"Применяю high-pass {int(hp)} Hz...", "percent": 36})
try:
seg = apply_highpass(seg, hp)
except Exception as exc:
post_log(job_id, {"type": "error", "msg": f"Ошибка high-pass: {exc}"})
return
time.sleep(0.04)
# gain in dB
gain_db = float(options.get("gain_db", 0) or 0)
if abs(gain_db) > 0.001:
post_log(job_id, {"type": "progress", "msg": f"Применяю усиление {gain_db} dB...", "percent": 42})
try:
seg = apply_gain(seg, gain_db)
except Exception as exc:
post_log(job_id, {"type": "error", "msg": f"Ошибка gain: {exc}"})
return
time.sleep(0.03)
# fade in/out
fi = int(options.get("fade_in_ms", 0) or 0)
fo = int(options.get("fade_out_ms", 0) or 0)
if fi > 0 or fo > 0:
post_log(job_id, {"type": "progress", "msg": f"Применяю fade in {fi} ms / fade out {fo} ms...", "percent": 48})
try:
seg = apply_fade(seg, fi, fo)
except Exception as exc:
post_log(job_id, {"type": "error", "msg": f"Ошибка fade: {exc}"})
return
time.sleep(0.03)
# reverse
if options.get("reverse"):
post_log(job_id, {"type": "progress", "msg": "Реверс трека...", "percent": 52})
try:
seg = apply_reverse(seg)
except Exception as exc:
post_log(job_id, {"type": "error", "msg": f"Ошибка reverse: {exc}"})
return
time.sleep(0.02)
# speed change
speed = float(options.get("speed", 1.0) or 1.0)
if abs(speed - 1.0) > 1e-6:
post_log(job_id, {"type": "progress", "msg": f"Изменяю скорость x{speed}...", "percent": 60})
try:
seg = apply_speed_change(seg, speed)
except Exception as exc:
post_log(job_id, {"type": "error", "msg": f"Ошибка изменения скорости: {exc}"})
return
time.sleep(0.03)
# normalize
if options.get("normalize"):
post_log(job_id, {"type": "progress", "msg": "Нормализация громкости...", "percent": 68})
try:
seg = apply_normalize(seg)
except Exception as exc:
post_log(job_id, {"type": "error", "msg": f"Ошибка при нормализации: {exc}"})
return
time.sleep(0.03)
# pitch shift via ffmpeg (preserve tempo)
pitch = float(options.get("pitch_semitones", 0) or 0)
if abs(pitch) > 0.001:
post_log(job_id, {"type": "progress", "msg": f"Сдвиг тона на {pitch} полутонов...", "percent": 75})
try:
seg = apply_pitch_shift_with_ffmpeg(seg, pitch)
except Exception as exc:
post_log(job_id, {"type": "error", "msg": f"Ошибка pitch shift: {exc}"})
return
time.sleep(0.04)
# echo
if options.get("echo"):
delay_ms = float(options.get("echo_delay_ms", 500) or 500)
decay = float(options.get("echo_decay", 0.5) or 0.5)
post_log(job_id, {"type": "progress", "msg": f"Применяю echo: delay {int(delay_ms)} ms, decay {decay}...", "percent": 82})
try:
seg = apply_echo_with_ffmpeg(seg, delay_ms=delay_ms, decay=decay)
except Exception as exc:
post_log(job_id, {"type": "error", "msg": f"Ошибка echo: {exc}"})
return
time.sleep(0.03)
# reverb
if options.get("reverb"):
rv_level = float(options.get("reverb_level", 50) or 50)
post_log(job_id, {"type": "progress", "msg": f"Применяю reverb (level {rv_level})...", "percent": 88})
try:
seg = apply_reverb_with_ffmpeg(seg, reverb_level=rv_level)
except Exception as exc:
post_log(job_id, {"type": "error", "msg": f"Ошибка reverb: {exc}"})
return
time.sleep(0.03)
# export to selected format
out_fmt = secure_extension_for_format(options.get("out_format", "wav"))
out_path = random_filename(out_fmt)
post_log(job_id, {"type": "progress", "msg": "Экспортирую файл...", "percent": 94})
try:
export_kwargs = {}
if out_fmt == "mp3":
export_kwargs["bitrate"] = "192k"
seg.export(out_path.as_posix(), format=out_fmt, **export_kwargs)
except Exception as exc:
post_log(job_id, {"type": "error", "msg": f"Ошибка при экспорте: {exc}"})
return
with jobs_lock:
job_files[job_id] = out_path
post_log(job_id, {"type": "done", "msg": "Готово", "percent": 100, "file": out_path.name})
except Exception as e:
post_log(job_id, {"type": "error", "msg": f"Внутренняя ошибка: {e}"})
# ----------------------------
# Шаблон UI (улучшен контраст)
# ----------------------------
INDEX_HTML = """
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>Music Processor — Pro</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
:root{
--bg:#0b1220;
--card:#0f1724cc;
--muted:#cfe8f5;
--accent:#7dd3fc;
--text:#eaf6ff;
--sub:#9fc4d8;
--panel:#0b1228;
}
body { background: linear-gradient(180deg,var(--bg), #071233); color: var(--text); min-height:100vh; }
.card { border-radius: 12px; background: var(--card); box-shadow: 0 8px 30px rgba(2,6,23,0.6); }
.accent { color: var(--accent); }
.small-muted { color: var(--sub); font-size: 0.95rem; }
.progress { height: 18px; }
.controls label { font-weight: 600; color: var(--text); }
footer { margin-top: 18px; color: var(--sub); }
#log { max-height: 220px; overflow:auto; color: var(--muted); }
.form-control, .form-select { background: rgba(255,255,255,0.03); color: var(--text); border: 1px solid rgba(255,255,255,0.06); }
.form-check-label { color: var(--text); }
.btn-primary { background: linear-gradient(90deg,#0ea5e9, #3b82f6); border: none; }
</style>
</head>
<body>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-xl-9 col-lg-10">
<div class="card p-4">
<h2 class="mb-1 accent">Онлайн обработка музыки — Pro</h2>
<p class="small-muted mb-3">Добавлено много эффектов: gain, fade, reverse, pitch, echo, reverb, фильтры, speed, normalize и др. Результат — в реальном времени через AJAX-поллинг.</p>
<form id="uploadForm" enctype="multipart/form-data" class="mb-3">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Аудиофайл</label>
<input class="form-control" type="file" id="audio_file" name="audio_file" accept="audio/*" required>
</div>
<div class="col-md-6">
<label class="form-label">Формат вывода</label>
<select class="form-select" id="out_format" name="out_format">
<option value="wav" selected>WAV</option>
<option value="mp3">MP3</option>
<option value="flac">FLAC</option>
<option value="ogg">OGG</option>
<option value="m4a">M4A</option>
</select>
</div>
</div>
<hr style="border-color:rgba(255,255,255,0.04)">
<div class="row g-2">
<div class="col-md-3">
<label class="form-label">Скорость (x)</label>
<input class="form-control" type="number" step="0.05" name="speed" id="speed" value="1.0">
</div>
<div class="col-md-3">
<label class="form-label">Gain (dB)</label>
<input class="form-control" type="number" step="0.5" name="gain_db" id="gain_db" value="0">
</div>
<div class="col-md-3">
<label class="form-label">Pitch (полутоны)</label>
<input class="form-control" type="number" step="0.5" name="pitch_semitones" id="pitch_semitones" value="0">
</div>
<div class="col-md-3">
<label class="form-label">Normalize</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="normalize" name="normalize">
<label class="form-check-label" for="normalize">вкл</label>
</div>
</div>
</div>
<div class="row g-2 mt-3">
<div class="col-md-4">
<label class="form-label">Low-pass (Hz)</label>
<input class="form-control" type="number" id="lowpass" name="lowpass" value="0">
</div>
<div class="col-md-4">
<label class="form-label">High-pass (Hz)</label>
<input class="form-control" type="number" id="highpass" name="highpass" value="0">
</div>
<div class="col-md-4">
<label class="form-label">Trim silence</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="trim_silence" name="trim_silence">
<label class="form-check-label" for="trim_silence">вкл</label>
</div>
</div>
</div>
<div class="row g-2 mt-3">
<div class="col-md-3">
<label class="form-label">Fade in (ms)</label>
<input class="form-control" type="number" id="fade_in_ms" name="fade_in_ms" value="0">
</div>
<div class="col-md-3">
<label class="form-label">Fade out (ms)</label>
<input class="form-control" type="number" id="fade_out_ms" name="fade_out_ms" value="0">
</div>
<div class="col-md-3">
<label class="form-label">Reverse</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="reverse" name="reverse">
<label class="form-check-label" for="reverse">вкл</label>
</div>
</div>
<div class="col-md-3">
<label class="form-label">Silence thresh (dB)</label>
<input class="form-control" type="number" id="sil_thresh" name="sil_thresh" value="-50">
</div>
</div>
<hr style="border-color:rgba(255,255,255,0.04)">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label">Echo</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="echo" name="echo">
<label class="form-check-label" for="echo">вкл</label>
</div>
<label class="form-label mt-2">Delay (ms)</label>
<input class="form-control" type="number" id="echo_delay_ms" name="echo_delay_ms" value="500">
<label class="form-label mt-2">Decay (0-1)</label>
<input class="form-control" type="number" step="0.05" id="echo_decay" name="echo_decay" value="0.5">
</div>
<div class="col-md-4">
<label class="form-label">Reverb</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="reverb" name="reverb">
<label class="form-check-label" for="reverb">вкл</label>
</div>
<label class="form-label mt-2">Reverb level (0-100)</label>
<input class="form-control" type="number" id="reverb_level" name="reverb_level" value="40">
</div>
<div class="col-md-4">
<label class="form-label">Other</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enable_all" />
<label class="form-check-label" for="enable_all">Enable quick demo (echo+reverb+norm)</label>
</div>
</div>
</div>
<div class="mt-4 d-flex gap-2">
<button type="submit" id="submitBtn" class="btn btn-primary">Обработать</button>
<button type="button" id="resetBtn" class="btn btn-outline-light">Сброс</button>
</div>
</form>
<div id="statusArea" style="display:none;">
<div class="mb-2 small-muted">Статус обработки:</div>
<div class="mb-2">
<div class="progress">
<div id="progressBar" class="progress-bar bg-info" role="progressbar" style="width: 0%">0%</div>
</div>
</div>
<div id="log" class="small-muted mb-2" style="min-height:64px;"></div>
<div id="resultArea" class="mt-3" style="display:none;">
<h5 class="accent">Готово — прослушать / скачать</h5>
<audio id="player" controls style="width:100%"></audio>
<div class="mt-2">
<a id="downloadLink" class="btn btn-outline-light" href="#" download>Скачать</a>
</div>
</div>
</div>
<hr style="border-color:rgba(255,255,255,0.04)">
<div class="small-muted">Максимальный размер файла: {{max_mb}} МБ. Временные файлы хранятся ~1 час.</div>
</div>
<footer class="text-center">
<small>Made with ♥ — pydub + ffmpeg. Если эффекты (echo/reverb/pitch) не работают — проверь установленность ffmpeg в контейнере.</small>
</footer>
</div>
</div>
</div>
<script>
const maxMB = {{max_mb}};
const uploadForm = document.getElementById('uploadForm');
const statusArea = document.getElementById('statusArea');
const logEl = document.getElementById('log');
const progressBar = document.getElementById('progressBar');
const resultArea = document.getElementById('resultArea');
const player = document.getElementById('player');
const downloadLink = document.getElementById('downloadLink');
const submitBtn = document.getElementById('submitBtn');
const resetBtn = document.getElementById('resetBtn');
const enableAll = document.getElementById('enable_all');
function log(msg) {
const time = new Date().toLocaleTimeString();
logEl.innerHTML = `<div>[${time}] ${msg}</div>` + logEl.innerHTML;
}
let pollInterval = null;
let last_idx = 0;
enableAll.addEventListener('change', function(){
const on = enableAll.checked;
document.getElementById('echo').checked = on;
document.getElementById('reverb').checked = on;
document.getElementById('normalize').checked = on;
});
uploadForm.addEventListener('submit', async function(e) {
e.preventDefault();
logEl.innerHTML = '';
resultArea.style.display = 'none';
statusArea.style.display = 'block';
progressBar.style.width = '0%';
progressBar.textContent = '0%';
last_idx = 0;
const fileInput = document.getElementById('audio_file');
if (!fileInput.files || fileInput.files.length === 0) {
alert('Выберите аудио-файл');
return;
}
const file = fileInput.files[0];
if (file.size > maxMB * 1024 * 1024) {
alert('Файл слишком большой (макс ' + maxMB + ' MB)');
return;
}
submitBtn.disabled = true;
const form = new FormData();
form.append('audio_file', file);
form.append('normalize', document.getElementById('normalize').checked ? '1' : '');
form.append('speed', document.getElementById('speed').value);
form.append('lowpass', document.getElementById('lowpass').value);
form.append('highpass', document.getElementById('highpass').value);
form.append('trim_silence', document.getElementById('trim_silence').checked ? '1' : '');
form.append('sil_thresh', document.getElementById('sil_thresh').value);
form.append('out_format', document.getElementById('out_format').value);
form.append('gain_db', document.getElementById('gain_db').value);
form.append('fade_in_ms', document.getElementById('fade_in_ms').value);
form.append('fade_out_ms', document.getElementById('fade_out_ms').value);
form.append('reverse', document.getElementById('reverse').checked ? '1' : '');
form.append('pitch_semitones', document.getElementById('pitch_semitones').value);
form.append('echo', document.getElementById('echo').checked ? '1' : '');
form.append('echo_delay_ms', document.getElementById('echo_delay_ms').value);
form.append('echo_decay', document.getElementById('echo_decay').value);
form.append('reverb', document.getElementById('reverb').checked ? '1' : '');
form.append('reverb_level', document.getElementById('reverb_level').value);
try {
const resp = await fetch('/start', { method: 'POST', body: form });
if (!resp.ok) {
const txt = await resp.text();
alert('Ошибка: ' + resp.status + '\\n' + txt);
submitBtn.disabled = false;
return;
}
const js = await resp.json();
const job_id = js.job_id;
log('Задача создана: ' + job_id);
// start polling
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(async () => {
try {
const sresp = await fetch('/status/' + job_id + '?last_idx=' + last_idx);
if (!sresp.ok) {
const t = await sresp.text();
log('Ошибка статуса: ' + sresp.status + ' ' + t);
clearInterval(pollInterval);
submitBtn.disabled = false;
return;
}
const data = await sresp.json();
const events = data.events || [];
last_idx = data.next_index || last_idx;
for (const ev of events) {
if (ev.type === 'progress') {
const pct = ev.percent || 0;
progressBar.style.width = pct + '%';
progressBar.textContent = pct + '%';
if (ev.msg) log(ev.msg);
} else if (ev.type === 'done') {
progressBar.style.width = '100%';
progressBar.textContent = '100%';
if (ev.msg) log(ev.msg);
} else if (ev.type === 'error') {
log('Ошибка: ' + (ev.msg || 'unknown'));
alert('Ошибка обработки: ' + (ev.msg || 'неизвестно'));
clearInterval(pollInterval);
submitBtn.disabled = false;
return;
}
if (ev.file) {
const url = window.location.origin + '/download/' + ev.file;
player.src = url;
downloadLink.href = url;
downloadLink.download = 'processed_' + (file.name || 'file.' + (ev.file.split('.').pop() || 'wav'));
resultArea.style.display = 'block';
log('Файл доступен: ' + url);
clearInterval(pollInterval);
submitBtn.disabled = false;
break;
}
if (ev.file_url) {
const url = ev.file_url;
player.src = url;
downloadLink.href = url;
downloadLink.download = 'processed_' + (file.name || 'file');
resultArea.style.display = 'block';
log('Файл доступен: ' + url);
clearInterval(pollInterval);
submitBtn.disabled = false;
break;
}
}
} catch (err) {
console.error('poll err', err);
log('Ошибка запроса статуса: ' + err);
clearInterval(pollInterval);
submitBtn.disabled = false;
}
}, 800);
} catch (err) {
alert('Ошибка при отправке: ' + err);
submitBtn.disabled = false;
}
});
resetBtn.addEventListener('click', function() {
uploadForm.reset();
statusArea.style.display = 'none';
resultArea.style.display = 'none';
logEl.innerHTML = '';
progressBar.style.width = '0%';
progressBar.textContent = '0%';
if (pollInterval) clearInterval(pollInterval);
submitBtn.disabled = false;
});
</script>
</body>
</html>
"""
# ----------------------------
# Маршруты
# ----------------------------
@app.route("/", methods=["GET"])
def index():
cleanup_old_files()
return render_template_string(INDEX_HTML, max_mb=MAX_UPLOAD_MB)
@app.route("/start", methods=["POST"])
def start_job():
cleanup_old_files()
if "audio_file" not in request.files:
return "No file part 'audio_file'", 400
fs = request.files["audio_file"]
if fs.filename == "":
return "No selected file", 400
job_id = str(uuid.uuid4())
with jobs_lock:
job_logs[job_id] = []
options = {
"normalize": bool(request.form.get("normalize")),
"speed": request.form.get("speed", "1.0"),
"lowpass": request.form.get("lowpass", "0"),
"highpass": request.form.get("highpass", "0"),
"trim_silence": bool(request.form.get("trim_silence")),
"sil_thresh": request.form.get("sil_thresh", "-50"),
"out_format": request.form.get("out_format", "wav"),
"gain_db": request.form.get("gain_db", "0"),
"fade_in_ms": request.form.get("fade_in_ms", "0"),
"fade_out_ms": request.form.get("fade_out_ms", "0"),
"reverse": bool(request.form.get("reverse")),
"pitch_semitones": request.form.get("pitch_semitones", "0"),
"echo": bool(request.form.get("echo")),
"echo_delay_ms": request.form.get("echo_delay_ms", "500"),
"echo_decay": request.form.get("echo_decay", "0.5"),
"reverb": bool(request.form.get("reverb")),
"reverb_level": request.form.get("reverb_level", "40"),
}
# Save uploaded file to tmp
temp_in = TMP_DIR / (job_id + "_in")
fs.stream.seek(0)
fs.save(temp_in)
# start background worker
t = threading.Thread(target=processing_worker, args=(job_id, temp_in, fs.filename, options), daemon=True)
t.start()
return jsonify({"job_id": job_id})
@app.route("/status/<job_id>", methods=["GET"])
def status(job_id):
last_idx = int(request.args.get("last_idx", "0") or 0)
with jobs_lock:
logs = job_logs.get(job_id, []).copy()
events = logs[last_idx:]
next_index = last_idx + len(events)
# add file_url based on local download route
for ev in events:
if ev.get("file") and not ev.get("file_url"):
download_path = url_for("download_file", filename=ev["file"], _external=False)
host = request.host_url.rstrip('/')
ev["file_url"] = host + download_path
return jsonify({"events": events, "next_index": next_index})
@app.route("/download/<filename>", methods=["GET"])
def download_file(filename):
cleanup_old_files()
safe_path = (TMP_DIR / filename).resolve()
try:
if not str(safe_path).startswith(str(TMP_DIR.resolve())):
abort(403)
if not safe_path.exists():
abort(404)
return send_file(safe_path.as_posix(), as_attachment=False)
except Exception as e:
return f"Error serving file: {e}", 500
@app.route("/healthz", methods=["GET"])
def health():
return "ok", 200
# ----------------------------
# Запуск
# ----------------------------
if __name__ == "__main__":
port = int(os.environ.get("PORT", 7860))
host = os.environ.get("HOST", "0.0.0.0")
app.run(host=host, port=port, debug=False)