9year / app.py
invokerx's picture
Update app.py
095d70d verified
# 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()