# app.py - 9th Anniversary Celebration App import gradio as gr import spaces import os import tempfile import shutil from pathlib import Path from datetime import datetime from utils import ( separate_vocals_and_instrumental, merge_vocals_and_instrumental, optimize_audio, ) from rvc_infer import rvc_convert PROJECT_ROOT = Path(__file__).parent SONGS_CONFIG = [ {"year": 2017, "file": "outputs/爱的故事上集-孙耀威_cloned.wav", "original": "songs/爱的故事上集-孙耀威.mp3", "message": "星的光点点洒于午夜,我们的故事,从这一年开始书写 💕"}, {"year": 2018, "file": "outputs/周杰伦 - 告白气球_cloned.wav", "original": "songs/周杰伦 - 告白气球.mp3", "message": "你说你有点难追,想让我知难而退。我没有退,这一年,我们更近了 ❤️"}, {"year": 2019, "file": "outputs/林俊杰 - 修炼爱情_cloned.wav", "original": "songs/林俊杰 - 修炼爱情.mp3", "message": "爱情需要修炼,每一年的陪伴,都是我们爱情的见证 🌟"}, {"year": 2020, "file": "outputs/周深-雪落下的声音_cloned.wav", "original": "songs/周深-雪落下的声音.mp3", "message": "就像雪花轻轻落下,你已经填满我的心 🎨"}, {"year": 2021, "file": "outputs/胡夏&郁可唯-知否知否_cloned.wav", "original": "songs/胡夏&郁可唯-知否知否.mp3", "message": "知否知否,时光荏苒,但我们的爱依然如初 💖"}, {"year": 2022, "file": "outputs/陈奕迅 - 陪你度过漫长岁月_cloned.wav", "original": "songs/陈奕迅 - 陪你度过漫长岁月.mp3", "message": "陪你把独自孤单,变成了勇敢 🌸"}, {"year": 2023, "file": "outputs/Edd_Sheeran_-_Perfect_cloned.wav", "original": "songs/Edd_Sheeran_-_Perfect.mp3", "message": "Baby, you're perfect in my eyes ✨"}, {"year": 2024, "file": "outputs/Michael_Learns_To_Rock_-_Take_Me_To_Your_Heart_Original_Version_cloned.wav", "original": "songs/Michael_Learns_To_Rock_-_Take_Me_To_Your_Heart_Original_Version.mp3", "message": "Take me to your heart, take me to your soul 🏠"}, {"year": 2025, "file": "outputs/Richard_Marx-Right_here_waiting_for_you_(mp3.pm)_cloned.wav", "original": "songs/Richard_Marx-Right_here_waiting_for_you_(mp3.pm).mp3", "message": "I will be right here waiting for you. 9年了,爱依然如故 💝"}, ] def get_audio_path(song, version="cloned"): key = "file" if version == "cloned" else "original" path = PROJECT_ROOT / song[key] return str(path) if path.exists() else None @spaces.GPU(duration=300) def convert_voice(audio_file, progress=gr.Progress()): if audio_file is None: return None, "❌ 请上传一个音频文件" progress(0.05, desc="🎵 开始处理...") with tempfile.TemporaryDirectory() as tmpdir: tmpdir = Path(tmpdir) input_path = Path(audio_file) progress(0.1, desc="步骤1: 读谱 - 分离人声和伴奏...") vocals_path, instrumental_path = separate_vocals_and_instrumental(input_path, tmpdir) if vocals_path is None: progress(0.3, desc="⚠️ 跳过分离,直接转换...") target_audio = input_path instrumental_path = None else: progress(0.4, desc="✅ 人声分离完成") target_audio = vocals_path progress(0.5, desc="步骤2: 清嗓子 - 声线转换...") converted_vocals = tmpdir / "converted.wav" model_dir = PROJECT_ROOT / "models" model_path = None for name in ["xiujia-1220-best", "xiujia-best", "xiujia"]: test = model_dir / f"{name}.pth" if test.exists(): model_path = test break if model_path and model_path.exists(): rvc_convert(str(target_audio), str(converted_vocals), str(model_path)) else: shutil.copy(target_audio, converted_vocals) progress(0.7, desc="⚠️ 未找到模型,使用原音") progress(0.8, desc="✅ 声线转换完成") progress(0.85, desc="步骤3: 开唱 - 合成音频...") final_output = tmpdir / "final.wav" if instrumental_path and instrumental_path.exists(): merge_vocals_and_instrumental(converted_vocals, instrumental_path, final_output) else: optimize_audio(converted_vocals, final_output) result_name = f"converted_{datetime.now().strftime('%H%M%S')}.wav" result_path = PROJECT_ROOT / "outputs" / result_name result_path.parent.mkdir(exist_ok=True) shutil.copy(final_output, result_path) progress(1.0, desc="✅ 完成!") return str(result_path), "🎉 转换成功!听听看吧~" css = """ /* 背景:奶油粉渐变 + 轻微纹理感 */ .gradio-container{ background: radial-gradient(circle at 20% 10%, #fff7fb 0%, transparent 40%), radial-gradient(circle at 80% 20%, #fff0f7 0%, transparent 45%), linear-gradient(135deg, #fff7fb, #fff0f6, #fff7fb) !important; } /* 全局字体与字距更温柔 */ * { letter-spacing: .2px; } /* 标题更像纪念册 */ h1, h2, h3 { color: #d63384 !important; text-align: center; } h1 { font-weight: 800 !important; } /* 主容器:卡片化 */ #app_wrap{ max-width: 980px; margin: 0 auto; padding: 18px 18px 28px 18px; } /* Hero(封面) */ .hero{ border-radius: 24px; padding: 22px 18px; background: linear-gradient(135deg, rgba(255, 221, 238, .55), rgba(255, 255, 255, .75)); border: 1px solid rgba(214, 51, 132, .18); box-shadow: 0 10px 30px rgba(214, 51, 132, .10); } .badges{ display:flex; justify-content:center; gap:10px; margin-top:10px; flex-wrap: wrap; } .badge{ font-size: 12px; padding: 6px 10px; border-radius: 999px; background: rgba(255,255,255,.75); border: 1px solid rgba(214,51,132,.18); color:#b02a5b; } /* 卡片块通用 */ .soft-card{ border-radius: 20px; padding: 16px 14px; background: rgba(255,255,255,.72); border: 1px solid rgba(214,51,132,.14); box-shadow: 0 8px 22px rgba(214,51,132,.08); } /* Tabs 更圆润 */ .tabitem{ border-radius: 18px !important; } .tabs{ border-radius: 20px !important; overflow: hidden; } /* 按钮:奶油粉果冻感 */ button.primary{ border-radius: 999px !important; background: linear-gradient(135deg, #ff77b7, #ff5aa8) !important; border: none !important; box-shadow: 0 10px 22px rgba(255, 90, 168, .22) !important; } button.primary:hover{ transform: translateY(-1px); box-shadow: 0 14px 26px rgba(255, 90, 168, .28) !important; } /* 输入组件更圆润 */ input, textarea{ border-radius: 14px !important; } /* 图片边框更像相册 */ img{ border-radius: 18px !important; } /* Audio 组件卡片感 */ .audio{ border-radius: 18px !important; border: 1px solid rgba(214,51,132,.12) !important; background: rgba(255,255,255,.65) !important; } /* ===== Accordion(年份折叠面板)温馨主题 ===== */ /* 外层卡片 */ .accordion{ border-radius: 18px !important; border: 1px solid rgba(214,51,132,.14) !important; background: rgba(255,255,255,.62) !important; box-shadow: 0 8px 18px rgba(214,51,132,.08); overflow: hidden; } /* Accordion 标题行(收起/展开那一条) */ .accordion > .label-wrap, .accordion .label-wrap{ background: linear-gradient(135deg, rgba(255, 223, 238, .65), rgba(255,255,255,.65)) !important; border-bottom: 1px solid rgba(214,51,132,.12) !important; } /* ✅ 关键:把左侧“黑色竖条/边线”变成粉色(不同版本 gradio 结构不同,多个选择器兜底) */ .accordion, .accordion *{ --ring-color: rgba(255, 90, 168, .35); } /* 有些版本左边那条是 border-left 或 box-shadow */ .accordion{ border-left: 6px solid rgba(255, 90, 168, .35) !important; } /* 标题文字 */ .accordion .label, .accordion .label span, .accordion .label-wrap span{ color: #b02a5b !important; font-weight: 700 !important; } /* hover:更温柔的提亮 */ .accordion:hover{ transform: translateY(-1px); box-shadow: 0 12px 26px rgba(214,51,132,.12); border-left: 6px solid rgba(255, 90, 168, .55) !important; } /* 展开状态:更明显但不刺眼 Gradio 不同版本的“open”类名不一样,这里多写几个兜底 */ .accordion.open, .accordion[open], .accordion:has(.wrap) { border-left: 6px solid rgba(255, 90, 168, .75) !important; } /* 里面内容区域 */ .accordion .wrap, .accordion .panel, .accordion .content{ background: rgba(255,255,255,.55) !important; } /* 小箭头/图标颜色(避免默认黑) */ .accordion svg{ color: rgba(176, 42, 91, .75) !important; fill: rgba(176, 42, 91, .75) !important; } /* Accordion 内 Markdown 文字更柔和 */ .accordion .md, .accordion p, .accordion em{ color: rgba(80, 10, 30, .78) !important; } """ css += """ /* ===== 强制修复:年份 Accordion 左侧黑条清除(适配不同 gradio DOM) ===== */ .year-acc{ position: relative !important; overflow: hidden !important; /* ✅ 直接把可能的黑色左边框干掉 */ border-left: 0 !important; box-shadow: 0 8px 20px rgba(214,51,132,.10) !important; } /* 兼容:如果内部某个元素画了左边框/阴影,全部清零 */ .year-acc *{ border-left-color: transparent !important; } /* ✅ 有些版本 Accordion 本体其实是 details/summary/button,逐个兜底灭掉左边黑条 */ .year-acc details, .year-acc summary, .year-acc button, .year-acc .label-wrap, .year-acc .label, .year-acc .wrap{ border-left: 0 !important; box-shadow: none !important; } /* ✅ 用我们自己的粉色竖条盖住最左侧(放到最顶层) */ .year-acc::before{ content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 10px; z-index: 5; pointer-events: none; background: linear-gradient(180deg, rgba(255, 90, 168, .85), rgba(255, 205, 226, .75)); border-top-left-radius: 18px; border-bottom-left-radius: 18px; } /* 标题区域往右挪,避免被粉条盖住 */ .year-acc .label-wrap, .year-acc summary, .year-acc button{ padding-left: 14px !important; } /* 箭头/图标别黑 */ .year-acc svg, .year-acc svg *{ stroke: rgba(176, 42, 91, .75) !important; fill: rgba(176, 42, 91, .75) !important; } /* hover 更温馨 */ .year-acc:hover::before{ background: linear-gradient(180deg, rgba(255, 90, 168, .95), rgba(255, 205, 226, .85)); } /* =============================== 彻底去掉年份 Accordion 的深色背景 =============================== */ /* Accordion header 本体(summary / button) */ .year-acc summary, .year-acc button, .year-acc .label-wrap, .year-acc [role="button"]{ background: linear-gradient( 135deg, rgba(255, 230, 240, 0.95), rgba(255, 250, 252, 0.95) ) !important; color: #b02a5b !important; border: none !important; box-shadow: none !important; } /* 有些版本深色来自 ::before / ::after,直接干掉 */ .year-acc summary::before, .year-acc summary::after, .year-acc button::before, .year-acc button::after{ background: transparent !important; } /* 去掉 hover 时恢复深色的问题 */ .year-acc summary:hover, .year-acc button:hover, .year-acc:hover summary{ background: linear-gradient( 135deg, rgba(255, 210, 230, 0.98), rgba(255, 245, 248, 0.98) ) !important; } /* 标题文字 */ .year-acc span, .year-acc .label, .year-acc summary span{ color: #b02a5b !important; font-weight: 700 !important; } /* 箭头图标别再黑 */ .year-acc svg, .year-acc svg *{ fill: rgba(176, 42, 91, .8) !important; stroke: rgba(176, 42, 91, .8) !important; } /* 展开态也保持浅色 */ .year-acc[open] summary, .year-acc.open summary{ background: linear-gradient( 135deg, rgba(255, 205, 225, 1), rgba(255, 245, 248, 1) ) !important; } """ css += """ /* ===== Timeline 九年样式(只作用在九年歌曲集区域) ===== */ #timeline{ position: relative; padding-left: 22px; margin-top: 8px; } /* 竖线 */ #timeline::before{ content:""; position:absolute; left: 10px; top: 6px; bottom: 6px; width: 4px; border-radius: 999px; background: linear-gradient(180deg, rgba(255, 90, 168, .55), rgba(255, 205, 226, .35)); } /* 每个年份卡片容器 */ .year-card{ position: relative; margin: 12px 0; border-radius: 18px !important; border: 1px solid rgba(214,51,132,.14) !important; background: rgba(255,255,255,.70) !important; box-shadow: 0 10px 22px rgba(214,51,132,.08) !important; overflow: hidden !important; } /* 左侧圆点 */ .year-card::before{ content:""; position:absolute; left: -18px; top: 18px; width: 14px; height: 14px; border-radius: 999px; background: linear-gradient(135deg, rgba(255, 90, 168, .95), rgba(255, 205, 226, .95)); box-shadow: 0 6px 14px rgba(255, 90, 168, .18); border: 2px solid rgba(255,255,255,.9); } /* ===== 彻底去掉 Accordion header 的深色背景(关键) ===== */ .year-card summary, .year-card button, .year-card .label-wrap, .year-card [role="button"]{ background: linear-gradient(135deg, rgba(255,230,240,.95), rgba(255,250,252,.95)) !important; color: #b02a5b !important; border: none !important; box-shadow: none !important; } /* 防止深色来自伪元素 */ .year-card summary::before, .year-card summary::after, .year-card button::before, .year-card button::after{ background: transparent !important; } /* 标题文字 */ .year-card .label, .year-card span{ color: rgba(176, 42, 91, .92) !important; font-weight: 800 !important; } /* hover 更温柔 */ .year-card:hover summary, .year-card summary:hover{ background: linear-gradient(135deg, rgba(255,210,230,.98), rgba(255,245,248,.98)) !important; } /* 内容区 */ .year-card .wrap, .year-card .panel, .year-card .content{ background: rgba(255,255,255,.62) !important; } /* 箭头图标 */ .year-card svg, .year-card svg *{ stroke: rgba(176, 42, 91, .78) !important; fill: rgba(176, 42, 91, .78) !important; } /* 小提示文字更柔 */ .year-card .md, .year-card p, .year-card em{ color: rgba(80, 10, 30, .78) !important; } /* 让每年的标题更“章节感” */ .year-title{ font-size: 16px; font-weight: 900; } """ with gr.Blocks(title="💕 9周年纪念", theme=gr.themes.Soft(primary_hue="pink"), css=css) as demo: gr.Markdown("# 💕 9th Anniversary Celebration 💕\n### 2017 - 2025 · 九年,久远") with gr.Row(): for img_name in ["couple.png", "couple1.png"]: img_path = PROJECT_ROOT / img_name if img_path.exists(): gr.Image(str(img_path), show_label=False, height=220, container=False) with gr.Tab("🎵 九年歌曲集"): gr.Markdown("## 🎵 九年,唱不尽的爱") gr.Markdown("像翻纪念册一样,打开每一年听听我们的旋律。") with gr.Column(elem_id="timeline"): for song in SONGS_CONFIG: with gr.Accordion( f"💗 {song['year']} 年", open=False, elem_classes=["year-card"], ): gr.Markdown(f"*{song['message']}*") with gr.Row(): cloned = get_audio_path(song, "cloned") original = get_audio_path(song, "original") if cloned: gr.Audio(cloned, label="🎤 老公唱") if original: gr.Audio(original, label="🎵 原唱") with gr.Tab("🎤 上传歌曲"): gr.Markdown("## 🎤 上传MP3,我唱给你听!") with gr.Row(): with gr.Column(): audio_in = gr.Audio(label="选择歌曲 🎵", type="filepath", sources=["upload"]) btn = gr.Button("✨ 开始转换", variant="primary", size="lg") status = gr.Textbox(label="状态", interactive=False) with gr.Column(): audio_out = gr.Audio(label="🎵 老公开唱", type="filepath") btn.click(convert_voice, [audio_in], [audio_out, status]) gr.Markdown("---\n## 💝 九年不是终点,而是我们故事的第九章 💝") with gr.Row(): for img_name in ["family.png", "family2.png"]: img_path = PROJECT_ROOT / img_name if img_path.exists(): gr.Image(str(img_path), show_label=False, height=220, container=False) if __name__ == "__main__": demo.launch()