mirror of
https://github.com/index-tts/index-tts.git
synced 2026-08-28 23:01:17 +08:00
Normalize label-style Qwen emotion outputs in v2/v2.5 inference (#751)
* Initial plan * fix: normalize label-style qwen emotion outputs Co-authored-by: nanaoto <19526637+nanaoto@users.noreply.github.com> * test: cover qwen emotion label redirects Co-authored-by: nanaoto <19526637+nanaoto@users.noreply.github.com> * style: fix qwen emotion test spacing Co-authored-by: nanaoto <19526637+nanaoto@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nanaoto <19526637+nanaoto@users.noreply.github.com>
This commit is contained in:
@@ -782,12 +782,51 @@ class QwenEmotion:
|
||||
) from exc
|
||||
return max(self.min_score, min(self.max_score, value))
|
||||
|
||||
def normalize_content(self, content):
|
||||
if isinstance(content, dict):
|
||||
normalized = dict(content)
|
||||
else:
|
||||
normalized = {}
|
||||
|
||||
def label_to_cn_key(value):
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
|
||||
value = value.strip()
|
||||
if value in self.cn_key_to_en:
|
||||
return value
|
||||
|
||||
value_lower = value.lower()
|
||||
for cn_key, en_key in self.cn_key_to_en.items():
|
||||
if value_lower == en_key:
|
||||
return cn_key
|
||||
return None
|
||||
|
||||
detected_key = label_to_cn_key(content) if isinstance(content, str) else None
|
||||
if detected_key is None:
|
||||
for alias in ("emotion", "emotion_label", "label", "情感", "情绪"):
|
||||
detected_key = label_to_cn_key(normalized.get(alias))
|
||||
if detected_key is not None:
|
||||
break
|
||||
if detected_key is not None and all(key not in normalized for key in self.desired_vector_order):
|
||||
normalized[detected_key] = 1.0
|
||||
|
||||
for cn_key in self.desired_vector_order:
|
||||
detected_key = label_to_cn_key(normalized.get(cn_key))
|
||||
if detected_key is not None:
|
||||
normalized[cn_key] = 1.0 if detected_key == cn_key else 0.0
|
||||
if detected_key != cn_key:
|
||||
normalized[detected_key] = 1.0
|
||||
|
||||
return normalized
|
||||
|
||||
def convert(self, content):
|
||||
# generate emotion vector dictionary:
|
||||
# - insert values in desired order (Python 3.7+ `dict` remembers insertion order)
|
||||
# - convert Chinese keys to English
|
||||
# - clamp all values to the allowed min/max range
|
||||
# - use 0.0 for any values that were missing in `content`
|
||||
content = self.normalize_content(content)
|
||||
emotion_dict = {
|
||||
self.cn_key_to_en[cn_key]: self.clamp_score(content.get(cn_key, 0.0))
|
||||
for cn_key in self.desired_vector_order
|
||||
|
||||
@@ -971,12 +971,51 @@ class QwenEmotion:
|
||||
def clamp_score(self, value):
|
||||
return max(self.min_score, min(self.max_score, value))
|
||||
|
||||
def normalize_content(self, content):
|
||||
if isinstance(content, dict):
|
||||
normalized = dict(content)
|
||||
else:
|
||||
normalized = {}
|
||||
|
||||
def label_to_cn_key(value):
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
|
||||
value = value.strip()
|
||||
if value in self.cn_key_to_en:
|
||||
return value
|
||||
|
||||
value_lower = value.lower()
|
||||
for cn_key, en_key in self.cn_key_to_en.items():
|
||||
if value_lower == en_key:
|
||||
return cn_key
|
||||
return None
|
||||
|
||||
detected_key = label_to_cn_key(content) if isinstance(content, str) else None
|
||||
if detected_key is None:
|
||||
for alias in ("emotion", "emotion_label", "label", "情感", "情绪"):
|
||||
detected_key = label_to_cn_key(normalized.get(alias))
|
||||
if detected_key is not None:
|
||||
break
|
||||
if detected_key is not None and all(key not in normalized for key in self.desired_vector_order):
|
||||
normalized[detected_key] = 1.0
|
||||
|
||||
for cn_key in self.desired_vector_order:
|
||||
detected_key = label_to_cn_key(normalized.get(cn_key))
|
||||
if detected_key is not None:
|
||||
normalized[cn_key] = 1.0 if detected_key == cn_key else 0.0
|
||||
if detected_key != cn_key:
|
||||
normalized[detected_key] = 1.0
|
||||
|
||||
return normalized
|
||||
|
||||
def convert(self, content):
|
||||
# generate emotion vector dictionary:
|
||||
# - insert values in desired order (Python 3.7+ `dict` remembers insertion order)
|
||||
# - convert Chinese keys to English
|
||||
# - clamp all values to the allowed min/max range
|
||||
# - use 0.0 for any values that were missing in `content`
|
||||
content = self.normalize_content(content)
|
||||
emotion_dict = {
|
||||
self.cn_key_to_en[cn_key]: self.clamp_score(content.get(cn_key, 0.0))
|
||||
for cn_key in self.desired_vector_order
|
||||
|
||||
@@ -256,6 +256,136 @@ def test_split_leaves_short_text_alone():
|
||||
assert splitter.split_text_by_tokens(text, 120, "<|zh|> ") == [text]
|
||||
|
||||
|
||||
def _load_qwen_emotion_module(module_name, monkeypatch):
|
||||
class _Dummy:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def register(name, **attrs):
|
||||
module = types.ModuleType(name)
|
||||
for key, value in attrs.items():
|
||||
setattr(module, key, value)
|
||||
monkeypatch.setitem(sys.modules, name, module)
|
||||
return module
|
||||
|
||||
torch = register("torch")
|
||||
torch.cuda = types.SimpleNamespace(is_available=lambda: False)
|
||||
torch.xpu = types.SimpleNamespace(is_available=lambda: False)
|
||||
torch.backends = types.SimpleNamespace(mps=types.SimpleNamespace(is_available=lambda: False))
|
||||
torch.float16 = "float16"
|
||||
torch.bfloat16 = "bfloat16"
|
||||
torch.Tensor = object
|
||||
torch.no_grad = lambda: (lambda func: func)
|
||||
|
||||
torch_nn = register("torch.nn")
|
||||
torch_nn_functional = register("torch.nn.functional")
|
||||
torch_nn_utils = register("torch.nn.utils")
|
||||
torch_nn_utils_rnn = register("torch.nn.utils.rnn", pad_sequence=lambda *args, **kwargs: None)
|
||||
torch.nn = torch_nn
|
||||
torch_nn.functional = torch_nn_functional
|
||||
torch_nn.utils = torch_nn_utils
|
||||
torch_nn_utils.rnn = torch_nn_utils_rnn
|
||||
|
||||
register("torchaudio")
|
||||
register("librosa")
|
||||
register("safetensors")
|
||||
register("transformers", AutoTokenizer=_Dummy, SeamlessM4TFeatureExtractor=_Dummy, Wav2Vec2BertModel=_Dummy)
|
||||
register("modelscope", AutoModelForCausalLM=_Dummy)
|
||||
register("omegaconf", OmegaConf=types.SimpleNamespace(load=lambda *args, **kwargs: None))
|
||||
register("indextts.gpt.model_v2", UnifiedVoice=_Dummy)
|
||||
register("indextts.codec.maskgct_codec", build_semantic_codec=lambda *args, **kwargs: None)
|
||||
register("indextts.codec.models", EnhancedCodec=_Dummy)
|
||||
register("indextts.utils.checkpoint", load_checkpoint=lambda *args, **kwargs: None)
|
||||
register("indextts.utils.front", TextNormalizer=_Dummy, TextTokenizer=_Dummy)
|
||||
register("indextts.utils.tokenizer", get_tokenizer=lambda *args, **kwargs: None, lang_to_token={})
|
||||
register("indextts.utils.ja_g2p", JapaneseG2PProcessor=_Dummy)
|
||||
register("indextts.utils.nemo_tn", normalize_text=lambda text: text)
|
||||
register("indextts.s2mel.modules.commons", load_checkpoint2=lambda *args, **kwargs: None, MyModel=_Dummy)
|
||||
register("indextts.s2mel.modules.bigvgan", bigvgan=object())
|
||||
register("indextts.s2mel.modules.campplus.DTDNN", CAMPPlus=_Dummy)
|
||||
register("indextts.s2mel.modules.audio", mel_spectrogram=lambda *args, **kwargs: None)
|
||||
|
||||
sys.modules.pop(module_name, None)
|
||||
return importlib.import_module(module_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("module_name", ["indextts.infer_v2", "indextts.infer_v2_5"])
|
||||
def test_qwen_emotion_convert_accepts_label_values(module_name, monkeypatch):
|
||||
module = _load_qwen_emotion_module(module_name, monkeypatch)
|
||||
|
||||
emo = module.QwenEmotion.__new__(module.QwenEmotion)
|
||||
emo.cn_key_to_en = {
|
||||
"高兴": "happy",
|
||||
"愤怒": "angry",
|
||||
"悲伤": "sad",
|
||||
"恐惧": "afraid",
|
||||
"反感": "disgusted",
|
||||
"低落": "melancholic",
|
||||
"惊讶": "surprised",
|
||||
"自然": "calm",
|
||||
}
|
||||
emo.desired_vector_order = list(emo.cn_key_to_en)
|
||||
emo.max_score = 1.2
|
||||
emo.min_score = 0.0
|
||||
|
||||
assert emo.convert({"自然": "自然"}) == {
|
||||
"happy": 0.0,
|
||||
"angry": 0.0,
|
||||
"sad": 0.0,
|
||||
"afraid": 0.0,
|
||||
"disgusted": 0.0,
|
||||
"melancholic": 0.0,
|
||||
"surprised": 0.0,
|
||||
"calm": 1.0,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("module_name", ["indextts.infer_v2", "indextts.infer_v2_5"])
|
||||
def test_qwen_emotion_convert_accepts_label_only_payload(module_name, monkeypatch):
|
||||
module = _load_qwen_emotion_module(module_name, monkeypatch)
|
||||
|
||||
emo = module.QwenEmotion.__new__(module.QwenEmotion)
|
||||
emo.cn_key_to_en = {
|
||||
"高兴": "happy",
|
||||
"愤怒": "angry",
|
||||
"悲伤": "sad",
|
||||
"恐惧": "afraid",
|
||||
"反感": "disgusted",
|
||||
"低落": "melancholic",
|
||||
"惊讶": "surprised",
|
||||
"自然": "calm",
|
||||
}
|
||||
emo.desired_vector_order = list(emo.cn_key_to_en)
|
||||
emo.max_score = 1.2
|
||||
emo.min_score = 0.0
|
||||
|
||||
assert emo.convert({"emotion": "自然"})["calm"] == 1.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("module_name", ["indextts.infer_v2", "indextts.infer_v2_5"])
|
||||
def test_qwen_emotion_convert_redirects_cross_key_labels(module_name, monkeypatch):
|
||||
module = _load_qwen_emotion_module(module_name, monkeypatch)
|
||||
|
||||
emo = module.QwenEmotion.__new__(module.QwenEmotion)
|
||||
emo.cn_key_to_en = {
|
||||
"高兴": "happy",
|
||||
"愤怒": "angry",
|
||||
"悲伤": "sad",
|
||||
"恐惧": "afraid",
|
||||
"反感": "disgusted",
|
||||
"低落": "melancholic",
|
||||
"惊讶": "surprised",
|
||||
"自然": "calm",
|
||||
}
|
||||
emo.desired_vector_order = list(emo.cn_key_to_en)
|
||||
emo.max_score = 1.2
|
||||
emo.min_score = 0.0
|
||||
|
||||
emotion_dict = emo.convert({"高兴": "自然"})
|
||||
assert emotion_dict["happy"] == 0.0
|
||||
assert emotion_dict["calm"] == 1.0
|
||||
|
||||
|
||||
# -- Inference (GPU required) --------------------------------------------------
|
||||
|
||||
INFER_TEXTS = [
|
||||
|
||||
Reference in New Issue
Block a user