From f16b80ab1fbf72e9e9bfb79f4a4e9803717f1d03 Mon Sep 17 00:00:00 2001 From: litagin02 Date: Sun, 16 Jun 2024 16:48:50 +0900 Subject: [PATCH] Feat: style vector merge for new merge methods, better UI --- gradio_tabs/merge.py | 968 +++++++++++++++++++++++++++++++++---------- 1 file changed, 748 insertions(+), 220 deletions(-) diff --git a/gradio_tabs/merge.py b/gradio_tabs/merge.py index 979d6a3..782168d 100644 --- a/gradio_tabs/merge.py +++ b/gradio_tabs/merge.py @@ -1,6 +1,6 @@ import json from pathlib import Path -from typing import Union +from typing import Union, Any import gradio as gr import numpy as np @@ -32,39 +32,60 @@ def load_safetensors(model_path: Union[str, Path]) -> dict[str, torch.Tensor]: return result -def merge_style( +def load_config(model_name: str) -> dict[str, Any]: + with open(assets_root / model_name / "config.json", encoding="utf-8") as f: + config = json.load(f) + return config + + +def save_config(config: dict[str, Any], model_name: str): + with open(assets_root / model_name / "config.json", "w", encoding="utf-8") as f: + json.dump(config, f, indent=2, ensure_ascii=False) + + +def load_recipe(model_name: str) -> dict[str, Any]: + receipe_path = assets_root / model_name / "recipe.json" + if receipe_path.exists(): + with open(receipe_path, encoding="utf-8") as f: + recipe = json.load(f) + else: + recipe = {} + return recipe + + +def save_recipe(recipe: dict[str, Any], model_name: str): + with open(assets_root / model_name / "recipe.json", "w", encoding="utf-8") as f: + json.dump(recipe, f, indent=2, ensure_ascii=False) + + +def load_style_vectors(model_name: str) -> np.ndarray: + return np.load(assets_root / model_name / "style_vectors.npy") + + +def save_style_vectors(style_vectors: np.ndarray, model_name: str): + np.save(assets_root / model_name / "style_vectors.npy", style_vectors) + + +def merge_style_usual( model_name_a: str, model_name_b: str, weight: float, output_name: str, - style_triple_list: list[tuple[str, ...]], -) -> tuple[Path, list[str]]: + style_tuple_list: list[tuple[str, ...]], +) -> list[str]: """ + new = (1 - weight) * A + weight * B style_triple_list: list[(model_aでのスタイル名, model_bでのスタイル名, 出力するスタイル名)] """ - # 新スタイル名リストにNeutralが含まれているか確認し、Neutralを先頭に持ってくる - if any(triple[2] == DEFAULT_STYLE for triple in style_triple_list): - # 存在する場合、リストをソート - sorted_list = sorted(style_triple_list, key=lambda x: x[2] != DEFAULT_STYLE) - else: - # 存在しない場合、エラーを発生 - raise ValueError(f"No element with {DEFAULT_STYLE} output style name found.") - - style_vectors_a = np.load( - assets_root / model_name_a / "style_vectors.npy" - ) # (style_num_a, 256) - style_vectors_b = np.load( - assets_root / model_name_b / "style_vectors.npy" - ) # (style_num_b, 256) - with open(assets_root / model_name_a / "config.json", encoding="utf-8") as f: - config_a = json.load(f) - with open(assets_root / model_name_b / "config.json", encoding="utf-8") as f: - config_b = json.load(f) + style_vectors_a = load_style_vectors(model_name_a) + style_vectors_b = load_style_vectors(model_name_b) + config_a = load_config(model_name_a) + config_b = load_config(model_name_b) style2id_a = config_a["data"]["style2id"] style2id_b = config_b["data"]["style2id"] new_style_vecs = [] new_style2id = {} - for style_a, style_b, style_out in sorted_list: + for style_a, style_b, style_out in style_tuple_list: if style_a not in style2id_a: logger.error(f"{style_a} is not in {model_name_a}.") raise ValueError(f"{style_a} は {model_name_a} にありません。") @@ -78,29 +99,19 @@ def merge_style( new_style_vecs.append(new_style) new_style2id[style_out] = len(new_style_vecs) - 1 new_style_vecs = np.array(new_style_vecs) - - output_style_path = assets_root / output_name / "style_vectors.npy" - np.save(output_style_path, new_style_vecs) + save_style_vectors(new_style_vecs, output_name) new_config = config_a.copy() new_config["data"]["num_styles"] = len(new_style2id) new_config["data"]["style2id"] = new_style2id new_config["model_name"] = output_name - with open(assets_root / output_name / "config.json", "w", encoding="utf-8") as f: - json.dump(new_config, f, indent=2, ensure_ascii=False) + save_config(new_config, output_name) - # recipe.jsonを読み込んで、style_triple_listを追記 - info_path = assets_root / output_name / "recipe.json" - if info_path.exists(): - with open(info_path, encoding="utf-8") as f: - info = json.load(f) - else: - info = {} - info["style_triple_list"] = style_triple_list - with open(info_path, "w", encoding="utf-8") as f: - json.dump(info, f, indent=2, ensure_ascii=False) + receipe = load_recipe(output_name) + receipe["style_tuple_list"] = style_tuple_list + save_recipe(receipe, output_name) - return output_style_path, list(new_style2id.keys()) + return list(new_style2id.keys()) def merge_style_add_diff( @@ -110,38 +121,23 @@ def merge_style_add_diff( weight: float, output_name: str, style_tuple_list: list[tuple[str, ...]], -) -> tuple[Path, list[str]]: +) -> list[str]: """ new = A + weight * (B - C) + style_tuple_list: list[(model_aでのスタイル名, model_bでのスタイル名, model_cでのスタイル名, 出力するスタイル名)] """ - if any(triple[3] == DEFAULT_STYLE for triple in style_tuple_list): - # 存在する場合、リストをソート - sorted_list = sorted(style_tuple_list, key=lambda x: x[3] != DEFAULT_STYLE) - else: - # 存在しない場合、エラーを発生 - raise ValueError(f"No element with {DEFAULT_STYLE} output style name found.") - - style_vectors_a = np.load( - assets_root / model_name_a / "style_vectors.npy" - ) # (style_num_a, 256) - style_vectors_b = np.load( - assets_root / model_name_b / "style_vectors.npy" - ) # (style_num_b, 256) - style_vectors_c = np.load( - assets_root / model_name_c / "style_vectors.npy" - ) # (style_num_c, 256) - with open(assets_root / model_name_a / "config.json", encoding="utf-8") as f: - config_a = json.load(f) - with open(assets_root / model_name_b / "config.json", encoding="utf-8") as f: - config_b = json.load(f) - with open(assets_root / model_name_c / "config.json", encoding="utf-8") as f: - config_c = json.load(f) + style_vectors_a = load_style_vectors(model_name_a) + style_vectors_b = load_style_vectors(model_name_b) + style_vectors_c = load_style_vectors(model_name_c) + config_a = load_config(model_name_a) + config_b = load_config(model_name_b) + config_c = load_config(model_name_c) style2id_a = config_a["data"]["style2id"] style2id_b = config_b["data"]["style2id"] style2id_c = config_c["data"]["style2id"] new_style_vecs = [] new_style2id = {} - for style_a, style_b, style_c, style_out in sorted_list: + for style_a, style_b, style_c, style_out in style_tuple_list: if style_a not in style2id_a: logger.error(f"{style_a} is not in {model_name_a}.") raise ValueError(f"{style_a} は {model_name_a} にありません。") @@ -158,28 +154,127 @@ def merge_style_add_diff( new_style2id[style_out] = len(new_style_vecs) - 1 new_style_vecs = np.array(new_style_vecs) - output_style_path = assets_root / output_name / "style_vectors.npy" - np.save(output_style_path, new_style_vecs) + save_style_vectors(new_style_vecs, output_name) new_config = config_a.copy() new_config["data"]["num_styles"] = len(new_style2id) new_config["data"]["style2id"] = new_style2id new_config["model_name"] = output_name - with open(assets_root / output_name / "config.json", "w", encoding="utf-8") as f: - json.dump(new_config, f, indent=2, ensure_ascii=False) + save_config(new_config, output_name) - # recipe.jsonを読み込んで、style_tuple_listを追記 - info_path = assets_root / output_name / "recipe.json" - if info_path.exists(): - with open(info_path, encoding="utf-8") as f: - info = json.load(f) - else: - info = {} - info["style_tuple_list"] = style_tuple_list - with open(info_path, "w", encoding="utf-8") as f: - json.dump(info, f, indent=2, ensure_ascii=False) + receipe = load_recipe(output_name) + receipe["style_tuple_list"] = style_tuple_list + save_recipe(receipe, output_name) - return output_style_path, list(new_style2id.keys()) + return list(new_style2id.keys()) + + +def merge_style_weighted_sum( + model_name_a: str, + model_name_b: str, + model_name_c: str, + model_a_coeff: float, + model_b_coeff: float, + model_c_coeff: float, + output_name: str, + style_tuple_list: list[tuple[str, ...]], +) -> list[str]: + """ + new = A * model_a_coeff + B * model_b_coeff + C * model_c_coeff + style_tuple_list: list[(model_aでのスタイル名, model_bでのスタイル名, model_cでのスタイル名, 出力するスタイル名)] + """ + style_vectors_a = load_style_vectors(model_name_a) + style_vectors_b = load_style_vectors(model_name_b) + style_vectors_c = load_style_vectors(model_name_c) + config_a = load_config(model_name_a) + config_b = load_config(model_name_b) + config_c = load_config(model_name_c) + style2id_a = config_a["data"]["style2id"] + style2id_b = config_b["data"]["style2id"] + style2id_c = config_c["data"]["style2id"] + new_style_vecs = [] + new_style2id = {} + for style_a, style_b, style_c, style_out in style_tuple_list: + if style_a not in style2id_a: + logger.error(f"{style_a} is not in {model_name_a}.") + raise ValueError(f"{style_a} は {model_name_a} にありません。") + if style_b not in style2id_b: + logger.error(f"{style_b} is not in {model_name_b}.") + raise ValueError(f"{style_b} は {model_name_b} にありません。") + if style_c not in style2id_c: + logger.error(f"{style_c} is not in {model_name_c}.") + raise ValueError(f"{style_c} は {model_name_c} にありません。") + new_style = ( + style_vectors_a[style2id_a[style_a]] * model_a_coeff + + style_vectors_b[style2id_b[style_b]] * model_b_coeff + + style_vectors_c[style2id_c[style_c]] * model_c_coeff + ) + new_style_vecs.append(new_style) + new_style2id[style_out] = len(new_style_vecs) - 1 + new_style_vecs = np.array(new_style_vecs) + + save_style_vectors(new_style_vecs, output_name) + + new_config = config_a.copy() + new_config["data"]["num_styles"] = len(new_style2id) + new_config["data"]["style2id"] = new_style2id + new_config["model_name"] = output_name + save_config(new_config, output_name) + + receipe = load_recipe(output_name) + receipe["style_tuple_list"] = style_tuple_list + save_recipe(receipe, output_name) + + return list(new_style2id.keys()) + + +def merge_style_add_zero( + model_name_a: str, + model_name_b: str, + weight: float, + output_name: str, + style_tuple_list: list[tuple[str, ...]], +) -> list[str]: + """ + new = A + weight * B + style_tuple_list: list[(model_aでのスタイル名, model_bでのスタイル名, 出力するスタイル名)] + """ + style_vectors_a = load_style_vectors(model_name_a) + style_vectors_b = load_style_vectors(model_name_b) + config_a = load_config(model_name_a) + config_b = load_config(model_name_b) + style2id_a = config_a["data"]["style2id"] + style2id_b = config_b["data"]["style2id"] + new_style_vecs = [] + new_style2id = {} + for style_a, style_b, style_out in style_tuple_list: + if style_a not in style2id_a: + logger.error(f"{style_a} is not in {model_name_a}.") + raise ValueError(f"{style_a} は {model_name_a} にありません。") + if style_b not in style2id_b: + logger.error(f"{style_b} is not in {model_name_b}.") + raise ValueError(f"{style_b} は {model_name_b} にありません。") + new_style = ( + style_vectors_a[style2id_a[style_a]] + + weight * style_vectors_b[style2id_b[style_b]] + ) + new_style_vecs.append(new_style) + new_style2id[style_out] = len(new_style_vecs) - 1 + new_style_vecs = np.array(new_style_vecs) + + save_style_vectors(new_style_vecs, output_name) + + new_config = config_a.copy() + new_config["data"]["num_styles"] = len(new_style2id) + new_config["data"]["style2id"] = new_style2id + new_config["model_name"] = output_name + save_config(new_config, output_name) + + receipe = load_recipe(output_name) + receipe["style_tuple_list"] = style_tuple_list + save_recipe(receipe, output_name) + + return list(new_style2id.keys()) def lerp_tensors(t: float, v0: torch.Tensor, v1: torch.Tensor): @@ -207,7 +302,7 @@ def slerp_tensors( ).to(device) -def merge_models( +def merge_models_usual( model_path_a: str, model_path_b: str, voice_weight: float, @@ -244,7 +339,7 @@ def merge_models( merged_model_path.parent.mkdir(parents=True, exist_ok=True) save_file(merged_model_weight, merged_model_path) - info = { + receipe = { "method": "usual", "model_a": model_path_a, "model_b": model_path_b, @@ -254,34 +349,26 @@ def merge_models( "tempo_weight": tempo_weight, "use_slerp_instead_of_lerp": use_slerp_instead_of_lerp, } - with open(assets_root / output_name / "recipe.json", "w", encoding="utf-8") as f: - json.dump(info, f, indent=2, ensure_ascii=False) + save_recipe(receipe, output_name) - # Default style merge only using Neutral style + # Merge default Neutral style vectors and save model_name_a = Path(model_path_a).parent.name model_name_b = Path(model_path_b).parent.name - style_vectors_a = np.load( - assets_root / model_name_a / "style_vectors.npy" - ) # (style_num_a, 256) - style_vectors_b = np.load( - assets_root / model_name_b / "style_vectors.npy" - ) # (style_num_b, 256) - with open(assets_root / model_name_a / "config.json", encoding="utf-8") as f: - new_config = json.load(f) + style_vectors_a = load_style_vectors(model_name_a) + style_vectors_b = load_style_vectors(model_name_b) + new_config = load_config(model_name_a) new_config["model_name"] = output_name new_config["data"]["num_styles"] = 1 new_config["data"]["style2id"] = {DEFAULT_STYLE: 0} - with open(assets_root / output_name / "config.json", "w", encoding="utf-8") as f: - json.dump(new_config, f, indent=2, ensure_ascii=False) + save_config(new_config, output_name) neutral_vector_a = style_vectors_a[0] neutral_vector_b = style_vectors_b[0] weight = speech_style_weight new_neutral_vector = (1 - weight) * neutral_vector_a + weight * neutral_vector_b new_style_vectors = np.array([new_neutral_vector]) - new_style_path = assets_root / output_name / "style_vectors.npy" - np.save(new_style_path, new_style_vectors) + save_style_vectors(new_style_vectors, output_name) return merged_model_path @@ -543,8 +630,13 @@ def merge_models_gr( "weighted_sum", "add_zero", ], f"Invalid method: {method}" + model_a_name = Path(model_path_a).parent.name + model_b_name = Path(model_path_b).parent.name + model_c_name = Path(model_path_c).parent.name if method == "usual": - merged_model_path = merge_models( + if output_name in [model_a_name, model_b_name]: + return "Error: マージ元のモデル名と同じ名前は使用できません。", None + merged_model_path = merge_models_usual( model_path_a, model_path_b, voice_weight, @@ -555,6 +647,8 @@ def merge_models_gr( use_slerp_instead_of_lerp, ) elif method == "add_diff": + if output_name in [model_a_name, model_b_name, model_c_name]: + return "Error: マージ元のモデル名と同じ名前は使用できません。", None merged_model_path = merge_models_add_diff( model_path_a, model_path_b, @@ -566,6 +660,8 @@ def merge_models_gr( output_name, ) elif method == "weighted_sum": + if output_name in [model_a_name, model_b_name, model_c_name]: + return "Error: マージ元のモデル名と同じ名前は使用できません。", None merged_model_path = merge_models_weighted_sum( model_path_a, model_path_b, @@ -576,6 +672,8 @@ def merge_models_gr( output_name, ) else: # add_zero + if output_name in [model_a_name, model_b_name]: + return "Error: マージ元のモデル名と同じ名前は使用できません。", None merged_model_path = merge_models_add_zero( model_path_a, model_path_b, @@ -585,75 +683,179 @@ def merge_models_gr( tempo_weight, output_name, ) - return f"Success: モデルを{merged_model_path}に保存しました。" + return f"Success: モデルを{merged_model_path}に保存しました。", gr.Dropdown( + choices=[DEFAULT_STYLE], value=DEFAULT_STYLE + ) -def merge_style_gr( +def merge_style_usual_gr( model_name_a: str, model_name_b: str, - model_name_c: str, - method: str, weight: float, output_name: str, - style_tuple_list_str: str, + style_tuple_list: list[tuple[str, ...]], ): if output_name == "": return "Error: 新しいモデル名を入力してください。", None - style_tuple_list: list[tuple[str, ...]] = [] - for line in style_tuple_list_str.split("\n"): - if not line: - continue - style_tuple = line.split(",") - if method == "usual": - if len(style_tuple) != 3: - logger.error(f"Invalid style triple: {line}") - return ( - f"Error: スタイルを3つのカンマ区切りで入力してください:\n{line}", - None, - ) - style_a, style_b, style_out = style_tuple - style_a = style_a.strip() - style_b = style_b.strip() - style_out = style_out.strip() - style_tuple_list.append((style_a, style_b, style_out)) - new_style_path, new_styles = merge_style( - model_name_a, model_name_b, weight, output_name, style_tuple_list - ) - else: - if len(style_tuple) != 4: - logger.error(f"Invalid style triple: {line}") - return ( - f"Error: スタイルを4つのカンマ区切りで入力してください:\n{line}", - None, - ) - style_a, style_b, style_c, style_out = style_tuple - style_a = style_a.strip() - style_b = style_b.strip() - style_c = style_c.strip() - style_out = style_out.strip() - style_tuple_list.append((style_a, style_b, style_c, style_out)) - new_style_path, new_styles = merge_style_add_diff( - model_name_a, - model_name_b, - model_name_c, - weight, - output_name, - style_tuple_list, - ) - return f"Success: スタイルを{new_style_path}に保存しました。", gr.Dropdown( + new_styles = merge_style_usual( + model_name_a, + model_name_b, + weight, + output_name, + style_tuple_list, + ) + return f"Success: {output_name}のスタイルを保存しました。", gr.Dropdown( choices=new_styles, value=new_styles[0] ) +def merge_style_add_diff_gr( + model_name_a: str, + model_name_b: str, + model_name_c: str, + weight: float, + output_name: str, + style_tuple_list: list[tuple[str, ...]], +): + if output_name == "": + return "Error: 新しいモデル名を入力してください。", None + new_styles = merge_style_add_diff( + model_name_a, + model_name_b, + model_name_c, + weight, + output_name, + style_tuple_list, + ) + return f"Success: {output_name}のスタイルを保存しました。", gr.Dropdown( + choices=new_styles, value=new_styles[0] + ) + + +def merge_style_weighted_sum_gr( + model_name_a: str, + model_name_b: str, + model_name_c: str, + model_a_coeff: float, + model_b_coeff: float, + model_c_coeff: float, + output_name: str, + style_tuple_list: list[tuple[str, ...]], +): + if output_name == "": + return "Error: 新しいモデル名を入力してください。", None + new_styles = merge_style_weighted_sum( + model_name_a, + model_name_b, + model_name_c, + model_a_coeff, + model_b_coeff, + model_c_coeff, + output_name, + style_tuple_list, + ) + return f"Success: {output_name}のスタイルを保存しました。", gr.Dropdown( + choices=new_styles, value=new_styles[0] + ) + + +def merge_style_add_zero_gr( + model_name_a: str, + model_name_b: str, + weight: float, + output_name: str, + style_tuple_list: list[tuple[str, ...]], +): + if output_name == "": + return "Error: 新しいモデル名を入力してください。", None + new_styles = merge_style_add_zero( + model_name_a, + model_name_b, + weight, + output_name, + style_tuple_list, + ) + return f"Success: {output_name}のスタイルを保存しました。", gr.Dropdown( + choices=new_styles, value=new_styles[0] + ) + + +# def merge_style_gr( +# model_name_a: str, +# model_name_b: str, +# model_name_c: str, +# model_a_coeff: float, +# model_b_coeff: float, +# model_c_coeff: float, +# method: str, +# weight: float, +# output_name: str, +# style_tuple_list: list[tuple[str, ...]], +# ): +# if output_name == "": +# return "Error: 新しいモデル名を入力してください。", None +# assert method in [ +# "usual", +# "add_diff", +# "weighted_sum", +# "add_zero", +# ], f"Invalid method: {method}" +# if method == "usual": +# new_styles = merge_style_usual( +# model_name_a, +# model_name_b, +# weight, +# output_name, +# style_tuple_list, +# ) +# elif method == "add_diff": +# new_styles = merge_style_add_diff( +# model_name_a, +# model_name_b, +# model_name_c, +# weight, +# output_name, +# style_tuple_list, +# ) +# elif method == "weighted_sum": +# new_styles = merge_style_weighted_sum( +# model_name_a, +# model_name_b, +# model_name_c, +# model_a_coeff, +# model_b_coeff, +# model_c_coeff, +# output_name, +# style_tuple_list, +# ) +# else: # add_zero +# new_styles = merge_style_add_zero( +# model_name_a, +# model_name_b, +# weight, +# output_name, +# style_tuple_list, +# ) +# return f"Success: {output_name}のスタイルを保存しました。", gr.Dropdown( +# choices=new_styles, value=new_styles[0] +# ) + + def simple_tts( model_name: str, text: str, style: str = DEFAULT_STYLE, style_weight: float = 1.0 ): + if model_name == "": + return "Error: モデル名を入力してください。", None model_path = assets_root / model_name / f"{model_name}.safetensors" config_path = assets_root / model_name / "config.json" style_vec_path = assets_root / model_name / "style_vectors.npy" model = TTSModel(model_path, config_path, style_vec_path, device) - return model.infer(text, style=style, style_weight=style_weight) + + return ( + "Success: 音声を生成しました。", + model.infer(text, style=style, style_weight=style_weight), + ) def update_three_model_names_dropdown(model_holder: TTSModelHolder): @@ -661,6 +863,18 @@ def update_three_model_names_dropdown(model_holder: TTSModelHolder): return new_names, new_files, new_names, new_files, new_names, new_files +def get_styles(model_name: str): + config_path = assets_root / model_name / "config.json" + with open(config_path, encoding="utf-8") as f: + config = json.load(f) + styles = list(config["data"]["style2id"].keys()) + return styles + + +def get_triple_styles(model_name_a: str, model_name_b: str, model_name_c: str): + return get_styles(model_name_a), get_styles(model_name_b), get_styles(model_name_c) + + def load_styles_gr(model_name_a: str, model_name_b: str): config_path_a = assets_root / model_name_a / "config.json" with open(config_path_a, encoding="utf-8") as f: @@ -690,11 +904,30 @@ def load_styles_gr(model_name_a: str, model_name_b: str): initial_md = """ ## 使い方 -1. マージしたい2つのモデルを選択してください(`model_assets`フォルダの中から選ばれます)。 -2. マージ後のモデルの名前を入力してください。 -3. マージ後のモデルの声質・話し方・話す速さを調整してください。 -4. 「モデルファイルのマージ」ボタンを押してください(safetensorsファイルがマージされる)。 -5. スタイルベクトルファイルも生成する必要があるので、指示に従ってマージ方法を入力後、「スタイルのマージ」ボタンを押してください。 +### マージ方法の選択 + +マージの方法には4つの方法があります。 +- 通常のマージ `new = (1 - weight) * A + weight * B`: AとBのモデルを指定して、要素ごとに比率を指定して混ぜる + - 単純にAとBの二人の話し方や声音を混ぜたいとき +- 差分マージ `new = A + weight * (B - C)`: AとBとCのモデルを指定して、「Bの要素からCの要素を引いたもの」をAに足す + - 例えば、Bが「Cと同じ人だけど囁いているモデル」とすると、`B - C`は「囁きを表すベクトル」だと思えるので、それをAに足すことで、Aの声のままで囁き声を出すモデルができたりする + - 他にも活用例はいろいろありそう +- 重み付き和 `new = a * A + b * B + c * C`: AとBとCのモデルを指定して、各モデルの係数を指定して混ぜる + - 例えば`new = A - B` としておくと、結果としてできたモデルを別のモデルと「ゼロモデルの加算」で使うことで、差分マージが実現できる + - 他にも何らかの活用法があるかもしれない +- ゼロモデルの加算 `new = A + weight * B`: AとBのモデルを指定して、Bのモデルに要素ごとに比率をかけたものをAに足す + - Bのモデルは重み付き和などで `C - D` などとして作っている場合を想定している + - 他にも何らかの活用法があるかもしれない + + +### マージの手順 + +1. マージ元のモデルたちを選択(`model_assets`フォルダの中から選ばれます) +2. マージ後のモデルの名前を入力 +3. 指示に従って重みや係数を入力 +4. 「モデルファイルのマージ」ボタンを押す (safetensorsファイルがマージされる) +5. 結果を簡易音声合成で確認 +6. 必要に応じてスタイルベクトルのマージを行う 以上でマージは完了で、`model_assets/マージ後のモデル名`にマージ後のモデルが保存され、音声合成のときに使えます。 @@ -703,26 +936,23 @@ initial_md = """ 一番下にマージしたモデルによる簡易的な音声合成機能もつけています。 ## 注意 -1.x系と2.x-JP-Extraのモデルマージは失敗するようです。 + +- 1.x系と2.x-JP-Extraのモデルマージは失敗するようです。 +- 話者数が違うモデル同士はおそらくマージできません。 """ style_merge_md = f""" -## スタイルベクトルのマージ +## 3. スタイルベクトルのマージ -1行に「モデルAのスタイル名, モデルBのスタイル名, 左の2つを混ぜて出力するスタイル名」 -という形式で入力してください。例えば、 -``` -{DEFAULT_STYLE}, {DEFAULT_STYLE}, {DEFAULT_STYLE} -Happy, Surprise, HappySurprise -``` -と入力すると、マージ後のスタイルベクトルは、 -- `{DEFAULT_STYLE}`: モデルAの`{DEFAULT_STYLE}`とモデルBの`{DEFAULT_STYLE}`を混ぜたもの -- `HappySurprise`: モデルAの`Happy`とモデルBの`Surprise`を混ぜたもの -の2つになります。 +1. マージ後のモデルにいくつスタイルを追加したいかを「作りたいスタイル数」で指定 +2. マージ前のモデルのスタイルを「各モデルのスタイルを取得」ボタンで取得 +3. どのスタイルたちから新しいスタイルを作るかを下の欄で入力 +4. 「スタイルのマージ」をクリック -### 注意 -- 必ず「{DEFAULT_STYLE}」という名前のスタイルを作ってください。これは、マージ後のモデルの平均スタイルになります。 -- 構造上の相性の関係で、スタイルベクトルを混ぜる重みは、上の「話し方」と同じ比率で混ぜられます。例えば「話し方」が0のときはモデルAのみしか使われません。 +### スタイルベクトルの混ぜられ方 + +- 構造上の相性の関係で、スタイルベクトルを混ぜる重みは、加重和以外の場合は、上の「話し方」と同じ比率で混ぜられます。例えば「話し方」が0のときはモデルAのみしか使われません。 +- 加重和の場合は、AとBとCの係数によって混ぜられます。 """ usual_md = """ @@ -731,6 +961,8 @@ usual_md = """ new_model = (1 - weight) * A + weight * B ``` としてマージされます。 + +つまり、`weight = 0` のときはモデルA、`weight = 1` のときはモデルBになります。 """ add_diff_md = """ @@ -748,10 +980,11 @@ new_model = a * A + b * B + c * C ``` としてマージされます。 -TIPS: +## TIPS -- A, B, C が全て通常モデルの場合は、`a + b + c = 1`となるようにするのがよいと思います。 -- `a + b + c = 0` とすると(たとえば `A - B`)、話者性を持たないゼロモデルを作ることができ、「ゼロモデルとの和」で結果を使うことが出来ます(差分マージなど) +- A, B, C が全て通常モデルで、通常モデルを作りたい場合は、`a + b + c = 1`となるようにするのがよいと思います。 +- `a + b + c = 0` とすると(たとえば `A - B`)、話者性を持たないゼロモデルを作ることができ、「ゼロモデルとの和」で結果を使うことが出来ます(差分マージの材料などに) +- 他にも、`a = 0.5, b = c = 0`などでモデルAを謎に小さくしたり大きくしたり負にしたりできるので、実験に使ってください。 """ add_zero_md = """ @@ -764,6 +997,12 @@ new_model = A + weight * B としてマージされます。 """ +tts_md = f""" +## 2. 結果のテスト + +マージ後のモデルで音声合成を行います。ただし、デフォルトではスタイルは`{DEFAULT_STYLE}`しか使えないので、他のスタイルを使いたい場合は、下の「スタイルベクトルのマージ」を行ってください。 +""" + def method_change(x: str): assert x in [ @@ -827,7 +1066,6 @@ def create_merge_app(model_holder: TTSModelHolder) -> gr.Blocks: ) return app initial_id = 0 - # initial_model_files = model_holder.model_files_dict[model_names[initial_id]] initial_model_files = [ str(f) for f in model_holder.model_files_dict[model_names[initial_id]] ] @@ -938,43 +1176,325 @@ def create_merge_app(model_holder: TTSModelHolder) -> gr.Blocks: value=False, visible=True, ) - with gr.Column(variant="panel"): - gr.Markdown("## モデルファイル(safetensors)のマージ") + with gr.Column(variant="panel"): + gr.Markdown("## 1. モデルファイル (safetensors) のマージ") + with gr.Row(): model_merge_button = gr.Button( "モデルファイルのマージ", variant="primary" ) info_model_merge = gr.Textbox(label="情報") - with gr.Column(variant="panel"): - gr.Markdown(style_merge_md) - with gr.Row(): - load_style_button = gr.Button("スタイル一覧をロード", scale=1) - styles_a = gr.Textbox(label="モデルAのスタイル一覧") - styles_b = gr.Textbox(label="モデルBのスタイル一覧") - style_triple_list = gr.TextArea( - label="スタイルのマージリスト", - placeholder=f"{DEFAULT_STYLE}, {DEFAULT_STYLE},{DEFAULT_STYLE}\nAngry, Angry, Angry", - value=f"{DEFAULT_STYLE}, {DEFAULT_STYLE}, {DEFAULT_STYLE}", - ) - style_merge_button = gr.Button("スタイルのマージ", variant="primary") - info_style_merge = gr.Textbox(label="情報") + with gr.Column(variant="panel"): + gr.Markdown(tts_md) + text_input = gr.TextArea( + label="テキスト", value="これはテストです。聞こえていますか?" + ) + with gr.Row(): + with gr.Column(): + style = gr.Dropdown( + label="スタイル", + choices=[DEFAULT_STYLE], + value=DEFAULT_STYLE, + ) + emotion_weight = gr.Slider( + minimum=0, + maximum=50, + value=1, + step=0.1, + label="スタイルの強さ", + ) + tts_button = gr.Button("音声合成", variant="primary") + tts_info = gr.Textbox(label="情報") + audio_output = gr.Audio(label="結果") + with gr.Column(variant="panel"): + gr.Markdown(style_merge_md) + style_a_list = gr.State([DEFAULT_STYLE]) + style_b_list = gr.State([DEFAULT_STYLE]) + style_c_list = gr.State([DEFAULT_STYLE]) + gr.Markdown("Hello world!") + with gr.Row(): + style_count = gr.Number(label="作るスタイルの数", value=1, step=1) + + get_style_btn = gr.Button("各モデルのスタイルを取得", variant="primary") + get_style_btn.click( + get_triple_styles, + inputs=[model_name_a, model_name_b, model_name_c], + outputs=[style_a_list, style_b_list, style_c_list], + ) + + def join_names(*args): + if all(arg == DEFAULT_STYLE for arg in args): + return DEFAULT_STYLE + return "_".join(args) + + @gr.render( + inputs=[ + style_count, + style_a_list, + style_b_list, + style_c_list, + method, + ] + ) + def render_style( + style_count, style_a_list, style_b_list, style_c_list, method + ): + print( + f"{style_count=}, {method=}, {style_a_list=}, {style_b_list=}, {style_c_list=}" + ) + a_components = [] + b_components = [] + c_components = [] + out_components = [] + if method in ["usual", "add_zero"]: + for i in range(style_count): + with gr.Row(): + style_a = gr.Dropdown( + label="モデルAのスタイル名", + key=f"style_a_{i}", + choices=style_a_list, + value=DEFAULT_STYLE, + interactive=i != 0, + ) + style_b = gr.Dropdown( + label="モデルBのスタイル名", + key=f"style_b_{i}", + choices=style_b_list, + value=DEFAULT_STYLE, + interactive=i != 0, + ) + style_out = gr.Textbox( + label="出力スタイル名", + key=f"style_out_{i}", + value=DEFAULT_STYLE, + interactive=i != 0, + ) + style_a.change( + join_names, + inputs=[style_a, style_b], + outputs=[style_out], + ) + style_b.change( + join_names, + inputs=[style_a, style_b], + outputs=[style_out], + ) + a_components.append(style_a) + b_components.append(style_b) + out_components.append(style_out) + if method == "usual": + + def _merge_usual(data): + style_tuple_list = [ + (data[a], data[b], data[out]) + for a, b, out in zip( + a_components, b_components, out_components + ) + ] + return merge_style_usual_gr( + data[model_name_a], + data[model_name_b], + data[speech_style_slider], + data[new_name], + style_tuple_list, + ) + + style_merge_btn.click( + _merge_usual, + inputs=set( + a_components + + b_components + + out_components + + [ + model_name_a, + model_name_b, + speech_style_slider, + new_name, + ] + ), + outputs=[info_style_merge, style], + ) + else: # add_zero + + def _merge_add_zero(data): + print("Method is add_zero") + style_tuple_list = [ + (data[a], data[b], data[out]) + for a, b, out in zip( + a_components, b_components, out_components + ) + ] + return merge_style_add_zero_gr( + data[model_name_a], + data[model_name_b], + data[speech_style_slider], + data[new_name], + style_tuple_list, + ) + + style_merge_btn.click( + _merge_add_zero, + inputs=set( + a_components + + b_components + + out_components + + [ + model_name_a, + model_name_b, + speech_style_slider, + new_name, + ] + ), + outputs=[info_style_merge, style], + ) + + elif method in ["add_diff", "weighted_sum"]: + for i in range(style_count): + with gr.Row(): + style_a = gr.Dropdown( + label="モデルAのスタイル名", + key=f"style_a_{i}", + choices=style_a_list, + value=DEFAULT_STYLE, + interactive=i != 0, + ) + style_b = gr.Dropdown( + label="モデルBのスタイル名", + key=f"style_b_{i}", + choices=style_b_list, + value=DEFAULT_STYLE, + interactive=i != 0, + ) + style_c = gr.Dropdown( + label="モデルCのスタイル名", + key=f"style_c_{i}", + choices=style_c_list, + value=DEFAULT_STYLE, + interactive=i != 0, + ) + style_out = gr.Textbox( + label="出力スタイル名", + key=f"style_out_{i}", + value=DEFAULT_STYLE, + interactive=i != 0, + ) + style_a.change( + join_names, + inputs=[style_a, style_b, style_c], + outputs=[style_out], + ) + style_b.change( + join_names, + inputs=[style_a, style_b, style_c], + outputs=[style_out], + ) + style_c.change( + join_names, + inputs=[style_a, style_b, style_c], + outputs=[style_out], + ) + + a_components.append(style_a) + b_components.append(style_b) + c_components.append(style_c) + out_components.append(style_out) + if method == "add_diff": + + def _merge_add_diff(data): + style_tuple_list = [ + (data[a], data[b], data[c], data[out]) + for a, b, c, out in zip( + a_components, + b_components, + c_components, + out_components, + ) + ] + return merge_style_add_diff_gr( + data[model_name_a], + data[model_name_b], + data[model_name_c], + data[speech_style_slider], + data[new_name], + style_tuple_list, + ) + + style_merge_btn.click( + _merge_add_diff, + inputs=set( + a_components + + b_components + + c_components + + out_components + + [ + model_name_a, + model_name_b, + model_name_c, + speech_style_slider, + new_name, + ] + ), + outputs=[info_style_merge, style], + ) + else: # weighted_sum + + def _merge_weighted_sum(data): + style_tuple_list = [ + (data[a], data[b], data[c], data[out]) + for a, b, c, out in zip( + a_components, + b_components, + c_components, + out_components, + ) + ] + return merge_style_weighted_sum_gr( + data[model_name_a], + data[model_name_b], + data[model_name_c], + data[model_a_coeff], + data[model_b_coeff], + data[model_c_coeff], + data[new_name], + style_tuple_list, + ) + + style_merge_btn.click( + _merge_weighted_sum, + inputs=set( + a_components + + b_components + + c_components + + out_components + + [ + model_name_a, + model_name_b, + model_name_c, + model_a_coeff, + model_b_coeff, + model_c_coeff, + new_name, + ] + ), + outputs=[info_style_merge, style], + ) + + with gr.Row(): + add_btn = gr.Button("スタイルを増やす") + del_btn = gr.Button("スタイルを減らす") + add_btn.click( + lambda x: x + 1, + inputs=[style_count], + outputs=[style_count], + ) + del_btn.click( + lambda x: x - 1 if x > 1 else 1, + inputs=[style_count], + outputs=[style_count], + ) + style_merge_btn = gr.Button("スタイルのマージ", variant="primary") + + info_style_merge = gr.Textbox(label="情報") - text_input = gr.TextArea( - label="テキスト", value="これはテストです。聞こえていますか?" - ) - style = gr.Dropdown( - label="スタイル", - choices=[DEFAULT_STYLE], - value=DEFAULT_STYLE, - ) - emotion_weight = gr.Slider( - minimum=0, - maximum=50, - value=1, - step=0.1, - label="スタイルの強さ", - ) - tts_button = gr.Button("音声合成", variant="primary") - audio_output = gr.Audio(label="結果") method.change( method_change, inputs=[method], @@ -1016,11 +1536,11 @@ def create_merge_app(model_holder: TTSModelHolder) -> gr.Blocks: ], ) - load_style_button.click( - load_styles_gr, - inputs=[model_name_a, model_name_b], - outputs=[styles_a, styles_b, style_triple_list], - ) + # load_style_button.click( + # load_styles_gr, + # inputs=[model_name_a, model_name_b], + # outputs=[styles_a, styles_b, style_triple_list], + # ) model_merge_button.click( merge_models_gr, @@ -1039,27 +1559,35 @@ def create_merge_app(model_holder: TTSModelHolder) -> gr.Blocks: tempo_slider, use_slerp_instead_of_lerp, ], - outputs=[info_model_merge], + outputs=[info_model_merge, style], ) - style_merge_button.click( - merge_style_gr, - inputs=[ - model_name_a, - model_name_b, - model_name_c, - method, - speech_style_slider, - new_name, - style_triple_list, - ], - outputs=[info_style_merge, style], - ) + # style_merge_button.click( + # merge_style_gr, + # inputs=[ + # model_name_a, + # model_name_b, + # model_name_c, + # method, + # speech_style_slider, + # new_name, + # style_triple_list, + # ], + # outputs=[info_style_merge, style], + # ) tts_button.click( simple_tts, inputs=[new_name, text_input, style, emotion_weight], - outputs=[audio_output], + outputs=[tts_info, audio_output], ) return app + + +if __name__ == "__main__": + model_holder = TTSModelHolder( + assets_root, device="cuda" if torch.cuda.is_available() else "cpu" + ) + app = create_merge_app(model_holder) + app.launch()