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()