generated from dellevin/template
22
This commit is contained in:
@@ -16,9 +16,11 @@ from .ai_dubbing import bp as ai_dubbing_bp
|
||||
from .rvc import bp as rvc_bp
|
||||
from .audio_slicer import bp as audio_slicer_bp
|
||||
from .uvr_sep import bp as uvr_sep_bp
|
||||
from .mp4_to_audio import bp as mp4_to_audio_bp
|
||||
from .ai_translate import bp as ai_translate_bp
|
||||
|
||||
__all__ = [
|
||||
'pin_tu_bp', 'base64_bp', 'down_video_bp', 'fen_ci_bp', 'content_tag_bp',
|
||||
'chmod_calc_bp', 'json_format_bp', 'qr_code_bp', 'http_status_bp', 'url_parser_bp', 'token_gen_bp',
|
||||
'sovits_tts_bp', 'stt_bp', 'ai_dubbing_bp', 'rvc_bp', 'audio_slicer_bp', 'uvr_sep_bp',
|
||||
'sovits_tts_bp', 'stt_bp', 'ai_dubbing_bp', 'rvc_bp', 'audio_slicer_bp', 'uvr_sep_bp', 'mp4_to_audio_bp', 'ai_translate_bp',
|
||||
]
|
||||
|
||||
621
flask-dev-api/blueprints/ai_translate.py
Normal file
621
flask-dev-api/blueprints/ai_translate.py
Normal file
@@ -0,0 +1,621 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ai-translate AI 翻译蓝图
|
||||
支持 NLLB-200 本地翻译引擎
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import uuid
|
||||
import threading
|
||||
import tempfile
|
||||
from flask import Blueprint, render_template, request, jsonify, send_file
|
||||
|
||||
bp = Blueprint('ai_translate', __name__, url_prefix='/ai-translate')
|
||||
|
||||
try:
|
||||
from config import BASE_DIR
|
||||
except ImportError:
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
CONFIG_PATH = os.path.join(BASE_DIR, 'config', 'ai_translate_config.json')
|
||||
|
||||
# 引擎状态
|
||||
_model = None
|
||||
_model_lock = threading.Lock()
|
||||
_engine_status = {'loaded': False, 'loading': False, 'engine_type': None, 'model_name': None, 'error': None}
|
||||
|
||||
_tasks = {}
|
||||
|
||||
SUBTITLE_EXTS = ('.srt', '.ass', '.ssa', '.vtt')
|
||||
LANGUAGES = {
|
||||
'zh': '中文', 'en': '英文', 'ja': '日文', 'ko': '韩文',
|
||||
'fr': '法文', 'de': '德文', 'es': '西班牙文', 'ru': '俄文',
|
||||
'th': '泰文', 'vi': '越南文', 'it': '意大利文',
|
||||
'pt': '葡萄牙文', 'auto': '自动检测',
|
||||
}
|
||||
|
||||
# NLLB-200 语言代码映射
|
||||
_NLLB_LANG_MAP = {
|
||||
'zh': 'zho_Hans', 'en': 'eng_Latn', 'ja': 'jpn_Jpan', 'ko': 'kor_Hang',
|
||||
'fr': 'fra_Latn', 'de': 'deu_Latn', 'es': 'spa_Latn', 'ru': 'rus_Cyrl',
|
||||
'th': 'tha_Thai', 'vi': 'vie_Latn', 'it': 'ita_Latn',
|
||||
'pt': 'por_Latn',
|
||||
}
|
||||
|
||||
# langdetect 返回值 → 我们的语言代码
|
||||
_DETECT_LANG_MAP = {
|
||||
'zh-cn': 'zh', 'zh-tw': 'zh', 'zh': 'zh',
|
||||
'en': 'en', 'ja': 'ja', 'ko': 'ko',
|
||||
'fr': 'fr', 'de': 'de', 'es': 'es', 'ru': 'ru',
|
||||
'th': 'th', 'vi': 'vi', 'it': 'it', 'pt': 'pt',
|
||||
}
|
||||
|
||||
ENGINE_TYPES = {
|
||||
'nllb': {'name': 'NLLB-200', 'desc': 'HF 目录或模型 ID'},
|
||||
}
|
||||
|
||||
|
||||
def _default_config():
|
||||
return {
|
||||
'engine_type': 'nllb',
|
||||
'nllb_dir': '',
|
||||
'max_new_tokens': 512,
|
||||
'batch_size': 8,
|
||||
'last_nllb_model': '',
|
||||
'last_src_lang': 'en',
|
||||
'last_tgt_lang': 'zh',
|
||||
}
|
||||
|
||||
|
||||
def _load_config():
|
||||
cfg = _default_config()
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
try:
|
||||
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
|
||||
cfg.update(json.load(f))
|
||||
except Exception:
|
||||
pass
|
||||
return cfg
|
||||
|
||||
|
||||
def _save_config(cfg):
|
||||
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
||||
with open(CONFIG_PATH, 'w', encoding='utf-8') as f:
|
||||
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
|
||||
# ==================== 字幕解析 ====================
|
||||
|
||||
def _parse_srt(text):
|
||||
blocks = re.split(r'\n\s*\n', text.strip())
|
||||
entries = []
|
||||
for block in blocks:
|
||||
lines = block.strip().split('\n')
|
||||
if len(lines) < 3:
|
||||
continue
|
||||
try:
|
||||
idx = int(lines[0].strip())
|
||||
except ValueError:
|
||||
continue
|
||||
timecode = lines[1].strip()
|
||||
content = '\n'.join(lines[2:]).strip()
|
||||
if content:
|
||||
entries.append((idx, timecode, content))
|
||||
return entries
|
||||
|
||||
|
||||
def _build_srt(entries):
|
||||
parts = []
|
||||
for idx, tc, content in entries:
|
||||
parts.append(f'{idx}\n{tc}\n{content}')
|
||||
return '\n\n'.join(parts) + '\n'
|
||||
|
||||
|
||||
def _parse_vtt(text):
|
||||
text = re.sub(r'^WEBVTT\s*\n', '', text.strip(), count=1)
|
||||
blocks = re.split(r'\n\s*\n', text.strip())
|
||||
entries = []
|
||||
for i, block in enumerate(blocks):
|
||||
lines = block.strip().split('\n')
|
||||
if len(lines) < 2:
|
||||
continue
|
||||
timecode = None
|
||||
content_lines = []
|
||||
for line in lines:
|
||||
if '-->' in line:
|
||||
timecode = line.strip()
|
||||
elif timecode is not None:
|
||||
content_lines.append(line)
|
||||
else:
|
||||
if '-->' not in line:
|
||||
continue
|
||||
if timecode and content_lines:
|
||||
entries.append((i + 1, timecode, '\n'.join(content_lines).strip()))
|
||||
return entries
|
||||
|
||||
|
||||
def _build_vtt(entries):
|
||||
parts = ['WEBVTT\n']
|
||||
for idx, tc, content in entries:
|
||||
parts.append(f'{idx}\n{tc}\n{content}')
|
||||
return '\n\n'.join(parts) + '\n'
|
||||
|
||||
|
||||
def _parse_ass(text):
|
||||
entries = []
|
||||
for line in text.split('\n'):
|
||||
line = line.strip()
|
||||
if line.lower().startswith('dialogue:'):
|
||||
entries.append(line)
|
||||
return entries
|
||||
|
||||
|
||||
def _ass_extract_text(dialogue_line):
|
||||
parts = dialogue_line.split(',', 9)
|
||||
if len(parts) < 10:
|
||||
return dialogue_line, ''
|
||||
text = parts[9]
|
||||
text = re.sub(r'\{[^}]*\}', '', text)
|
||||
text = text.replace('\\N', '\n').replace('\\n', '\n')
|
||||
return dialogue_line, text.strip()
|
||||
|
||||
|
||||
def _ass_replace_text(dialogue_line, new_text):
|
||||
parts = dialogue_line.split(',', 9)
|
||||
if len(parts) < 10:
|
||||
return dialogue_line
|
||||
translated = new_text.replace('\n', '\\N')
|
||||
parts[9] = translated
|
||||
return ','.join(parts)
|
||||
|
||||
|
||||
def _build_ass(original_lines, translated_texts):
|
||||
result = []
|
||||
trans_idx = 0
|
||||
for line in original_lines:
|
||||
if line.lower().startswith('dialogue:'):
|
||||
if trans_idx < len(translated_texts):
|
||||
result.append(_ass_replace_text(line, translated_texts[trans_idx]))
|
||||
trans_idx += 1
|
||||
else:
|
||||
result.append(line)
|
||||
else:
|
||||
result.append(line)
|
||||
return '\n'.join(result)
|
||||
|
||||
|
||||
def _detect_subtitle_format(filename, content):
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext == '.srt':
|
||||
return 'srt'
|
||||
if ext == '.vtt':
|
||||
return 'vtt'
|
||||
if ext in ('.ass', '.ssa'):
|
||||
return 'ass'
|
||||
if 'WEBVTT' in content[:20]:
|
||||
return 'vtt'
|
||||
if 'Dialogue:' in content:
|
||||
return 'ass'
|
||||
return 'srt'
|
||||
|
||||
|
||||
# ==================== 模型加载与翻译 ====================
|
||||
|
||||
def _load_model(model_path, model_name):
|
||||
global _model
|
||||
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
|
||||
import torch
|
||||
|
||||
_engine_status['loading'] = True
|
||||
_engine_status['error'] = None
|
||||
try:
|
||||
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
||||
tokenizer = AutoTokenizer.from_pretrained(model_path, clean_up_tokenization_spaces=True)
|
||||
model = AutoModelForSeq2SeqLM.from_pretrained(model_path)
|
||||
model = model.to(device)
|
||||
model.eval()
|
||||
_model = {'model': model, 'tokenizer': tokenizer, 'device': device}
|
||||
|
||||
_engine_status['loaded'] = True
|
||||
_engine_status['model_name'] = model_name
|
||||
_engine_status['engine_type'] = 'nllb'
|
||||
_engine_status['error'] = None
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
_engine_status['error'] = str(e)
|
||||
return False, str(e)
|
||||
finally:
|
||||
_engine_status['loading'] = False
|
||||
|
||||
|
||||
def _unload_model():
|
||||
global _model
|
||||
_model = None
|
||||
_engine_status['loaded'] = False
|
||||
_engine_status['model_name'] = None
|
||||
_engine_status['engine_type'] = None
|
||||
import gc
|
||||
gc.collect()
|
||||
try:
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _detect_lang(text):
|
||||
"""用 langdetect 检测语言,返回我们的语言代码(如 'zh'、'en')"""
|
||||
try:
|
||||
from langdetect import detect
|
||||
code = detect(text)
|
||||
return _DETECT_LANG_MAP.get(code, _DETECT_LANG_MAP.get(code.split('-')[0], 'en'))
|
||||
except Exception:
|
||||
return 'en'
|
||||
|
||||
|
||||
def _do_translate(texts, src_lang, tgt_lang, cfg):
|
||||
if not _model:
|
||||
return None, '模型未加载'
|
||||
try:
|
||||
import torch
|
||||
model = _model['model']
|
||||
tokenizer = _model['tokenizer']
|
||||
device = _model['device']
|
||||
|
||||
nllb_tgt = _NLLB_LANG_MAP.get(tgt_lang, _NLLB_LANG_MAP.get('zh', 'zho_Hans'))
|
||||
tgt_token_id = tokenizer.convert_tokens_to_ids(nllb_tgt)
|
||||
|
||||
# 自动检测时,用第一条文本检测语言
|
||||
if src_lang == 'auto':
|
||||
sample = ' '.join(texts[:3])
|
||||
detected = _detect_lang(sample)
|
||||
nllb_src = _NLLB_LANG_MAP.get(detected, 'eng_Latn')
|
||||
else:
|
||||
nllb_src = _NLLB_LANG_MAP.get(src_lang, 'eng_Latn')
|
||||
|
||||
results = []
|
||||
for text in texts:
|
||||
tokenizer.src_lang = nllb_src
|
||||
inputs = tokenizer(text, return_tensors='pt', padding=True, truncation=True, max_length=512).to(device)
|
||||
with torch.no_grad():
|
||||
translated = model.generate(
|
||||
**inputs,
|
||||
forced_bos_token_id=tgt_token_id,
|
||||
max_new_tokens=cfg.get('max_new_tokens', 512)
|
||||
)
|
||||
result = tokenizer.decode(translated[0], skip_special_tokens=True)
|
||||
results.append(result)
|
||||
return results, None
|
||||
except Exception as e:
|
||||
return None, str(e)
|
||||
|
||||
|
||||
def _ensure_model(src_lang=None, tgt_lang=None):
|
||||
if _engine_status['loaded'] and _model:
|
||||
return True, None
|
||||
cfg = _load_config()
|
||||
models_dir = cfg.get('nllb_dir', '')
|
||||
if not models_dir or not os.path.isdir(models_dir):
|
||||
return False, '请在设置中配置 NLLB 模型目录'
|
||||
|
||||
# 用 last 模型或第一个可用
|
||||
model_name = cfg.get('last_nllb_model', '')
|
||||
if model_name:
|
||||
full = os.path.join(models_dir, model_name)
|
||||
if not (os.path.isdir(full) and os.path.exists(os.path.join(full, 'config.json'))):
|
||||
model_name = ''
|
||||
if not model_name:
|
||||
for name in sorted(os.listdir(models_dir)):
|
||||
full = os.path.join(models_dir, name)
|
||||
if os.path.isdir(full) and os.path.exists(os.path.join(full, 'config.json')):
|
||||
model_name = name
|
||||
break
|
||||
if not model_name:
|
||||
return False, '未找到 NLLB 模型,请在设置中配置目录'
|
||||
|
||||
model_path = os.path.join(models_dir, model_name)
|
||||
with _model_lock:
|
||||
if _engine_status['loaded'] and _model:
|
||||
return True, None
|
||||
ok, load_err = _load_model(model_path, model_name)
|
||||
if ok:
|
||||
cfg['last_nllb_model'] = model_name
|
||||
_save_config(cfg)
|
||||
return ok, load_err
|
||||
|
||||
|
||||
# ==================== 启动时自动加载模型 ====================
|
||||
|
||||
_auto_load_done = False
|
||||
|
||||
|
||||
def _trigger_auto_load():
|
||||
global _auto_load_done
|
||||
if _auto_load_done:
|
||||
return
|
||||
_auto_load_done = True
|
||||
|
||||
threading.Thread(target=_ensure_model, daemon=True).start()
|
||||
|
||||
|
||||
# ==================== 路由 ====================
|
||||
|
||||
@bp.route('/')
|
||||
def page():
|
||||
_trigger_auto_load()
|
||||
cfg = _load_config()
|
||||
return render_template('ai_translate.html', config=cfg, languages=LANGUAGES, engine_types=ENGINE_TYPES)
|
||||
|
||||
|
||||
@bp.route('/model-status')
|
||||
def model_status():
|
||||
return jsonify({
|
||||
'loaded': _engine_status['loaded'],
|
||||
'loading': _engine_status['loading'],
|
||||
'model_name': _engine_status.get('model_name'),
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/config', methods=['GET'])
|
||||
def get_config():
|
||||
return jsonify(_load_config())
|
||||
|
||||
|
||||
@bp.route('/config', methods=['POST'])
|
||||
def save_config():
|
||||
data = request.get_json()
|
||||
cfg = _load_config()
|
||||
for key in ('nllb_dir', 'last_src_lang', 'last_tgt_lang', 'max_new_tokens', 'batch_size'):
|
||||
if key in data:
|
||||
cfg[key] = data[key]
|
||||
_save_config(cfg)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@bp.route('/models')
|
||||
def list_models():
|
||||
cfg = _load_config()
|
||||
models_dir = cfg.get('nllb_dir', '')
|
||||
models = []
|
||||
if models_dir and os.path.isdir(models_dir):
|
||||
for name in sorted(os.listdir(models_dir)):
|
||||
full = os.path.join(models_dir, name)
|
||||
if os.path.isdir(full) and os.path.exists(os.path.join(full, 'config.json')):
|
||||
models.append({'name': name, 'type': 'huggingface', 'path': full})
|
||||
return jsonify({'models': models, 'models_dir': models_dir, 'last_model': cfg.get('last_nllb_model', '')})
|
||||
|
||||
|
||||
@bp.route('/languages')
|
||||
def list_languages():
|
||||
langs = [{'code': c, 'name': n} for c, n in LANGUAGES.items() if c != 'auto']
|
||||
return jsonify({'src_langs': [{'code': 'auto', 'name': '自动检测'}] + langs, 'tgt_langs': langs})
|
||||
|
||||
|
||||
@bp.route('/unload-model', methods=['POST'])
|
||||
def unload_model():
|
||||
with _model_lock:
|
||||
_unload_model()
|
||||
cfg = _load_config()
|
||||
cfg['last_nllb_model'] = ''
|
||||
_save_config(cfg)
|
||||
return jsonify({'success': True, 'message': '模型已卸载'})
|
||||
|
||||
|
||||
@bp.route('/load-model', methods=['POST'])
|
||||
def load_model():
|
||||
data = request.get_json() or {}
|
||||
model_name = data.get('model_name', '').strip()
|
||||
if not model_name:
|
||||
return jsonify({'success': False, 'error': '请指定模型名称'}), 400
|
||||
|
||||
cfg = _load_config()
|
||||
models_dir = cfg.get('nllb_dir', '')
|
||||
if not models_dir or not os.path.isdir(models_dir):
|
||||
return jsonify({'success': False, 'error': '请先配置 NLLB 模型目录'}), 400
|
||||
|
||||
model_path = os.path.join(models_dir, model_name)
|
||||
if not os.path.isdir(model_path):
|
||||
return jsonify({'success': False, 'error': f'模型目录不存在: {model_name}'}), 400
|
||||
|
||||
with _model_lock:
|
||||
_unload_model()
|
||||
ok, err = _load_model(model_path, model_name)
|
||||
if ok:
|
||||
cfg['last_nllb_model'] = model_name
|
||||
_save_config(cfg)
|
||||
return jsonify({'success': True, 'model_name': model_name})
|
||||
return jsonify({'success': False, 'error': err or '加载失败'}), 400
|
||||
|
||||
|
||||
@bp.route('/translate-text', methods=['POST'])
|
||||
def translate_text():
|
||||
data = request.get_json()
|
||||
text = (data.get('text') or '').strip()
|
||||
if not text:
|
||||
return jsonify({'success': False, 'error': '请输入要翻译的文本'}), 400
|
||||
|
||||
src_lang = data.get('src_lang', 'en')
|
||||
tgt_lang = data.get('tgt_lang', 'zh')
|
||||
cfg = _load_config()
|
||||
batch_size = cfg.get('batch_size', 8)
|
||||
|
||||
cfg['last_src_lang'] = src_lang
|
||||
cfg['last_tgt_lang'] = tgt_lang
|
||||
_save_config(cfg)
|
||||
|
||||
ok, err = _ensure_model(src_lang, tgt_lang)
|
||||
if not ok:
|
||||
return jsonify({'success': False, 'error': err}), 400
|
||||
|
||||
task_id = uuid.uuid4().hex
|
||||
_tasks[task_id] = {'status': 'translating', 'progress': 0, 'result': None, 'error': None}
|
||||
|
||||
def _do_task():
|
||||
try:
|
||||
paragraphs = [p.strip() for p in text.split('\n') if p.strip()]
|
||||
if not paragraphs:
|
||||
paragraphs = [text]
|
||||
|
||||
all_results = []
|
||||
total = len(paragraphs)
|
||||
for i in range(0, total, batch_size):
|
||||
batch = paragraphs[i:i + batch_size]
|
||||
results, err = _do_translate(batch, src_lang, tgt_lang, cfg)
|
||||
if err:
|
||||
_tasks[task_id] = {'status': 'error', 'error': err}
|
||||
return
|
||||
all_results.extend(results)
|
||||
_tasks[task_id]['progress'] = round((i + len(batch)) / total * 100)
|
||||
|
||||
_tasks[task_id] = {'status': 'done', 'result': '\n'.join(all_results), 'progress': 100}
|
||||
|
||||
except Exception as e:
|
||||
_tasks[task_id] = {'status': 'error', 'error': str(e)}
|
||||
|
||||
threading.Thread(target=_do_task, daemon=True).start()
|
||||
return jsonify({'success': True, 'task_id': task_id})
|
||||
|
||||
|
||||
@bp.route('/translate-file', methods=['POST'])
|
||||
def translate_file():
|
||||
if 'subtitle' not in request.files:
|
||||
return jsonify({'success': False, 'error': '请上传字幕文件'}), 400
|
||||
f = request.files['subtitle']
|
||||
if not f.filename:
|
||||
return jsonify({'success': False, 'error': '请上传字幕文件'}), 400
|
||||
|
||||
ext = os.path.splitext(f.filename)[1].lower()
|
||||
if ext not in SUBTITLE_EXTS:
|
||||
return jsonify({'success': False, 'error': f'不支持的格式: {ext},支持 SRT/ASS/VTT'}), 400
|
||||
|
||||
src_lang = request.form.get('src_lang', 'en')
|
||||
tgt_lang = request.form.get('tgt_lang', 'zh')
|
||||
cfg = _load_config()
|
||||
batch_size = cfg.get('batch_size', 8)
|
||||
|
||||
cfg['last_src_lang'] = src_lang
|
||||
cfg['last_tgt_lang'] = tgt_lang
|
||||
_save_config(cfg)
|
||||
|
||||
ok, err = _ensure_model(src_lang, tgt_lang)
|
||||
if not ok:
|
||||
return jsonify({'success': False, 'error': err}), 400
|
||||
|
||||
content = f.read().decode('utf-8', errors='replace')
|
||||
fmt = _detect_subtitle_format(f.filename, content)
|
||||
|
||||
task_id = uuid.uuid4().hex
|
||||
_tasks[task_id] = {'status': 'translating', 'progress': 0, 'result': None, 'error': None,
|
||||
'filename': f.filename, 'format': fmt}
|
||||
|
||||
def _do_task():
|
||||
try:
|
||||
if fmt == 'srt':
|
||||
entries = _parse_srt(content)
|
||||
texts = [e[2] for e in entries]
|
||||
elif fmt == 'vtt':
|
||||
entries = _parse_vtt(content)
|
||||
texts = [e[2] for e in entries]
|
||||
elif fmt == 'ass':
|
||||
full_lines = content.split('\n')
|
||||
dialogue_lines = [l for l in full_lines if l.strip().lower().startswith('dialogue:')]
|
||||
texts = []
|
||||
for dl in dialogue_lines:
|
||||
_, txt = _ass_extract_text(dl)
|
||||
texts.append(txt if txt else '')
|
||||
entries = dialogue_lines
|
||||
else:
|
||||
_tasks[task_id] = {'status': 'error', 'error': '未知字幕格式'}
|
||||
return
|
||||
|
||||
if not texts:
|
||||
_tasks[task_id] = {'status': 'error', 'error': '字幕文件中未找到可翻译的文本'}
|
||||
return
|
||||
|
||||
all_results = []
|
||||
total = len(texts)
|
||||
|
||||
for i in range(0, total, batch_size):
|
||||
batch = texts[i:i + batch_size]
|
||||
batch = [t for t in batch if t.strip()]
|
||||
if not batch:
|
||||
all_results.extend(texts[i:i + batch_size])
|
||||
_tasks[task_id]['progress'] = round((i + batch_size) / total * 100)
|
||||
continue
|
||||
|
||||
results, err = _do_translate(batch, src_lang, tgt_lang, cfg)
|
||||
if err:
|
||||
_tasks[task_id] = {'status': 'error', 'error': err}
|
||||
return
|
||||
|
||||
ri = 0
|
||||
for j in range(i, min(i + batch_size, total)):
|
||||
if texts[j].strip():
|
||||
all_results.append(results[ri] if ri < len(results) else texts[j])
|
||||
ri += 1
|
||||
else:
|
||||
all_results.append('')
|
||||
_tasks[task_id]['progress'] = round(min(i + batch_size, total) / total * 100)
|
||||
|
||||
if fmt == 'srt':
|
||||
translated_entries = [(entries[i][0], entries[i][1], all_results[i]) for i in range(len(entries))]
|
||||
output = _build_srt(translated_entries)
|
||||
elif fmt == 'vtt':
|
||||
translated_entries = [(entries[i][0], entries[i][1], all_results[i]) for i in range(len(entries))]
|
||||
output = _build_vtt(translated_entries)
|
||||
elif fmt == 'ass':
|
||||
output = _build_ass(content.split('\n'), all_results)
|
||||
else:
|
||||
output = '\n'.join(all_results)
|
||||
|
||||
out_path = os.path.join(tempfile.gettempdir(), f'ai_trans_{task_id}{ext}')
|
||||
with open(out_path, 'w', encoding='utf-8') as fout:
|
||||
fout.write(output)
|
||||
|
||||
_tasks[task_id] = {
|
||||
'status': 'done', 'progress': 100,
|
||||
'result': output, 'file_path': out_path,
|
||||
'filename': f.filename, 'format': fmt,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
_tasks[task_id] = {'status': 'error', 'error': str(e)}
|
||||
|
||||
threading.Thread(target=_do_task, daemon=True).start()
|
||||
return jsonify({'success': True, 'task_id': task_id})
|
||||
|
||||
|
||||
@bp.route('/task-status/<task_id>')
|
||||
def task_status(task_id):
|
||||
task = _tasks.get(task_id)
|
||||
if not task:
|
||||
return jsonify({'success': False, 'error': '任务不存在'}), 404
|
||||
return jsonify({'success': True, **task})
|
||||
|
||||
|
||||
@bp.route('/download/<task_id>')
|
||||
def download(task_id):
|
||||
task = _tasks.get(task_id)
|
||||
if not task or task.get('status') != 'done':
|
||||
return jsonify({'error': '文件不存在'}), 404
|
||||
file_path = task.get('file_path')
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
return jsonify({'error': '文件不存在'}), 404
|
||||
orig = task.get('filename', 'translated')
|
||||
base, ext = os.path.splitext(orig)
|
||||
return send_file(file_path, as_attachment=True, download_name=f'{base}_translated{ext}')
|
||||
|
||||
|
||||
@bp.route('/cleanup/<task_id>', methods=['POST'])
|
||||
def cleanup(task_id):
|
||||
task = _tasks.pop(task_id, None)
|
||||
if task and task.get('file_path'):
|
||||
try:
|
||||
os.remove(task['file_path'])
|
||||
except OSError:
|
||||
pass
|
||||
return jsonify({'success': True})
|
||||
@@ -156,6 +156,93 @@ def analyze():
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/manual-slice', methods=['POST'])
|
||||
def manual_slice():
|
||||
data = request.get_json()
|
||||
task_id = data.get('task_id')
|
||||
task = _tasks.get(task_id)
|
||||
if not task:
|
||||
return jsonify({'success': False, 'error': '任务不存在'}), 404
|
||||
|
||||
cut_points = sorted(data.get('cut_points', []))
|
||||
sr = task['sr']
|
||||
total = task['total']
|
||||
duration = task['duration']
|
||||
|
||||
valid = [p for p in cut_points if 0 < p < duration]
|
||||
if not valid:
|
||||
return jsonify({'success': False, 'error': '没有有效的切割点'}), 400
|
||||
|
||||
samples = [int(round(p * sr)) for p in valid]
|
||||
boundaries = [0] + samples + [total]
|
||||
ranges = [(boundaries[i], boundaries[i + 1])
|
||||
for i in range(len(boundaries) - 1)
|
||||
if boundaries[i + 1] > boundaries[i]]
|
||||
|
||||
task['ranges'] = ranges
|
||||
task['status'] = 'analyzed'
|
||||
|
||||
preview = [{'index': i, 'duration': round((e - b) / sr, 2), 'samples': e - b}
|
||||
for i, (b, e) in enumerate(ranges)]
|
||||
return jsonify({'success': True, 'count': len(ranges), 'preview': preview,
|
||||
'sample_rate': int(sr), 'channels': int(task['ch'])})
|
||||
|
||||
|
||||
@bp.route('/preview-range/<task_id>')
|
||||
def preview_range(task_id):
|
||||
task = _tasks.get(task_id)
|
||||
if not task:
|
||||
return jsonify({'error': '任务不存在'}), 404
|
||||
try:
|
||||
start = float(request.args.get('start', 0))
|
||||
end = float(request.args.get('end', task['duration']))
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({'error': '参数错误'}), 400
|
||||
|
||||
sr = task['sr']
|
||||
ch = task['ch']
|
||||
begin = max(0, int(start * sr))
|
||||
end_sample = min(task['total'], int(end * sr))
|
||||
if end_sample <= begin:
|
||||
return jsonify({'error': '无效范围'}), 400
|
||||
|
||||
buf = io.BytesIO()
|
||||
with soundfile.SoundFile(task['src']) as src:
|
||||
src.seek(begin)
|
||||
frames = end_sample - begin
|
||||
data = src.read(frames)
|
||||
with soundfile.SoundFile(buf, mode='w', samplerate=sr, channels=ch,
|
||||
format='WAV') as dst:
|
||||
dst.write(data)
|
||||
buf.seek(0)
|
||||
return send_file(buf, mimetype='audio/wav')
|
||||
|
||||
|
||||
@bp.route('/waveform-peaks/<task_id>')
|
||||
def waveform_peaks(task_id):
|
||||
"""返回波形峰值数据,供前端绘制波形图"""
|
||||
task = _tasks.get(task_id)
|
||||
if not task:
|
||||
return jsonify({'error': '任务不存在'}), 404
|
||||
num_samples = int(request.args.get('samples', 400))
|
||||
with soundfile.SoundFile(task['src']) as f:
|
||||
sr = f.samplerate
|
||||
ch = f.channels
|
||||
total = len(f)
|
||||
samples_per_bucket = max(1, total // num_samples)
|
||||
peaks = []
|
||||
for i in range(num_samples):
|
||||
start = i * samples_per_bucket
|
||||
length = min(samples_per_bucket, total - start)
|
||||
if length <= 0:
|
||||
break
|
||||
data = f.read(length)
|
||||
if ch > 1:
|
||||
data = data.mean(axis=1)
|
||||
peaks.append(float(abs(data).max()))
|
||||
return jsonify({'peaks': peaks, 'duration': task['duration'], 'sr': sr})
|
||||
|
||||
|
||||
@bp.route('/slice', methods=['POST'])
|
||||
def start_slice():
|
||||
data = request.get_json()
|
||||
@@ -166,6 +253,16 @@ def start_slice():
|
||||
if not task.get('ranges'):
|
||||
return jsonify({'success': False, 'error': '请先分析'}), 400
|
||||
|
||||
# 支持选择性切割
|
||||
selected = data.get('selected_indices')
|
||||
all_ranges = task['ranges']
|
||||
if selected is not None and isinstance(selected, list) and len(selected) > 0:
|
||||
ranges = [all_ranges[i] for i in selected if 0 <= i < len(all_ranges)]
|
||||
else:
|
||||
ranges = all_ranges
|
||||
if not ranges:
|
||||
return jsonify({'success': False, 'error': '未选择任何片段'}), 400
|
||||
|
||||
task['status'] = 'slicing'
|
||||
task['progress'] = 0
|
||||
task['slices'] = []
|
||||
@@ -176,7 +273,6 @@ def start_slice():
|
||||
|
||||
def _do_slice():
|
||||
base = os.path.splitext(task['orig_name'])[0]
|
||||
ranges = task['ranges']
|
||||
total = len(ranges)
|
||||
for i, (begin, end) in enumerate(ranges):
|
||||
out_path = os.path.join(out_dir, f'{base}_{i:03d}.wav')
|
||||
|
||||
231
flask-dev-api/blueprints/mp4_to_audio.py
Normal file
231
flask-dev-api/blueprints/mp4_to_audio.py
Normal file
@@ -0,0 +1,231 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
mp4-to-audio MP4转音频蓝图
|
||||
基于 ffmpeg 提取视频中的音频流
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import uuid
|
||||
import subprocess
|
||||
import threading
|
||||
import tempfile
|
||||
from flask import Blueprint, render_template, request, jsonify, send_file, after_this_request
|
||||
|
||||
try:
|
||||
from config import BASE_DIR
|
||||
except ImportError:
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
bp = Blueprint('mp4_to_audio', __name__, url_prefix='/mp4-to-audio')
|
||||
|
||||
VIDEO_EXTS = ('.mp4', '.mkv', '.avi', '.webm', '.mov', '.flv', '.wmv')
|
||||
CONFIG_PATH = os.path.join(BASE_DIR, 'config', 'mp4_to_audio_config.json')
|
||||
|
||||
DEFAULT_PARAMS = {
|
||||
'output_format': 'mp3',
|
||||
'mp3_bitrate': '320k',
|
||||
'wav_sample_rate': 'original',
|
||||
}
|
||||
|
||||
_tasks = {}
|
||||
|
||||
|
||||
def _load_config():
|
||||
cfg = dict(DEFAULT_PARAMS)
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
try:
|
||||
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
|
||||
cfg.update(json.load(f))
|
||||
except Exception:
|
||||
pass
|
||||
return cfg
|
||||
|
||||
|
||||
def _save_config(cfg):
|
||||
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
||||
with open(CONFIG_PATH, 'w', encoding='utf-8') as f:
|
||||
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def _check_ffmpeg():
|
||||
try:
|
||||
r = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True, timeout=5)
|
||||
return r.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _get_duration(path):
|
||||
try:
|
||||
cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration',
|
||||
'-of', 'default=noprint_wrappers=1:nokey=1', path]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||
return float(r.stdout.strip())
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
|
||||
def _do_convert(task_id):
|
||||
task = _tasks[task_id]
|
||||
src = task['src']
|
||||
output_format = task['output_format']
|
||||
out_ext = '.mp3' if output_format == 'mp3' else '.wav'
|
||||
base = os.path.splitext(task['orig_name'])[0]
|
||||
out_filename = f'{base}{out_ext}'
|
||||
out_path = os.path.join(tempfile.gettempdir(), f'mp4audio_{task_id}{out_ext}')
|
||||
|
||||
cmd = ['ffmpeg', '-y', '-i', src]
|
||||
if output_format == 'mp3':
|
||||
cmd += ['-vn', '-acodec', 'libmp3lame', '-b:a', task['mp3_bitrate']]
|
||||
else:
|
||||
cmd += ['-vn', '-acodec', 'pcm_s16le']
|
||||
if task['wav_sample_rate'] != 'original':
|
||||
cmd += ['-ar', task['wav_sample_rate']]
|
||||
cmd.append(out_path)
|
||||
|
||||
duration = task['duration']
|
||||
try:
|
||||
proc = subprocess.Popen(cmd, stderr=subprocess.PIPE, universal_newlines=True)
|
||||
for line in proc.stderr:
|
||||
m = re.search(r'time=(\d+):(\d+):(\d+\.?\d*)', line)
|
||||
if m and duration > 0:
|
||||
cur = int(m.group(1)) * 3600 + int(m.group(2)) * 60 + float(m.group(3))
|
||||
task['progress'] = min(99, int(cur / duration * 100))
|
||||
proc.wait()
|
||||
if proc.returncode != 0:
|
||||
task['status'] = 'error'
|
||||
task['error'] = 'ffmpeg 转换失败'
|
||||
return
|
||||
except Exception as e:
|
||||
task['status'] = 'error'
|
||||
task['error'] = str(e)
|
||||
return
|
||||
|
||||
task['output_path'] = out_path
|
||||
task['filename'] = out_filename
|
||||
task['status'] = 'done'
|
||||
task['progress'] = 100
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
def page():
|
||||
cfg = _load_config()
|
||||
ffmpeg_ok = _check_ffmpeg()
|
||||
return render_template('mp4_to_audio.html', config=cfg, ffmpeg_ok=ffmpeg_ok)
|
||||
|
||||
|
||||
@bp.route('/config', methods=['GET'])
|
||||
def get_config():
|
||||
return jsonify(_load_config())
|
||||
|
||||
|
||||
@bp.route('/config', methods=['POST'])
|
||||
def save_config():
|
||||
data = request.get_json()
|
||||
cfg = _load_config()
|
||||
for key in DEFAULT_PARAMS:
|
||||
if key in data:
|
||||
cfg[key] = data[key]
|
||||
_save_config(cfg)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@bp.route('/upload', methods=['POST'])
|
||||
def upload():
|
||||
if 'video' not in request.files:
|
||||
return jsonify({'success': False, 'error': '请上传视频文件'}), 400
|
||||
f = request.files['video']
|
||||
if not f.filename:
|
||||
return jsonify({'success': False, 'error': '请上传视频文件'}), 400
|
||||
ext = os.path.splitext(f.filename)[1].lower()
|
||||
if ext not in VIDEO_EXTS:
|
||||
return jsonify({'success': False, 'error': f'不支持的格式: {ext}'}), 400
|
||||
|
||||
task_id = uuid.uuid4().hex
|
||||
save_path = os.path.join(tempfile.gettempdir(), f'mp4audio_{task_id}{ext}')
|
||||
f.save(save_path)
|
||||
|
||||
duration = _get_duration(save_path)
|
||||
size = os.path.getsize(save_path)
|
||||
|
||||
_tasks[task_id] = {
|
||||
'src': save_path, 'orig_name': f.filename,
|
||||
'output_path': None, 'output_format': 'mp3',
|
||||
'duration': duration, 'size': size,
|
||||
'status': 'uploaded', 'progress': 0, 'error': None,
|
||||
'filename': None, 'mp3_bitrate': '320k', 'wav_sample_rate': 'original',
|
||||
}
|
||||
return jsonify({
|
||||
'success': True, 'task_id': task_id,
|
||||
'filename': f.filename, 'duration': round(duration, 2), 'size': size,
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/convert', methods=['POST'])
|
||||
def start_convert():
|
||||
data = request.get_json()
|
||||
task_id = data.get('task_id')
|
||||
task = _tasks.get(task_id)
|
||||
if not task:
|
||||
return jsonify({'success': False, 'error': '任务不存在'}), 404
|
||||
|
||||
task['output_format'] = data.get('output_format', 'mp3')
|
||||
task['mp3_bitrate'] = data.get('mp3_bitrate', '320k')
|
||||
task['wav_sample_rate'] = data.get('wav_sample_rate', 'original')
|
||||
task['status'] = 'converting'
|
||||
task['progress'] = 0
|
||||
task['error'] = None
|
||||
|
||||
threading.Thread(target=_do_convert, args=(task_id,), daemon=True).start()
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@bp.route('/status/<task_id>')
|
||||
def status(task_id):
|
||||
task = _tasks.get(task_id)
|
||||
if not task:
|
||||
return jsonify({'success': False, 'error': '任务不存在'}), 404
|
||||
resp = {
|
||||
'success': True, 'status': task['status'], 'progress': task['progress'],
|
||||
}
|
||||
if task['status'] == 'done':
|
||||
resp['filename'] = task['filename']
|
||||
resp['output_format'] = task['output_format']
|
||||
elif task['status'] == 'error':
|
||||
resp['error'] = task.get('error', '未知错误')
|
||||
return jsonify(resp)
|
||||
|
||||
|
||||
@bp.route('/download/<task_id>')
|
||||
def download(task_id):
|
||||
task = _tasks.get(task_id)
|
||||
if not task or not task.get('output_path') or not os.path.exists(task['output_path']):
|
||||
return jsonify({'error': '文件不存在'}), 404
|
||||
mime = 'audio/mpeg' if task['output_format'] == 'mp3' else 'audio/wav'
|
||||
return send_file(task['output_path'], mimetype=mime, as_attachment=True,
|
||||
download_name=task['filename'])
|
||||
|
||||
|
||||
@bp.route('/play/<task_id>')
|
||||
def play(task_id):
|
||||
task = _tasks.get(task_id)
|
||||
if not task or not task.get('output_path') or not os.path.exists(task['output_path']):
|
||||
return jsonify({'error': '文件不存在'}), 404
|
||||
mime = 'audio/mpeg' if task['output_format'] == 'mp3' else 'audio/wav'
|
||||
return send_file(task['output_path'], mimetype=mime)
|
||||
|
||||
|
||||
@bp.route('/cleanup/<task_id>', methods=['POST'])
|
||||
def cleanup(task_id):
|
||||
task = _tasks.pop(task_id, None)
|
||||
if not task:
|
||||
return jsonify({'success': True})
|
||||
for key in ('src', 'output_path'):
|
||||
try:
|
||||
p = task.get(key)
|
||||
if p and os.path.exists(p):
|
||||
os.remove(p)
|
||||
except OSError:
|
||||
pass
|
||||
return jsonify({'success': True})
|
||||
@@ -40,6 +40,7 @@ _tasks = {}
|
||||
DEFAULT_PARAMS = {
|
||||
'uvr_project_path': '', 'model_dir_mode': 'absolute',
|
||||
'demucs_model_dir': '', 'vr_model_dir': '', 'mdx_model_dir': '',
|
||||
'demucs_model_dir_mode': 'absolute', 'vr_model_dir_mode': 'absolute', 'mdx_model_dir_mode': 'absolute',
|
||||
'arch_type': 'Demucs',
|
||||
'save_format': 'wav', 'wav_type': 'PCM_16', 'mp3_bitrate': '320k',
|
||||
'is_gpu': True, 'device_set': 'Default',
|
||||
@@ -70,7 +71,7 @@ def _save_config(cfg):
|
||||
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def _resolve_model_dir(raw_dir, cfg=None):
|
||||
def _resolve_model_dir(raw_dir, cfg=None, mode=None):
|
||||
"""将模型目录路径解析为绝对路径"""
|
||||
if not raw_dir:
|
||||
return ''
|
||||
@@ -79,7 +80,8 @@ def _resolve_model_dir(raw_dir, cfg=None):
|
||||
if cfg is None:
|
||||
cfg = _load_config()
|
||||
uvr_path = cfg.get('uvr_project_path', '')
|
||||
mode = cfg.get('model_dir_mode', 'absolute')
|
||||
if mode is None:
|
||||
mode = cfg.get('model_dir_mode', 'absolute')
|
||||
if mode == 'relative' and uvr_path:
|
||||
return os.path.normpath(os.path.join(uvr_path, raw_dir))
|
||||
# absolute 模式下如果输入的是相对路径,也尝试拼接 uvr_project_path
|
||||
@@ -92,13 +94,15 @@ def _resolve_model_dir(raw_dir, cfg=None):
|
||||
|
||||
def _get_model_dir_for_arch(arch_type, explicit_dir=None, cfg=None):
|
||||
"""获取指定架构的模型目录:优先显式路径,否则从配置自动推断"""
|
||||
if explicit_dir:
|
||||
return _resolve_model_dir(explicit_dir, cfg)
|
||||
if cfg is None:
|
||||
cfg = _load_config()
|
||||
mode_map = {'Demucs': 'demucs_model_dir_mode', 'VR Arc': 'vr_model_dir_mode', 'MDX-Net': 'mdx_model_dir_mode'}
|
||||
mode = cfg.get(mode_map.get(arch_type, ''), cfg.get('model_dir_mode', 'absolute'))
|
||||
if explicit_dir:
|
||||
return _resolve_model_dir(explicit_dir, cfg, mode=mode)
|
||||
key_map = {'Demucs': 'demucs_model_dir', 'VR Arc': 'vr_model_dir', 'MDX-Net': 'mdx_model_dir'}
|
||||
raw = cfg.get(key_map.get(arch_type, ''), '')
|
||||
return _resolve_model_dir(raw, cfg)
|
||||
return _resolve_model_dir(raw, cfg, mode=mode)
|
||||
|
||||
|
||||
def _get_uvr_paths():
|
||||
@@ -639,18 +643,47 @@ def _update_progress(task_id, step, inference_iterations=0):
|
||||
return
|
||||
progress = min(99, max(1, int((step + inference_iterations) * 100)))
|
||||
task['progress'] = progress
|
||||
import time as _t
|
||||
# 记录子进度(inference_iterations > 0 表示推理中的迭代进度)
|
||||
if inference_iterations > 0:
|
||||
last = task.get('_last_iter_log', 0)
|
||||
if inference_iterations - last >= 0.1:
|
||||
task['_last_iter_log'] = inference_iterations
|
||||
task['logs'].append('[%s] Inference iteration: %.0f%%' % (_t.strftime('%H:%M:%S'), inference_iterations * 100))
|
||||
if len(task['logs']) > 500:
|
||||
task['logs'] = task['logs'][-500:]
|
||||
else:
|
||||
# 主进度每 20% 记录一条
|
||||
last = task.get('_last_progress_log', 0)
|
||||
if progress - last >= 20:
|
||||
task['_last_progress_log'] = progress
|
||||
task['logs'].append('[%s] Progress: %d%%' % (_t.strftime('%H:%M:%S'), progress))
|
||||
if len(task['logs']) > 500:
|
||||
task['logs'] = task['logs'][-500:]
|
||||
|
||||
|
||||
def _make_process_data(task_id, model_data, audio_path, export_path):
|
||||
"""构造 process_data 字典"""
|
||||
base = os.path.splitext(os.path.basename(audio_path))[0]
|
||||
task = _tasks.get(task_id)
|
||||
def _write_console(*args, **kwargs):
|
||||
msg = ' '.join(str(a) for a in args)
|
||||
msg = msg.replace('\r\n', '\n').replace('\r', '\n').strip()
|
||||
if msg and task:
|
||||
import time as _t
|
||||
for line in msg.split('\n'):
|
||||
line = line.strip()
|
||||
if line:
|
||||
task['logs'].append('[%s] %s' % (_t.strftime('%H:%M:%S'), line))
|
||||
if len(task['logs']) > 500:
|
||||
task['logs'] = task['logs'][-500:]
|
||||
return {
|
||||
'model_data': model_data,
|
||||
'export_path': export_path,
|
||||
'audio_file_base': base,
|
||||
'audio_file': audio_path,
|
||||
'set_progress_bar': lambda step, it=0: _update_progress(task_id, step, it),
|
||||
'write_to_console': lambda *_, **__: None,
|
||||
'write_to_console': _write_console,
|
||||
'process_iteration': lambda: None,
|
||||
'cached_source_callback': lambda *_, **__: (None, None),
|
||||
'cached_model_source_holder': lambda *_, **__: None,
|
||||
@@ -663,6 +696,29 @@ def _make_process_data(task_id, model_data, audio_path, export_path):
|
||||
def _do_separate(task_id, model_path, process_method, audio_path, params, model_meta):
|
||||
"""后台线程执行分离"""
|
||||
task = _tasks[task_id]
|
||||
import time as _time
|
||||
|
||||
# 日志捕获
|
||||
class _LogCapture:
|
||||
def __init__(self, orig):
|
||||
self._orig = orig
|
||||
def write(self, msg):
|
||||
if msg and msg.strip():
|
||||
text = msg.replace('\r\n', '\n').replace('\r', '\n').strip()
|
||||
for line in text.split('\n'):
|
||||
line = line.strip()
|
||||
if line:
|
||||
task['logs'].append('[%s] %s' % (_time.strftime('%H:%M:%S'), line))
|
||||
if len(task['logs']) > 500:
|
||||
task['logs'] = task['logs'][-500:]
|
||||
self._orig.write(msg)
|
||||
def flush(self):
|
||||
self._orig.flush()
|
||||
|
||||
old_stdout, old_stderr = sys.stdout, sys.stderr
|
||||
sys.stdout = _LogCapture(old_stdout)
|
||||
sys.stderr = _LogCapture(old_stderr)
|
||||
|
||||
try:
|
||||
_ensure_uvr_imports()
|
||||
|
||||
@@ -727,6 +783,8 @@ def _do_separate(task_id, model_path, process_method, audio_path, params, model_
|
||||
task['error'] = str(e)
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
sys.stderr = old_stderr
|
||||
try:
|
||||
os.remove(audio_path)
|
||||
except OSError:
|
||||
@@ -945,6 +1003,7 @@ def start_separate():
|
||||
'stems': [],
|
||||
'error': None,
|
||||
'device': '',
|
||||
'logs': [],
|
||||
}
|
||||
|
||||
threading.Thread(
|
||||
@@ -968,6 +1027,7 @@ def status(task_id):
|
||||
}
|
||||
if task.get('device'):
|
||||
resp['device'] = task['device']
|
||||
resp['logs'] = task.get('logs', [])[-200:]
|
||||
if task['status'] == 'done':
|
||||
resp['stems'] = task['stems']
|
||||
resp['count'] = len(task['stems'])
|
||||
@@ -976,6 +1036,14 @@ def status(task_id):
|
||||
return jsonify(resp)
|
||||
|
||||
|
||||
@bp.route('/clear-logs/<task_id>', methods=['POST'])
|
||||
def clear_logs(task_id):
|
||||
task = _tasks.get(task_id)
|
||||
if task:
|
||||
task['logs'] = []
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@bp.route('/download/<task_id>/<stem>')
|
||||
def download_stem(task_id, stem):
|
||||
task = _tasks.get(task_id)
|
||||
|
||||
Reference in New Issue
Block a user