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:
Copilot
2026-08-12 16:27:22 +08:00
committed by GitHub
parent 8b6ecba1f7
commit 8887408e85
3 changed files with 208 additions and 0 deletions
+39
View File
@@ -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
+39
View File
@@ -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
+130
View File
@@ -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 = [