Files
diodiogod-TTS-Audio-Suite/tests/unit/test_fish_audio_s2_processor.py

637 lines
20 KiB
Python

import importlib.util
from pathlib import Path
import sys
from types import SimpleNamespace
import pytest
import torch
REPO_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO_ROOT))
PROCESSOR_PATH = (
REPO_ROOT
/ "nodes"
/ "fish_audio_s2"
/ "fish_audio_s2_processor.py"
)
SPEC = importlib.util.spec_from_file_location("fish_audio_s2_processor_test_module", PROCESSOR_PATH)
PROCESSOR_MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(PROCESSOR_MODULE)
class FakeCharacterParser:
def set_available_characters(self, _characters):
pass
def set_character_language_default(self, _character, _language):
pass
def reset_session_cache(self):
pass
def parse_text_segments(self, text, engine_type=None):
characters = ["alice", "bob", "rick"] if text == "all" else ["bob", "rick"]
return [
SimpleNamespace(character=character, text=character.title(), parameters={}, language="en")
for character in characters
]
class CapturingAdapter:
def __init__(self):
self.calls = []
def update_config(self, _config):
pass
def generate_dialogue(self, turns, voice_refs, **_kwargs):
self.calls.append((list(turns), list(voice_refs)))
return torch.zeros(1, 16)
@pytest.mark.unit
def test_late_global_character_uses_local_reference_order(monkeypatch):
parser = FakeCharacterParser()
def parse_subtitle_three(text, engine_type=None):
if text == "subtitle3":
return [
SimpleNamespace(character="male_01", text="Bob line", parameters={}, language="en"),
SimpleNamespace(character="narrator", text="Narrator line", parameters={}, language="en"),
]
return [
SimpleNamespace(character="narrator", text="Narrator", parameters={}, language="en"),
SimpleNamespace(character="female_01", text="Alice", parameters={}, language="en"),
SimpleNamespace(character="clint_eastwood cc3 (enhanced2)", text="Cowboy", parameters={}, language="en"),
SimpleNamespace(character="male_01", text="Bob", parameters={}, language="en"),
]
parser.parse_text_segments = parse_subtitle_three
monkeypatch.setattr(PROCESSOR_MODULE, "character_parser", parser)
monkeypatch.setattr(PROCESSOR_MODULE, "get_available_characters", lambda: [])
monkeypatch.setattr(
PROCESSOR_MODULE,
"get_character_mapping",
lambda *_args, **_kwargs: {"male_01": ("male_01.wav", "Bob reference")},
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"model_management",
SimpleNamespace(interrupt_processing=False),
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"voice_discovery",
SimpleNamespace(
get_character_aliases=lambda: {},
get_character_language_defaults=lambda: {},
),
)
adapter = CapturingAdapter()
processor = PROCESSOR_MODULE.FishAudioS2Processor(
adapter,
{
"speaker_references": [
{"character_name": "Ana"},
{"character_name": "Tony"},
],
"multi_speaker_mode": "Native Multi-Speaker",
"language_prompting": "Off",
},
)
reference_order = processor.get_character_order("all")
processor.process_text(
"subtitle3",
{"narrator": {"character_name": "Joe"}},
seed=1,
reference_order=reference_order,
show_text_logging=False,
)
turns, used_references = adapter.calls[0]
assert turns == [(0, "Bob line"), (1, "Narrator line")]
assert used_references == [
{"audio_path": "male_01.wav", "reference_text": "Bob reference"},
{"character_name": "Joe"},
]
@pytest.mark.unit
def test_srt_uses_global_reference_lookup_with_local_speaker_ids(monkeypatch):
parser = FakeCharacterParser()
monkeypatch.setattr(PROCESSOR_MODULE, "character_parser", parser)
monkeypatch.setattr(PROCESSOR_MODULE, "get_available_characters", lambda: [])
monkeypatch.setattr(PROCESSOR_MODULE, "get_character_mapping", lambda *_args, **_kwargs: {})
monkeypatch.setattr(
PROCESSOR_MODULE,
"model_management",
SimpleNamespace(interrupt_processing=False),
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"voice_discovery",
SimpleNamespace(
get_character_aliases=lambda: {},
get_character_language_defaults=lambda: {},
),
)
adapter = CapturingAdapter()
references = [{"id": "bob"}, {"id": "rick"}]
processor = PROCESSOR_MODULE.FishAudioS2Processor(
adapter,
{"speaker_references": references},
)
reference_order = processor.get_character_order("all")
processor.process_text("subtitle", {}, seed=1, reference_order=reference_order)
turns, used_references = adapter.calls[0]
assert turns == [(0, "Bob"), (1, "Rick")]
assert [reference["id"] for reference in used_references] == ["bob", "rick"]
@pytest.mark.unit
def test_custom_switching_generates_each_segment_as_local_speaker_zero(monkeypatch):
parser = FakeCharacterParser()
monkeypatch.setattr(PROCESSOR_MODULE, "character_parser", parser)
monkeypatch.setattr(PROCESSOR_MODULE, "get_available_characters", lambda: [])
monkeypatch.setattr(PROCESSOR_MODULE, "get_character_mapping", lambda *_args, **_kwargs: {})
monkeypatch.setattr(
PROCESSOR_MODULE,
"model_management",
SimpleNamespace(interrupt_processing=False),
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"voice_discovery",
SimpleNamespace(
get_character_aliases=lambda: {},
get_character_language_defaults=lambda: {},
),
)
adapter = CapturingAdapter()
references = [{"id": "bob"}, {"id": "rick"}]
processor = PROCESSOR_MODULE.FishAudioS2Processor(
adapter,
{
"speaker_references": references,
"multi_speaker_mode": "Custom Character Switching",
},
)
reference_order = processor.get_character_order("all")
records = processor.process_text(
"subtitle", {}, seed=1, reference_order=reference_order
)
assert [call[0] for call in adapter.calls] == [[(0, "Bob")], [(0, "Rick")]]
assert [[ref["id"] for ref in call[1]] for call in adapter.calls] == [["bob"], ["rick"]]
assert [record["text"] for record in records] == ["Bob", "Rick"]
@pytest.mark.unit
def test_language_prompting_prepends_shared_display_name(monkeypatch):
parser = FakeCharacterParser()
def parse_with_language(_text, engine_type=None):
assert engine_type == "fish_audio_s2"
return [
SimpleNamespace(character="alice", text="Hallo", parameters={}, language="de", explicit_language=False),
]
parser.parse_text_segments = parse_with_language
monkeypatch.setattr(PROCESSOR_MODULE, "character_parser", parser)
monkeypatch.setattr(PROCESSOR_MODULE, "get_available_characters", lambda: [])
monkeypatch.setattr(PROCESSOR_MODULE, "get_character_mapping", lambda *_args, **_kwargs: {})
monkeypatch.setattr(
PROCESSOR_MODULE,
"model_management",
SimpleNamespace(interrupt_processing=False),
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"voice_discovery",
SimpleNamespace(
get_character_aliases=lambda: {},
get_character_language_defaults=lambda: {},
),
)
adapter = CapturingAdapter()
processor = PROCESSOR_MODULE.FishAudioS2Processor(
adapter,
{
"speaker_references": [{"id": "alice"}],
"multi_speaker_mode": "Custom Character Switching",
"language_prompting": "Auto Inline Tag",
},
)
processor.process_text("subtitle", {}, seed=1, reference_order=["alice"], show_text_logging=False)
turns, _used_references = adapter.calls[0]
assert turns == [(0, "<German> Hallo")]
@pytest.mark.unit
def test_explicit_english_language_prompt_is_preserved(monkeypatch):
parser = FakeCharacterParser()
def parse_with_explicit_english(_text, engine_type=None):
assert engine_type == "fish_audio_s2"
return [
SimpleNamespace(character="alice", text="Hello", parameters={}, language="en", explicit_language=True),
]
parser.parse_text_segments = parse_with_explicit_english
monkeypatch.setattr(PROCESSOR_MODULE, "character_parser", parser)
monkeypatch.setattr(PROCESSOR_MODULE, "get_available_characters", lambda: [])
monkeypatch.setattr(PROCESSOR_MODULE, "get_character_mapping", lambda *_args, **_kwargs: {})
monkeypatch.setattr(
PROCESSOR_MODULE,
"model_management",
SimpleNamespace(interrupt_processing=False),
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"voice_discovery",
SimpleNamespace(
get_character_aliases=lambda: {},
get_character_language_defaults=lambda: {},
),
)
adapter = CapturingAdapter()
processor = PROCESSOR_MODULE.FishAudioS2Processor(
adapter,
{
"speaker_references": [{"id": "alice"}],
"multi_speaker_mode": "Custom Character Switching",
"language_prompting": "Auto Inline Tag",
},
)
processor.process_text("subtitle", {}, seed=1, reference_order=["alice"], show_text_logging=False)
turns, _used_references = adapter.calls[0]
assert turns == [(0, "<English> Hello")]
@pytest.mark.unit
def test_custom_override_logs_connected_voice_and_does_not_inherit_alias_language(monkeypatch, capsys):
parser = FakeCharacterParser()
def parse_bob(_text, engine_type=None):
assert engine_type == "fish_audio_s2"
return [
SimpleNamespace(character="bob", text="Bonjour", parameters={}, language="fr"),
]
parser.parse_text_segments = parse_bob
monkeypatch.setattr(PROCESSOR_MODULE, "character_parser", parser)
monkeypatch.setattr(PROCESSOR_MODULE, "get_available_characters", lambda: [])
monkeypatch.setattr(
PROCESSOR_MODULE,
"get_character_mapping",
lambda *_args, **_kwargs: {"bob": ("bob.wav", "Bob ref")},
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"model_management",
SimpleNamespace(interrupt_processing=False),
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"voice_discovery",
SimpleNamespace(
get_character_aliases=lambda: {},
get_character_language_defaults=lambda: {},
),
)
adapter = CapturingAdapter()
processor = PROCESSOR_MODULE.FishAudioS2Processor(
adapter,
{
"speaker_references": [{"id": "tony", "character_name": "Tony"}],
"multi_speaker_mode": "Custom Character Switching",
"language_prompting": "Auto Inline Tag",
},
)
processor.process_text(
"subtitle",
{"narrator": {"audio_path": "joe.wav", "character_name": "Joe"}},
seed=1,
reference_order=["narrator", "bob"],
)
output = capsys.readouterr().out
assert "Speaker 2 input overrides ['bob'] alias" in output
assert "[Tony] Bonjour" in output
assert "<French>" not in output
@pytest.mark.unit
def test_single_character_native_call_uses_narrator_as_speaker_one_when_no_global_narrator_exists(monkeypatch):
parser = FakeCharacterParser()
def parse_single_bob(_text, engine_type=None):
assert engine_type == "fish_audio_s2"
return [
SimpleNamespace(character="bob", text="Bonjour", parameters={}, language="fr", explicit_language=False),
]
parser.parse_text_segments = parse_single_bob
monkeypatch.setattr(PROCESSOR_MODULE, "character_parser", parser)
monkeypatch.setattr(PROCESSOR_MODULE, "get_available_characters", lambda: [])
monkeypatch.setattr(
PROCESSOR_MODULE,
"get_character_mapping",
lambda *_args, **_kwargs: {"bob": ("bob.wav", "Bob ref")},
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"model_management",
SimpleNamespace(interrupt_processing=False),
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"voice_discovery",
SimpleNamespace(
get_character_aliases=lambda: {},
get_character_language_defaults=lambda: {},
),
)
adapter = CapturingAdapter()
processor = PROCESSOR_MODULE.FishAudioS2Processor(
adapter,
{
"speaker_references": [{"id": "speaker1_female"}],
"multi_speaker_mode": "Native Multi-Speaker",
"language_prompting": "Off",
},
)
processor.process_text(
"subtitle",
{"narrator": {"id": "narrator_voice"}},
seed=1,
reference_order=["bob"],
show_text_logging=False,
)
turns, used_references = adapter.calls[0]
assert turns == [(0, "Bonjour")]
assert used_references == [{"id": "narrator_voice"}]
@pytest.mark.unit
def test_first_tagged_character_uses_speaker2_when_global_narrator_exists(monkeypatch):
parser = FakeCharacterParser()
def parse_single_bob(_text, engine_type=None):
assert engine_type == "fish_audio_s2"
return [
SimpleNamespace(character="bob", text="Bonjour", parameters={}, language="fr", explicit_language=False),
]
parser.parse_text_segments = parse_single_bob
monkeypatch.setattr(PROCESSOR_MODULE, "character_parser", parser)
monkeypatch.setattr(PROCESSOR_MODULE, "get_available_characters", lambda: [])
monkeypatch.setattr(
PROCESSOR_MODULE,
"get_character_mapping",
lambda *_args, **_kwargs: {"bob": ("bob.wav", "Bob ref")},
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"model_management",
SimpleNamespace(interrupt_processing=False),
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"voice_discovery",
SimpleNamespace(
get_character_aliases=lambda: {},
get_character_language_defaults=lambda: {},
),
)
adapter = CapturingAdapter()
processor = PROCESSOR_MODULE.FishAudioS2Processor(
adapter,
{
"speaker_references": [{"id": "speaker2_voice"}],
"multi_speaker_mode": "Native Multi-Speaker",
"language_prompting": "Off",
},
)
processor.process_text(
"subtitle",
{"narrator": {"id": "narrator_voice"}},
seed=1,
reference_order=["narrator", "bob"],
show_text_logging=False,
)
turns, used_references = adapter.calls[0]
assert turns == [(0, "Bonjour")]
assert used_references == [{"id": "speaker2_voice"}]
@pytest.mark.unit
def test_second_character_uses_speaker2_override_when_narrator_not_reserved(monkeypatch):
parser = FakeCharacterParser()
def parse_bob_then_rick(_text, engine_type=None):
assert engine_type == "fish_audio_s2"
return [
SimpleNamespace(character="bob", text="Bonjour", parameters={}, language="fr", explicit_language=False),
SimpleNamespace(character="rick", text="Salut", parameters={}, language="fr", explicit_language=False),
]
parser.parse_text_segments = parse_bob_then_rick
monkeypatch.setattr(PROCESSOR_MODULE, "character_parser", parser)
monkeypatch.setattr(PROCESSOR_MODULE, "get_available_characters", lambda: [])
monkeypatch.setattr(
PROCESSOR_MODULE,
"get_character_mapping",
lambda *_args, **_kwargs: {"bob": ("bob.wav", "Bob ref"), "rick": ("rick.wav", "Rick ref")},
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"model_management",
SimpleNamespace(interrupt_processing=False),
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"voice_discovery",
SimpleNamespace(
get_character_aliases=lambda: {},
get_character_language_defaults=lambda: {},
),
)
adapter = CapturingAdapter()
processor = PROCESSOR_MODULE.FishAudioS2Processor(
adapter,
{
"speaker_references": [{"id": "speaker2_voice"}],
"multi_speaker_mode": "Native Multi-Speaker",
"language_prompting": "Off",
},
)
processor.process_text(
"subtitle",
{"narrator": {"id": "narrator_voice"}},
seed=1,
reference_order=["bob", "rick"],
show_text_logging=False,
)
turns, used_references = adapter.calls[0]
assert turns == [(0, "Bonjour"), (1, "Salut")]
assert used_references == [{"id": "narrator_voice"}, {"id": "speaker2_voice"}]
@pytest.mark.unit
def test_native_override_uses_effective_speaker_default_language_for_auto_inline_tag(monkeypatch):
parser = FakeCharacterParser()
def parse_single_bob(_text, engine_type=None):
assert engine_type == "fish_audio_s2"
return [
SimpleNamespace(
character="male_01",
original_character="Bob",
text="Bonjour",
parameters={},
language="fr",
explicit_language=False,
),
]
parser.parse_text_segments = parse_single_bob
monkeypatch.setattr(PROCESSOR_MODULE, "character_parser", parser)
monkeypatch.setattr(PROCESSOR_MODULE, "get_available_characters", lambda: [])
monkeypatch.setattr(
PROCESSOR_MODULE,
"get_character_mapping",
lambda *_args, **_kwargs: {"male_01": ("bob.wav", "Bob ref")},
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"get_character_default_language",
lambda character: {"anna": "de", "male_01": "fr"}.get(character.lower()),
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"model_management",
SimpleNamespace(interrupt_processing=False),
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"voice_discovery",
SimpleNamespace(
get_character_aliases=lambda: {},
get_character_language_defaults=lambda: {},
),
)
adapter = CapturingAdapter()
processor = PROCESSOR_MODULE.FishAudioS2Processor(
adapter,
{
"speaker_references": [{"character_name": "Anna"}],
"multi_speaker_mode": "Native Multi-Speaker",
"language_prompting": "Auto Inline Tag",
},
)
processor.process_text(
"subtitle",
{"narrator": {"character_name": "Joe"}},
seed=1,
reference_order=["narrator", "male_01"],
show_text_logging=False,
)
turns, used_references = adapter.calls[0]
assert turns == [(0, "<German> Bonjour")]
assert used_references == [{"character_name": "Anna"}]
@pytest.mark.unit
def test_native_override_with_unmapped_connected_voice_does_not_fall_back_to_alias_language(monkeypatch):
parser = FakeCharacterParser()
def parse_single_bob(_text, engine_type=None):
assert engine_type == "fish_audio_s2"
return [
SimpleNamespace(
character="male_01",
original_character="Bob",
text="Bonjour",
parameters={},
language="fr",
explicit_language=False,
),
]
parser.parse_text_segments = parse_single_bob
monkeypatch.setattr(PROCESSOR_MODULE, "character_parser", parser)
monkeypatch.setattr(PROCESSOR_MODULE, "get_available_characters", lambda: [])
monkeypatch.setattr(
PROCESSOR_MODULE,
"get_character_mapping",
lambda *_args, **_kwargs: {"male_01": ("bob.wav", "Bob ref")},
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"get_character_default_language",
lambda character: None,
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"model_management",
SimpleNamespace(interrupt_processing=False),
)
monkeypatch.setattr(
PROCESSOR_MODULE,
"voice_discovery",
SimpleNamespace(
get_character_aliases=lambda: {},
get_character_language_defaults=lambda: {},
),
)
adapter = CapturingAdapter()
processor = PROCESSOR_MODULE.FishAudioS2Processor(
adapter,
{
"speaker_references": [{"character_name": "Tony"}],
"multi_speaker_mode": "Native Multi-Speaker",
"language_prompting": "Auto Inline Tag",
},
)
processor.process_text(
"subtitle",
{"narrator": {"character_name": "Joe"}},
seed=1,
reference_order=["narrator", "male_01"],
show_text_logging=False,
)
turns, used_references = adapter.calls[0]
assert turns == [(0, "Bonjour")]
assert used_references == [{"character_name": "Tony"}]