Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import os | |
| import shutil | |
| import matplotlib | |
| import traceback | |
| import subprocess | |
| import re | |
| # 1. 核心修复:强制使用非交互式后端 | |
| matplotlib.use('Agg') | |
| # 导入你的核心模块 | |
| from score_eval import MIDIPitchEvaluator | |
| from pitch_error_filter import SRTPitchErrorFilter | |
| from audio_cliper import AudioClipExtractor | |
| from pitch_animation import PitchAnimationGenerator | |
| # 设定最大支持展示的句段数 | |
| MAX_VISIBLE_SEGMENTS = 30 | |
| def check_ffmpeg(): | |
| """检查 FFmpeg 是否在环境中可用""" | |
| try: | |
| subprocess.check_output(["ffmpeg", "-version"]) | |
| return True | |
| except Exception: | |
| return False | |
| def process_pipeline(orig_midi_file, cover_midi_file, orig_audio_file, cover_audio_file, tolerance, severe_rate): | |
| """ | |
| 全流程处理:支持流式视频渲染、逐个展示对比块、过滤短句 | |
| """ | |
| base_dir = os.path.dirname(os.path.abspath(__file__)) | |
| os.chdir(base_dir) | |
| # --- 初始化状态容器 --- | |
| log = "🚀 任务启动...\n" | |
| report_md = "### 📊 等待评分计算..." | |
| # 槽位更新列表:[Log, Report, Row1, V1, A1, Row2, V2, A2...] | |
| current_slots = [] | |
| for _ in range(MAX_VISIBLE_SEGMENTS): | |
| current_slots.extend([ | |
| gr.update(visible=False), # Row | |
| gr.update(value=None), # Video | |
| gr.update(value=None) # Audio | |
| ]) | |
| yield [log, report_md] + current_slots | |
| if not check_ffmpeg(): | |
| log += "❌ 错误:系统中未检测到 FFmpeg。\n" | |
| yield [log, report_md] + current_slots | |
| return | |
| # 初始化输出目录 | |
| output_dirs = ["results", "filtered_srt", "audio_clips", "pitch_animation"] | |
| for d in output_dirs: | |
| dir_path = os.path.join(base_dir, d) | |
| if os.path.exists(dir_path): shutil.rmtree(dir_path) | |
| os.makedirs(dir_path, exist_ok=True) | |
| try: | |
| # --- Step 1: MIDI 评价 --- | |
| log += "Step 1: 正在进行 MIDI 评价分析...\n" | |
| evaluator = MIDIPitchEvaluator(orig_midi_file.name, cover_midi_file.name, tolerance=tolerance) | |
| if evaluator.evaluate(): | |
| stats = evaluator.statistics | |
| log += f"✅ Step 1 完成!基础得分: {stats['overall_score']:.1f}\n" | |
| report_md = f""" | |
| ### 📊 MIDI 评价最终报告 | |
| | 指标项目 | 统计结果 | | |
| | :--- | :--- | | |
| | **最终评价得分** | **{stats['overall_score']:.1f} / 100** | | |
| | 音高准确率 | {stats['accurate_pitch_percentage']:.2f}% | | |
| | 平均音高偏差 | {stats['mean_abs_pitch_dev']:.2f} 半音 | | |
| | 严重走音比例 | {stats['severe_off_percentage']:.2f}% | | |
| | 节奏总偏差 | {stats['total_time_total']:.3f} 秒 | | |
| --- | |
| **📉 扣分详情:** | |
| - **音准惩罚**: `-{100 - stats['base_score']:.1f}` | |
| - **漏唱惩罚**: `-{stats['unmatched_penalty']:.1f}` | |
| - **节奏惩罚**: `-{stats['time_penalty']:.1f}` | |
| """ | |
| else: | |
| log += "❌ Step 1 失败。\n" | |
| yield [log, report_md] + current_slots | |
| return | |
| yield [log, report_md] + current_slots | |
| # --- Step 2 & 3: 筛选与切片 --- | |
| log += "Step 2: 正在解析并筛选句段...\n" | |
| srt_path = os.path.join(base_dir, "results", "音高与节奏对比字幕.srt") | |
| filter_tool = SRTPitchErrorFilter(srt_path, "filtered_srt") | |
| filter_tool.parse_srt() | |
| filter_tool.detect_sentences() | |
| filter_tool.filter_sentences(min_severe_error_rate=severe_rate) | |
| filter_tool.generate_filtered_srt() | |
| log += "Step 3: 正在切分原唱音频片段...\n" | |
| yield [log, report_md] + current_slots | |
| extractor = AudioClipExtractor(os.path.join("filtered_srt", "走音句段汇总报告.txt"), | |
| orig_audio_file.name, cover_audio_file.name, "audio_clips") | |
| extractor.clip_audio() | |
| # --- Step 4: 流式视频渲染 --- | |
| log += "Step 4: 生成对比视频 (已跳过不足1秒的句段)...\n" | |
| generator = PitchAnimationGenerator( | |
| summary_report_path=os.path.join("filtered_srt", "走音句段汇总报告.txt"), | |
| pitch_srt_path=os.path.join("results", "音高与节奏对比字幕.srt"), | |
| original_audio_dir=os.path.join("audio_clips", "original"), | |
| cover_audio_dir=os.path.join("audio_clips", "cover") | |
| ) | |
| orig_audio_map = {} | |
| orig_dir = os.path.join("audio_clips", "original") | |
| if os.path.exists(orig_dir): | |
| for f in os.listdir(orig_dir): | |
| m = re.search(r'句段(\d+)', f) | |
| if m: orig_audio_map[int(m.group(1))] = os.path.join(orig_dir, f) | |
| # 核心循环渲染:增加时长过滤提示 | |
| display_idx = 0 # 记录实际在界面显示的序号 | |
| for i, segment in enumerate(generator.pitch_segments): | |
| seq_num = segment['seq_num'] | |
| duration = segment.get('duration', 0) | |
| # --- 核心修改:时长过滤与日志提示 --- | |
| if duration < 1.0: | |
| log += f"⏳ 句段 {seq_num} 时长为 {duration:.2f}s (不足1秒),跳过视频生成。\n" | |
| yield [log, report_md] + current_slots | |
| continue | |
| if display_idx >= MAX_VISIBLE_SEGMENTS: | |
| log += f"⚠️ 已达到最大展示限制,后续句段不再显示。\n" | |
| break | |
| log += f"🎬 正在渲染句段 {seq_num}...\n" | |
| yield [log, report_md] + current_slots | |
| video_path = generator.generate_single_animation(segment) | |
| if video_path and os.path.exists(video_path): | |
| # 填充到 UI | |
| slot_ptr = display_idx * 3 | |
| current_slots[slot_ptr] = gr.update(visible=True) | |
| current_slots[slot_ptr + 1] = gr.update(value=video_path) | |
| if seq_num in orig_audio_map: | |
| current_slots[slot_ptr + 2] = gr.update(value=orig_audio_map[seq_num]) | |
| log += f"✅ 句段 {seq_num} 完成并展示。\n" | |
| display_idx += 1 | |
| yield [log, report_md] + current_slots | |
| log += f"🎉 全部流程已完成!共展示 {display_idx} 个句段。\n" | |
| yield [log, report_md] + current_slots | |
| except Exception as e: | |
| log += f"❌ 运行异常: {str(e)}\n" | |
| yield [log, report_md] + current_slots | |
| # --- Gradio 界面设计保持不变 --- | |
| with gr.Blocks(title="MIDI Pitch Analysis Pro", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown("# 🎤 MIDI 音准评价与对比可视化系统") | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| gr.Markdown("### 📤 文件上传") | |
| with gr.Row(): | |
| in_orig_midi = gr.File(label="原唱 MIDI", file_types=[".mid"]) | |
| in_cover_midi = gr.File(label="翻唱 MIDI", file_types=[".mid"]) | |
| with gr.Row(): | |
| in_orig_audio = gr.File(label="原唱音频", file_types=["audio"]) | |
| in_cover_audio = gr.File(label="翻唱音频", file_types=["audio"]) | |
| gr.Markdown("### ⚙️ 设置") | |
| with gr.Row(): | |
| in_tolerance = gr.Slider(0, 2, value=0, step=1, label="音准宽容度") | |
| in_severe_rate = gr.Slider(5, 60, value=20, step=5, label="严重走音筛选阈值 (%)") | |
| btn_run = gr.Button("🚀 开始全流程分析", variant="primary") | |
| with gr.Column(scale=3): | |
| out_report = gr.Markdown(value="等待分析...") | |
| out_log = gr.Textbox(label="实时流水线日志", lines=12, interactive=False) | |
| gr.Markdown("---") | |
| gr.Markdown("### 🎬 句段详细对比 (左侧:音高动画 | 右侧:原唱音频)") | |
| all_ui_components = [] | |
| for i in range(MAX_VISIBLE_SEGMENTS): | |
| with gr.Row(variant="panel", visible=False) as row: | |
| v = gr.Video(label=f"对比动画槽位 {i+1}", interactive=False) | |
| a = gr.Audio(label=f"原唱片段参考", interactive=False) | |
| all_ui_components.extend([row, v, a]) | |
| btn_run.click( | |
| fn=process_pipeline, | |
| inputs=[in_orig_midi, in_cover_midi, in_orig_audio, in_cover_audio, in_tolerance, in_severe_rate], | |
| outputs=[out_log, out_report] + all_ui_components | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |