Skip to content

Commit cb23b3f

Browse files
committed
Fix WanGP media routing and model controls
1 parent 5ea9a36 commit cb23b3f

3 files changed

Lines changed: 163 additions & 12 deletions

File tree

Dockerfile.combined

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ ARG CORE_IMAGE=ghcr.io/nodetool-ai/nodetool:latest
55
FROM ${CORE_IMAGE}
66

77
ARG NODETOOL_CORE_REPOSITORY=https://github.com/nodetool-ai/nodetool-core.git
8-
ARG NODETOOL_CORE_COMMIT=a7938288e894af65764287e626ea8d0924bf954a
8+
ARG NODETOOL_CORE_COMMIT=2f5c2519d9e46b38975c349c8467e41e053ed0e8
99
ARG WANGP_REPOSITORY=https://github.com/deepbeepmeep/Wan2GP.git
1010
ARG WANGP_COMMIT=362c3467a70e1136ceb52eec95907205a8f88543
1111
ARG MMGP_REPOSITORY=https://github.com/deepbeepmeep/mmgp.git
@@ -15,7 +15,7 @@ LABEL org.opencontainers.image.title="NodeTool WanGP non-commercial GPU worker"
1515
org.opencontainers.image.description="Authenticated direct-Python WanGP provider worker; free non-monetized evaluation only" \
1616
org.opencontainers.image.licenses="AGPL-3.0-or-later AND LicenseRef-WanGP-Community-2.0 AND LicenseRef-mmGP-NonCommercial" \
1717
org.opencontainers.image.source="https://github.com/nodetool-ai/nodetool-wan2gp" \
18-
io.nodetool.core.commit="a7938288e894af65764287e626ea8d0924bf954a" \
18+
io.nodetool.core.commit="2f5c2519d9e46b38975c349c8467e41e053ed0e8" \
1919
io.nodetool.wangp.commit="362c3467a70e1136ceb52eec95907205a8f88543" \
2020
io.nodetool.mmgp.commit="589ba050d320c4df879f48d11b23d6a88e6c68c8" \
2121
io.nodetool.commercial-use="prohibited-without-separate-written-license"
@@ -89,7 +89,7 @@ ENV WANGP_CONFIG_DIR=/workspace/wan2gp/config \
8989
WANGP_ROOT=/opt/Wan2GP \
9090
NODETOOL_MODEL_PREPARE_COMMAND_WANGP="/opt/wan2gp-venv/bin/python /opt/nodetool-wan2gp-combined/prepare_model.py" \
9191
NODETOOL_PROVIDER_ADAPTER_COMMAND_WANGP="/opt/wan2gp-venv/bin/python /opt/nodetool-wan2gp-combined/provider_adapter.py" \
92-
NODETOOL_PROVIDER_ADAPTER_CAPABILITIES_WANGP="text_to_video,image_to_video,text_to_image,image_to_image,text_to_audio,text_to_speech" \
92+
NODETOOL_PROVIDER_ADAPTER_CAPABILITIES_WANGP="text_to_video,image_to_video,text_to_image,image_to_image,text_to_audio,text_to_speech_encoded" \
9393
NODETOOL_PROVIDER_ADAPTER_DISPLAY_NAME_WANGP="WanGP" \
9494
HF_HOME=/workspace/cache/huggingface \
9595
PYTHONUNBUFFERED=1 \

combined/provider_adapter.py

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ def _is_music(metadata: dict[str, Any]) -> bool:
9393
return str(metadata.get("family") or "").casefold() == "music"
9494

9595

96+
def _choice_values(definition: Any) -> list[str]:
97+
if not isinstance(definition, dict):
98+
return []
99+
values: list[str] = []
100+
for choice in definition.get("choices", []):
101+
value = choice.get("value") if isinstance(choice, dict) else choice
102+
if value is not None and str(value):
103+
values.append(str(value))
104+
return values
105+
106+
96107
def _models(session: Any, model_kind: str) -> list[dict[str, Any]]:
97108
"""Translate WanGP model metadata into NodeTool provider models."""
98109
if model_kind not in {"video", "image", "tts", "music"}:
@@ -127,7 +138,42 @@ def _models(session: Any, model_kind: str) -> list[dict[str, Any]]:
127138
"provider": "wangp",
128139
}
129140
if model_kind == "tts":
130-
model["capabilities"] = supported
141+
base_model_type = str(metadata.get("base_model_type") or model_type)
142+
tts_capabilities = list(supported)
143+
media_inputs = metadata.get("media_inputs")
144+
audio_inputs = (
145+
media_inputs.get("audio", {}) if isinstance(media_inputs, dict) else {}
146+
)
147+
if audio_inputs.get("prompt"):
148+
tts_capabilities.append("voice_cloning")
149+
if base_model_type in {"qwen3_tts_base", "omnivoice"}:
150+
tts_capabilities.append("reference_transcript")
151+
if base_model_type in {
152+
"qwen3_tts_customvoice",
153+
"index_tts2",
154+
"index_tts25",
155+
}:
156+
tts_capabilities.append("instruction_control")
157+
if base_model_type in {"qwen3_tts_voicedesign", "omnivoice"}:
158+
tts_capabilities.append("voice_design")
159+
160+
setting_values = metadata.get("setting_values")
161+
model_mode = (
162+
setting_values.get("model_mode")
163+
if isinstance(setting_values, dict)
164+
else None
165+
)
166+
mode_label = str(
167+
model_mode.get("label", "") if isinstance(model_mode, dict) else ""
168+
).casefold()
169+
mode_values = _choice_values(model_mode)
170+
if mode_label == "speaker":
171+
tts_capabilities.append("preset_voice")
172+
model["voices"] = mode_values
173+
elif mode_label == "language":
174+
tts_capabilities.append("language_selection")
175+
model["languages"] = mode_values
176+
model["capabilities"] = list(dict.fromkeys(tts_capabilities))
131177
else:
132178
model["supportedTasks"] = supported
133179
models.append(model)
@@ -205,6 +251,9 @@ def _settings(
205251
elif params.get("durationSeconds") is not None and operation.endswith("video"):
206252
settings["video_length"] = f"{float(params['durationSeconds']):g}s"
207253

254+
if operation in {"text_to_image", "image_to_image"}:
255+
settings["image_mode"] = 1
256+
208257
if operation in {"image_to_image", "image_to_video"}:
209258
image_path = str(_input_path(request, "image_path"))
210259
image_inputs = (metadata or {}).get("media_inputs", {}).get("image", {})
@@ -226,8 +275,12 @@ def _settings(
226275
if operation == "text_to_audio":
227276
style_prompt = str(params.get("prompt") or "")
228277
lyrics = str(params.get("lyrics") or "").strip()
229-
settings["prompt"] = lyrics or "[Instrumental]"
230-
settings["alt_prompt"] = style_prompt
278+
base_model_type = str((metadata or {}).get("base_model_type") or model_type)
279+
if base_model_type.startswith("stable_audio3"):
280+
settings["prompt"] = style_prompt
281+
else:
282+
settings["prompt"] = lyrics or "[Instrumental]"
283+
settings["alt_prompt"] = style_prompt
231284
if params.get("durationSeconds") is not None:
232285
settings["duration_seconds"] = float(params["durationSeconds"])
233286

@@ -236,11 +289,16 @@ def _settings(
236289
settings["alt_prompt"] = str(params["referenceText"])
237290
elif params.get("instructions") is not None:
238291
settings["alt_prompt"] = str(params["instructions"])
239-
model_mode = params.get("voice") or params.get("language")
292+
base_model_type = str((metadata or {}).get("base_model_type") or model_type)
293+
model_mode = (
294+
params.get("voice")
295+
if base_model_type == "qwen3_tts_customvoice"
296+
else params.get("language")
297+
)
240298
if model_mode:
241299
settings["model_mode"] = str(model_mode)
242-
if params.get("speed") is not None:
243-
settings["speech_speed"] = float(params["speed"])
300+
if params.get("speed") is not None and base_model_type == "index_tts25":
301+
settings["custom_settings"] = {"speech_speed": float(params["speed"])}
244302
if request.get("reference_audio_path"):
245303
settings["audio_guide"] = str(
246304
_input_path(request, "reference_audio_path")
@@ -276,9 +334,18 @@ def _generated_path(result: Any, media_type: str | None = None) -> str:
276334
path = getattr(artifact, "path", None)
277335
if path and Path(path).is_file():
278336
return str(Path(path).resolve())
337+
media_suffixes = {
338+
"image": {".bmp", ".gif", ".jpeg", ".jpg", ".png", ".tif", ".tiff", ".webp"},
339+
"video": {".avi", ".mkv", ".mov", ".mp4", ".webm"},
340+
"audio": {".aac", ".flac", ".m4a", ".mp3", ".ogg", ".opus", ".wav"},
341+
}
342+
expected_suffixes = media_suffixes.get(media_type or "")
279343
for path in getattr(result, "generated_files", ()):
280344
candidate = Path(str(path))
281-
if candidate.is_file():
345+
if candidate.is_file() and (
346+
expected_suffixes is None
347+
or candidate.suffix.casefold() in expected_suffixes
348+
):
282349
return str(candidate.resolve())
283350
raise RuntimeError("WanGP completed without a generated media file")
284351

tests/test_provider_adapter.py

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,17 @@ def test_models_exposes_image_tts_and_music_separately() -> None:
7373
"family": "tts",
7474
"main_output": ["audio"],
7575
"capabilities": {"text_to_audio": True},
76+
"base_model_type": "qwen3_tts_base",
77+
"media_inputs": {"audio": {"prompt": True}},
78+
"setting_values": {
79+
"model_mode": {
80+
"label": "Language",
81+
"choices": [
82+
{"label": "English", "value": "English"},
83+
{"label": "Auto", "value": "auto"},
84+
],
85+
}
86+
},
7687
},
7788
{
7889
"model_type": "ace_step",
@@ -97,7 +108,13 @@ def test_models_exposes_image_tts_and_music_separately() -> None:
97108
"id": "qwen3_tts",
98109
"name": "Qwen3 TTS",
99110
"provider": "wangp",
100-
"capabilities": ["text_to_speech"],
111+
"languages": ["English", "auto"],
112+
"capabilities": [
113+
"text_to_speech",
114+
"voice_cloning",
115+
"reference_transcript",
116+
"language_selection",
117+
],
101118
}
102119
]
103120
assert _models(session, "music") == [
@@ -176,6 +193,17 @@ def test_image_to_image_uses_reference_input_when_required(tmp_path: Path) -> No
176193
assert settings["image_refs"] == [str(image.resolve())]
177194
assert settings["resolution"] == "1024x768"
178195
assert settings["denoising_strength"] == 0.65
196+
assert settings["image_mode"] == 1
197+
198+
199+
def test_text_to_image_selects_image_output_for_dual_output_models() -> None:
200+
settings = _settings(
201+
{
202+
"operation": "text_to_image",
203+
"params": {"model": "t2v_2_2", "prompt": "sunrise"},
204+
}
205+
)
206+
assert settings["image_mode"] == 1
179207

180208

181209
def test_image_to_image_uses_model_control_mode(tmp_path: Path) -> None:
@@ -226,6 +254,23 @@ def test_music_settings_map_lyrics_style_and_duration() -> None:
226254
}
227255

228256

257+
def test_stable_audio_uses_description_as_its_main_prompt() -> None:
258+
assert _settings(
259+
{
260+
"operation": "text_to_audio",
261+
"params": {
262+
"model": "stable_audio3_small",
263+
"prompt": "soft rain and distant thunder",
264+
"lyrics": "ignored lyrics",
265+
},
266+
},
267+
{"base_model_type": "stable_audio3_small"},
268+
) == {
269+
"model_type": "stable_audio3_small",
270+
"prompt": "soft rain and distant thunder",
271+
}
272+
273+
229274
def test_tts_settings_map_voice_clone_fields(tmp_path: Path) -> None:
230275
audio = tmp_path / "voice.wav"
231276
audio.write_bytes(b"wav")
@@ -246,17 +291,56 @@ def test_tts_settings_map_voice_clone_fields(tmp_path: Path) -> None:
246291
"prompt": "Hello",
247292
"alt_prompt": "Reference words",
248293
"model_mode": "English",
249-
"speech_speed": 1.1,
250294
"audio_guide": str(audio.resolve()),
251295
}
252296

253297

298+
def test_tts_uses_voice_only_for_custom_voice_and_speed_only_for_index25() -> None:
299+
custom_voice = _settings(
300+
{
301+
"operation": "tts_encoded",
302+
"params": {
303+
"model": "qwen3_tts_customvoice",
304+
"text": "Hello",
305+
"voice": "Ryan",
306+
"language": "English",
307+
},
308+
},
309+
{"base_model_type": "qwen3_tts_customvoice"},
310+
)
311+
assert custom_voice["model_mode"] == "Ryan"
312+
313+
index = _settings(
314+
{
315+
"operation": "tts_encoded",
316+
"params": {
317+
"model": "index_tts25",
318+
"text": "Hello",
319+
"voice": "unused",
320+
"language": "EN",
321+
"speed": 1.25,
322+
},
323+
},
324+
{"base_model_type": "index_tts25"},
325+
)
326+
assert index["model_mode"] == "EN"
327+
assert index["custom_settings"] == {"speech_speed": 1.25}
328+
329+
254330
def test_generated_path_requires_successful_existing_file(tmp_path: Path) -> None:
255331
video = tmp_path / "output.mp4"
256332
video.write_bytes(b"video")
257333
result = SimpleNamespace(success=True, generated_files=[str(video)], artifacts=[])
258334
assert _generated_path(result, "video") == str(video.resolve())
259335

336+
image = tmp_path / "wrong.png"
337+
image.write_bytes(b"image")
338+
with pytest.raises(RuntimeError, match="without a generated media file"):
339+
_generated_path(
340+
SimpleNamespace(success=True, generated_files=[str(image)], artifacts=[]),
341+
"video",
342+
)
343+
260344
with pytest.raises(RuntimeError, match="denied"):
261345
_generated_path(
262346
SimpleNamespace(

0 commit comments

Comments
 (0)