From 6931a97594ce9bf1acdf27c5df8785f3101c7a84 Mon Sep 17 00:00:00 2001 From: XucroYuri Date: Tue, 7 Jul 2026 14:18:35 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20emotion=E5=8F=8C=E6=BA=90=E5=85=BC?= =?UTF-8?q?=E5=AE=B9(.list+audio=5Fmetadata.json)=20+=20=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E8=81=94=E5=8A=A8=20+=20=E5=B8=83=E5=B1=80=E7=B4=A7=E5=87=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. emotion 双源兼容 (核心): proplus 训练WebUI 有两种 emotion 数据源, 互斥存在: - 旧: ASR标注 .list 第5列 (output/asr_opt//*.list) - 新: logs//audio_metadata.json (推理WebUI回写) _read_emotion_map_for_webui / _read_emotion_map 现合并两源, audio_metadata.json 优先(推理时回写的更新)。 实测: 光头TTS新(emotion在.list)->惊恐; 九九-许珺雯(emotion在json)->挑眉自信。 2. on_model_name_change 修复初始 emotion 不显示: 原返回3值[样本,GPT,SoVITS], 依赖 ref_sample_dropdown.change 级联触发。 但 app.load 设置 dropdown value 后 Gradio 不一定触发 .change, 导致页面加载时 emotion 框为空。 现返回6值[样本,GPT,SoVITS,参考音频,参考文本,情绪], 一次性联动全部。 3. 布局紧凑: - 参考音频去掉 Column 包裹+equal_height(导致Audio被拉伸留白), 自然高度 - 参考文本 4行->3行 - 多参考文件+v3参数从右列移出为独立满宽行(不再干扰主行对齐) 主行变两列: 参考音频(10) + 语种&文本&情绪(10), 紧凑对齐。 --- GPT_SoVITS/inference_webui.py | 156 +++++++++++++++++++--------------- api_v2.py | 50 +++++++---- 2 files changed, 121 insertions(+), 85 deletions(-) diff --git a/GPT_SoVITS/inference_webui.py b/GPT_SoVITS/inference_webui.py index b97486ba..4f8b281b 100644 --- a/GPT_SoVITS/inference_webui.py +++ b/GPT_SoVITS/inference_webui.py @@ -1270,43 +1270,61 @@ def _read_name2text_for_webui(logs_dir: Path) -> dict[str, dict[str, str]]: def _read_emotion_map_for_webui(model_name: str) -> dict[str, str]: - """读取 ASR 标注 .list 中的 emotion 列,返回 {audio_name|stem: emotion}。 + """读取训练数据的 emotion,返回 {audio_name|stem: emotion}。 - 扫描 output/asr_opt// 下所有 *.list(4/5/6 列均兼容)。 - 无 emotion 列或文件不存在时返回空 dict。 + 兼容新旧两种 emotion 数据源: + 1. 旧: ASR 标注 .list 第5列 (output/asr_opt//*.list) + 2. 新: logs//audio_metadata.json (推理WebUI回写的元数据) + 合并两者, audio_metadata.json 优先(推理时回写的更新)。 """ from tools.list_metadata import parse_list_line + from config import exp_root emotion_map: dict[str, str] = {} + + def _add(name: str, emotion: str): + if name and emotion: + emotion_map[name] = emotion + emotion_map[Path(name).stem] = emotion + + # 源1: .list (旧, ASR 标注) list_dir = Path("output") / "asr_opt" / model_name - if not list_dir.exists(): - return emotion_map - for list_path in sorted(list_dir.glob("*.list")): + if list_dir.exists(): + for list_path in sorted(list_dir.glob("*.list")): + try: + for line in list_path.read_text(encoding="utf-8", errors="ignore").splitlines(): + item = parse_list_line(line) + if item is None or not item.emotion: + continue + _add(os.path.basename(item.wav_path.strip("\"'")), item.emotion) + except Exception: + continue + + # 源2: audio_metadata.json (新, 推理WebUI回写; 优先级高, 覆盖源1) + meta_path = Path(exp_root) / model_name / "audio_metadata.json" + if meta_path.exists(): try: - for line in list_path.read_text(encoding="utf-8", errors="ignore").splitlines(): - item = parse_list_line(line) - if item is None or not item.emotion: - continue - name = os.path.basename(item.wav_path.strip("\"'")) - if not name: - continue - emotion_map[name] = item.emotion - emotion_map[Path(name).stem] = item.emotion + import json + meta = json.loads(meta_path.read_text(encoding="utf-8", errors="ignore")) + for name, info in meta.items(): + if isinstance(info, dict) and info.get("emotion"): + _add(name, info["emotion"]) except Exception: - continue + pass + return emotion_map def on_model_name_change(model_name: str): - """模型名变化时:加载训练样本列表 + 自动匹配最佳 GPT/SoVITS 权重。 + """模型名变化时:加载训练样本 + 自动匹配权重 + 自动引用首样本为参考。 - 返回: [样本下拉框, GPT下拉框, SoVITS下拉框] - 样本下拉框 value 设为首个样本, 会级联触发 on_ref_sample_change 自动引用为参考音频。 - GPT/SoVITS 自动选中 epoch 最接近推荐值(8/15)的权重, 用户仍可手动改。 + 返回: [样本下拉, GPT下拉, SoVITS下拉, 参考音频, 参考文本, 情绪/括注] + 一次性联动全部字段, 不依赖 ref_sample_dropdown.change 的级联触发 + (app.load 设置 dropdown value 后 Gradio 不一定触发 .change)。 """ model_name = _coerce_single(model_name) if not model_name: - return gr.Dropdown(choices=[], value=""), gr.Dropdown(), gr.Dropdown() + return gr.Dropdown(choices=[], value=""), gr.Dropdown(choices=[], value=""), gr.Dropdown(choices=[], value=""), None, "", "" from config import exp_root # 1. 加载训练样本 @@ -1327,10 +1345,14 @@ def on_model_name_change(model_name: str): label += f" | 【{emotion}】" samples.append((label, str(f))) choices = [s[0] for s in samples] - value = samples[0][0] if samples else "" # 2. 自动匹配 GPT/SoVITS 权重(epoch 最接近 8/15) gpt_dd, sovits_dd = auto_match_weights_for_model(model_name) - return gr.Dropdown(choices=choices, value=value), gpt_dd, sovits_dd + # 3. 自动引用首样本为参考音频+文本+情绪(避免依赖级联触发) + if samples: + first_audio, first_text, first_emotion = on_ref_sample_change(samples[0][0], model_name) + else: + first_audio, first_text, first_emotion = None, "", "" + return gr.Dropdown(choices=choices, value=choices[0] if choices else ""), gpt_dd, sovits_dd, first_audio, first_text, first_emotion def on_ref_sample_change(sample_label: str, model_name: str): @@ -1504,61 +1526,59 @@ with gr.Blocks(title="GPT-SoVITS WebUI", analytics_enabled=False, js=js, css=css # v3/v4 不支持该模式: 代码层直接隐藏, 避免用户误操作报错 visible=model_version not in v3v4set, ) - with gr.Row(equal_height=True): - with gr.Column(scale=10): - inp_ref = gr.Audio(label=i18n("请上传参考音频(推荐3~10秒)"), type="filepath") + with gr.Row(): + inp_ref = gr.Audio(label=i18n("请上传参考音频(推荐3~10秒)"), type="filepath", scale=10) with gr.Column(scale=10): prompt_language = gr.Dropdown( label=i18n("参考音频的语种"), choices=list(dict_language.keys()), value=i18n("中文"), ) - prompt_text = gr.Textbox(label=i18n("参考音频的文本"), value="", lines=4, max_lines=4, scale=1) + prompt_text = gr.Textbox(label=i18n("参考音频的文本"), value="", lines=3, max_lines=3) ref_emotion_text = gr.Textbox( label=i18n("情绪/括注(来自训练样本,无数据留空)"), value="", interactive=False, - scale=1, - ) - with gr.Column(scale=8): - inp_refs = ( - gr.File( - label=i18n( - "可选项:通过拖拽多个文件上传多个参考音频(建议同性),平均融合他们的音色。如不填写此项,音色由左侧单个参考音频控制。如是微调模型,建议参考音频全部在微调训练集音色内,底模不用管。" - ), - file_count="multiple", - ) - if model_version not in v3v4set - else gr.File( - label=i18n( - "可选项:通过拖拽多个文件上传多个参考音频(建议同性),平均融合他们的音色。如不填写此项,音色由左侧单个参考音频控制。如是微调模型,建议参考音频全部在微调训练集音色内,底模不用管。" - ), - file_count="multiple", - visible=False, - ) - ) - sample_steps = ( - gr.Radio( - label=i18n("采样步数,如果觉得电,提高试试,如果觉得慢,降低试试"), - value=32 if model_version == "v3" else 8, - choices=[4, 8, 16, 32, 64, 128] if model_version == "v3" else [4, 8, 16, 32], - visible=True, - ) - if model_version in v3v4set - else gr.Radio( - label=i18n("采样步数,如果觉得电,提高试试,如果觉得慢,降低试试"), - choices=[4, 8, 16, 32, 64, 128] if model_version == "v3" else [4, 8, 16, 32], - visible=False, - value=32 if model_version == "v3" else 8, - ) - ) - if_sr_Checkbox = gr.Checkbox( - label=i18n("v3输出如果觉得闷可以试试开超分"), - value=False, - interactive=True, - show_label=True, - visible=False if model_version != "v3" else True, ) + # 多参考文件 + v3采样参数: 独立行(满宽), 不参与主行高度对齐 + inp_refs = ( + gr.File( + label=i18n( + "可选项:通过拖拽多个文件上传多个参考音频(建议同性),平均融合他们的音色。如不填写此项,音色由左侧单个参考音频控制。如是微调模型,建议参考音频全部在微调训练集音色内,底模不用管。" + ), + file_count="multiple", + ) + if model_version not in v3v4set + else gr.File( + label=i18n( + "可选项:通过拖拽多个文件上传多个参考音频(建议同性),平均融合他们的音色。如不填写此项,音色由左侧单个参考音频控制。如是微调模型,建议参考音频全部在微调训练集音色内,底模不用管。" + ), + file_count="multiple", + visible=False, + ) + ) + sample_steps = ( + gr.Radio( + label=i18n("采样步数,如果觉得电,提高试试,如果觉得慢,降低试试"), + value=32 if model_version == "v3" else 8, + choices=[4, 8, 16, 32, 64, 128] if model_version == "v3" else [4, 8, 16, 32], + visible=True, + ) + if model_version in v3v4set + else gr.Radio( + label=i18n("采样步数,如果觉得电,提高试试,如果觉得慢,降低试试"), + choices=[4, 8, 16, 32, 64, 128] if model_version == "v3" else [4, 8, 16, 32], + visible=False, + value=32 if model_version == "v3" else 8, + ) + ) + if_sr_Checkbox = gr.Checkbox( + label=i18n("v3输出如果觉得闷可以试试开超分"), + value=False, + interactive=True, + show_label=True, + visible=False if model_version != "v3" else True, + ) gr.Markdown(html_center(i18n("*请填写需要合成的目标文本和语种模式"), "h3")) with gr.Row(): with gr.Column(scale=13): @@ -1682,7 +1702,7 @@ with gr.Blocks(title="GPT-SoVITS WebUI", analytics_enabled=False, js=js, css=css model_name_dropdown.change( fn=on_model_name_change, inputs=[model_name_dropdown], - outputs=[ref_sample_dropdown, GPT_dropdown, SoVITS_dropdown], + outputs=[ref_sample_dropdown, GPT_dropdown, SoVITS_dropdown, inp_ref, prompt_text, ref_emotion_text], ) ref_sample_dropdown.change( fn=on_ref_sample_change, diff --git a/api_v2.py b/api_v2.py index 5ead07d4..9f9e316e 100644 --- a/api_v2.py +++ b/api_v2.py @@ -638,31 +638,47 @@ def _read_name2text(logs_dir: Path) -> dict[str, dict[str, str]]: def _read_emotion_map(model_name: str) -> dict[str, str]: - """读取 ASR 标注 .list 中的 emotion 列,返回 {audio_name|stem: emotion}。 + """读取训练数据的 emotion,返回 {audio_name|stem: emotion}。 - 扫描 output/asr_opt// 下所有 *.list(兼容 4/5/6 列)。 - 兼容 proplus-hc-dev 分支训练数据:emotion 为第 5 列,缺省为空串。 - 无 .list 或无 emotion 列时返回空 dict。 + 兼容新旧两种 emotion 数据源: + 1. 旧: ASR 标注 .list 第5列 (output/asr_opt//*.list) + 2. 新: logs//audio_metadata.json (推理WebUI回写的元数据) + 合并两者, audio_metadata.json 优先(推理时回写的更新)。 """ + import json from tools.list_metadata import parse_list_line emotion_map: dict[str, str] = {} + + def _add(name: str, emotion: str): + if name and emotion: + emotion_map[name] = emotion + emotion_map[Path(name).stem] = emotion + + # 源1: .list (旧, ASR 标注) list_dir = Path("output") / "asr_opt" / model_name - if not list_dir.exists(): - return emotion_map - for list_path in sorted(list_dir.glob("*.list")): + if list_dir.exists(): + for list_path in sorted(list_dir.glob("*.list")): + try: + for line in list_path.read_text(encoding="utf-8", errors="ignore").splitlines(): + item = parse_list_line(line) + if item is None or not item.emotion: + continue + _add(os.path.basename(item.wav_path.strip("\"'")), item.emotion) + except Exception: + continue + + # 源2: audio_metadata.json (新, 推理WebUI回写; 优先级高, 覆盖源1) + meta_path = Path(exp_root) / model_name / "audio_metadata.json" + if meta_path.exists(): try: - for line in list_path.read_text(encoding="utf-8", errors="ignore").splitlines(): - item = parse_list_line(line) - if item is None or not item.emotion: - continue - name = os.path.basename(item.wav_path.strip("\"'")) - if not name: - continue - emotion_map[name] = item.emotion - emotion_map[Path(name).stem] = item.emotion + meta = json.loads(meta_path.read_text(encoding="utf-8", errors="ignore")) + for name, info in meta.items(): + if isinstance(info, dict) and info.get("emotion"): + _add(name, info["emotion"]) except Exception: - continue + pass + return emotion_map