From 835e22b6dce58b7cc34da8a5c89bc30ce390adb7 Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Wed, 17 Jun 2026 17:37:16 +0800 Subject: [PATCH] 555 --- flask-dev-api/.claude/settings.local.json | 4 +- flask-dev-api/app.py | 15 +- flask-dev-api/blueprints/__init__.py | 5 +- flask-dev-api/blueprints/ai_translate.py | 621 ---------------- flask-dev-api/config/ai_translate_config.json | 9 - flask-dev-api/config/mp4_to_audio_config.json | 5 + flask-dev-api/requirements.txt | 34 +- flask-dev-api/stats.db | Bin 204800 -> 204800 bytes flask-dev-api/templates/ai_translate.html | 664 ------------------ flask-dev-api/templates/index.html | 1 - 10 files changed, 40 insertions(+), 1318 deletions(-) delete mode 100644 flask-dev-api/blueprints/ai_translate.py delete mode 100644 flask-dev-api/config/ai_translate_config.json create mode 100644 flask-dev-api/config/mp4_to_audio_config.json delete mode 100644 flask-dev-api/templates/ai_translate.html diff --git a/flask-dev-api/.claude/settings.local.json b/flask-dev-api/.claude/settings.local.json index 10b4bb3..0585fac 100644 --- a/flask-dev-api/.claude/settings.local.json +++ b/flask-dev-api/.claude/settings.local.json @@ -85,7 +85,9 @@ "Bash(python -c \"import llama_cpp; print\\('file:', llama_cpp.__file__\\); print\\('supports_gpu:', llama_cpp.llama_supports_gpu_offload\\(\\)\\)\")", "Bash(cat \"E:/AI/ai-translation/argos/translate-en_zh-1_9/metadata.json\")", "Bash(ls \"E:\\\\AI\\\\ai-translation\\\\argos\")", - "Bash(cat \"E:\\\\AI\\\\ai-translation\\\\nllb\\\\nllb-200-distilled-600M\\\\config.json\" 2>&1 | head -20)" + "Bash(cat \"E:\\\\AI\\\\ai-translation\\\\nllb\\\\nllb-200-distilled-600M\\\\config.json\" 2>&1 | head -20)", + "Bash(ls \"E:/AI/\" 2>/dev/null)", + "WebSearch" ] } } diff --git a/flask-dev-api/app.py b/flask-dev-api/app.py index 79f3f2e..76cdb3e 100644 --- a/flask-dev-api/app.py +++ b/flask-dev-api/app.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- """ 整合工具集主入口 只负责创建 Flask 应用、注册蓝图和启动服务 @@ -11,7 +11,7 @@ from datetime import datetime from flask import Flask, render_template, jsonify, g, request, session, redirect from config import PORT, LOGIN_ENABLED, LOGIN_USERNAME, LOGIN_PASSWORD, SECRET_KEY -from blueprints import 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, mp4_to_audio_bp, ai_translate_bp +from blueprints import 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, mp4_to_audio_bp from utils.stats_db import init_db, record_call, get_total_count, get_avg_duration, get_daily_counts, get_monthly_counts, get_available_years, get_available_months, get_daily_counts_by_month, get_hourly_counts, get_endpoint_stats, get_today_count, get_yesterday_count, get_month_count, get_success_rate, get_duration_distribution, get_slowest_endpoints, get_week_compare, get_recent_calls, get_active_days from utils.stats_db import ALLOWED_TABLES, get_table_info, get_table_rows, get_table_count, insert_row, update_row, delete_rows, clear_table @@ -248,23 +248,12 @@ def create_app(): app.register_blueprint(audio_slicer_bp) app.register_blueprint(uvr_sep_bp) app.register_blueprint(mp4_to_audio_bp) - app.register_blueprint(ai_translate_bp) return app app = create_app() -def start_flask(): - """供桌面启动器调用,以非调试模式运行 Flask""" - from config import AUTO_OPEN_BROWSER - if AUTO_OPEN_BROWSER: - import webbrowser - import threading as _t - _t.Timer(1, webbrowser.open, args=[f'http://127.0.0.1:{PORT}']).start() - a = create_app() - a.run(host='127.0.0.1', port=PORT, use_reloader=False, threaded=True) - if __name__ == '__main__': from config import AUTO_OPEN_BROWSER diff --git a/flask-dev-api/blueprints/__init__.py b/flask-dev-api/blueprints/__init__.py index 5670c23..1b3afb7 100644 --- a/flask-dev-api/blueprints/__init__.py +++ b/flask-dev-api/blueprints/__init__.py @@ -1,4 +1,4 @@ -# blueprints package +# blueprints package from .pin_tu import bp as pin_tu_bp from .base64_codec import bp as base64_bp from .down_video import bp as down_video_bp @@ -17,10 +17,9 @@ 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', 'mp4_to_audio_bp', 'ai_translate_bp', + 'sovits_tts_bp', 'stt_bp', 'ai_dubbing_bp', 'rvc_bp', 'audio_slicer_bp', 'uvr_sep_bp', 'mp4_to_audio_bp', ] diff --git a/flask-dev-api/blueprints/ai_translate.py b/flask-dev-api/blueprints/ai_translate.py deleted file mode 100644 index 66663ee..0000000 --- a/flask-dev-api/blueprints/ai_translate.py +++ /dev/null @@ -1,621 +0,0 @@ -# -*- 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/') -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/') -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/', 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}) diff --git a/flask-dev-api/config/ai_translate_config.json b/flask-dev-api/config/ai_translate_config.json deleted file mode 100644 index 5edef36..0000000 --- a/flask-dev-api/config/ai_translate_config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "engine_type": "nllb", - "nllb_dir": "E:\\AI\\ai-translation\\nllb", - "max_new_tokens": 512, - "batch_size": 8, - "last_nllb_model": "nllb-200-distilled-1.3B", - "last_src_lang": "auto", - "last_tgt_lang": "zh" -} \ No newline at end of file diff --git a/flask-dev-api/config/mp4_to_audio_config.json b/flask-dev-api/config/mp4_to_audio_config.json new file mode 100644 index 0000000..796c64f --- /dev/null +++ b/flask-dev-api/config/mp4_to_audio_config.json @@ -0,0 +1,5 @@ +{ + "output_format": "wav", + "mp3_bitrate": "320k", + "wav_sample_rate": "original" +} \ No newline at end of file diff --git a/flask-dev-api/requirements.txt b/flask-dev-api/requirements.txt index c8242f2..f44eb04 100644 --- a/flask-dev-api/requirements.txt +++ b/flask-dev-api/requirements.txt @@ -1,10 +1,32 @@ +# Web 框架 Flask>=2.0.0 -jieba>=0.42.1 -yt-dlp>=2023.0.0 -chardet>=5.0.0 Werkzeug>=2.0.0 -faster-whisper>=1.0.0 -opencc-python-reimplemented>=0.1.7 -# AI 配音 (GPT-SoVITS 依赖) +requests>=2.28.0 + +# 音频处理 soundfile>=0.12.0 numpy>=1.20.0 +librosa>=0.9.0 + +# NLP / 文本处理 +jieba>=0.42.1 +chardet>=5.0.0 +opencc-python-reimplemented>=0.1.7 +langdetect>=1.0.9 + +# 语音 / AI / 机器学习 +faster-whisper>=1.0.0 +ctranslate2>=3.20.0 +transformers>=4.30.0 +demucs>=4.0.0 + +# 配置 / 工具 +yt-dlp>=2023.0.0 +python-dotenv>=1.0.0 +PyYAML>=6.0 +ml_collections>=0.1.1 + +# PyTorch — 请根据 CUDA 版本自行安装 +# CPU: pip install torch torchvision torchaudio +# CUDA: pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 +torch>=2.0.0 diff --git a/flask-dev-api/stats.db b/flask-dev-api/stats.db index b83bf5a6f068e0efb5a0c487aeca53f3858087d8..e936fc2724e4debee3aa1c4b575240e18edd7271 100644 GIT binary patch delta 1384 zcma)6OH30{6m8%1JN+PP{bBj|1C-kNww-4u5~`w!MhLnvaUl_mAN)a)h$eyrb*CoY zyu^jkXks+cq=}#g7WJ>ejmF>>H*Wl5#0`mhXR3u36PevR^Uk^Fo_lAy(%!DL_gs;6 z^!&nvbu_hOZ`gXPXDEDnC~Unu6eik8i^5e|3I+;}6v+PX{yYApU-5nS-SM^ibng#u z%G>FcJzqUHJguHp{5U_rALCc?Huqz9uX~-_;(Fma<*IdAoe!KRoi&be$AF{Vq1wmn zefD;H#P-XUwwB%BH%R==OD@I^}njmi>LB@L<|MM{Fno4brh z5~N5-h=jz<4UsBx7*?=RCt3Q3gpe4A<*aNiSrn|>arG^)AD+UJG!-h@@t$lcfeJz# zD%i&!WNquNy*<10T`o(~sEVxeNi2k9Ee^{}R_V5X)iSQc6j~#jN|fRn9ag#i)SLB9?kd{z58M5GrG3_p^{p6-9?q<5ndb zDdv22SWuv%kbn}WB@rM_Io!A>lx%H0(ALtJYz{Z=*_jOQYd+d>u(heJZgayH%k%2t z-DxAcJWrUDszdRFH1;BDR8iMJkQM$U<)ujF%=p&lQjJ@p4hv218_n?{B+79pGCf)U z?}^J(j8P?`!va&WA)|d)7TQb=UrhpU29hhkjYOXu?r4*%NM1exnDIcSF7Gxb}WTtu<7sdc1 zRE_CSFa^Z<`}09#vOWVex6&lbssi3@g$NZC1w7faisEa4&#uhCn<>3VsTxHW)!@#A z-d}ymXmoNbQ7WS_3b`hP4)#4fyS*~^mLyUU?Km^#Q5C1D)PRE(_LGX>g&7rN$jPJN prlYVuD?GBPBY#scMoccIf{oR`CM$U(R`avw%)igwi$QJ|`3;&Djr9Nk delta 79 zcmV-V0I>gnzzl%E43HZE$B`UE0mrdmYA*qUgUc_6%P#@9%P#^s;1~)GbpQ{$4|WeT l4)hMR4s;GQ4fGAP4Rx~-AXyCqJe32NAw>crx0)RTW&&yM8QA~; diff --git a/flask-dev-api/templates/ai_translate.html b/flask-dev-api/templates/ai_translate.html deleted file mode 100644 index f128185..0000000 --- a/flask-dev-api/templates/ai_translate.html +++ /dev/null @@ -1,664 +0,0 @@ - - - - - - - AI 翻译 - - - -
-
-
-
正在加载模型,请稍候...
-
- - -
-
- - - -
NLLB-200 本地翻译 · 点击齿轮配置模型目录
- - - - - -
- - -
- - -
-
- 语言设置 - -
-
-
- - -
- -
- - -
-
-
- - -
-
-
输入文本
-
- -
-
- -
- - - - -
-
- - -
-
-

翻译结果

- -
-
-
-
-
翻译中0%
-
-
-
- -
翻译结果将显示在这里
-
-
-
-
- - - - diff --git a/flask-dev-api/templates/index.html b/flask-dev-api/templates/index.html index f7f3709..c57f687 100644 --- a/flask-dev-api/templates/index.html +++ b/flask-dev-api/templates/index.html @@ -1262,7 +1262,6 @@ { id: 'data-manage', name: '数据管理', icon: 'fa-database', url: '/data-manage', section: '概览' }, { id: 'pin-tu', name: '路径文件查阅器', icon: 'fa-images', url: '/pin-tu', section: '工具' }, { id: 'down-video', name: 'XBIY视频下载器', icon: 'fa-video', url: '/down-video', section: '工具' }, - { id: 'ai-translate', name: 'NLLB 翻译与字幕', icon: 'fa-language', url: '/ai-translate',section: '工具' }, { id: 'content-tag', name: 'AI生成文章标签', icon: 'fa-tags', url: '/content-tag', section: '工具' }, { id: 'mp4-to-audio', name: 'VTA 视频转音频', icon: 'fa-video', url: '/mp4-to-audio',section: '工具' }, { id: 'audio-slicer', name: 'AS 智能音频分割', icon: 'fa-scissors', url: '/audio-slicer',section: '工具' },