midi-pitch-eval / app.py
zziC7's picture
Update app.py
b18b22e verified
Raw
History Blame Contribute Delete
8.41 kB
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()