Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
41494d4
Constrain speech prompts and split validation helpers
IAnMove Sep 9, 2026
11e26a0
feat(studio): share durable speech commands across UI Wizard and MCP
IAnMove Sep 9, 2026
1efe288
Merge commit '57c980caf2e270009feaf18ee599780d8db27a9d' into feat/sha…
IAnMove Sep 9, 2026
4d5c2ac
Merge commit 'b0fc2dd3525229dfff1fba5d3465053df0454fd5' into feat/sha…
IAnMove Sep 9, 2026
8889286
feat: add shared tools upscale command contract
IAnMove Sep 9, 2026
40365d5
Merge reviewed image recovery folder fix into speech candidate
IAnMove Sep 9, 2026
af2ce04
feat(tools): admit upscale through canonical commands and native worker
IAnMove Sep 9, 2026
4b80875
Merge reviewed recovery and speech candidate into Tools commands
IAnMove Sep 9, 2026
ba974fb
refactor(tools): separate source projection from source validation
IAnMove Sep 9, 2026
9a157fe
fix(tools): identify durable upscale tasks in Activity
IAnMove Sep 9, 2026
1868f10
fix(tools): keep inactive processor defaults out of native requests
IAnMove Sep 9, 2026
98a585c
docs: explain Wizard and external MCP command recovery
IAnMove Sep 9, 2026
0ce4985
fix(tools): scope upscale assets by source workspace
IAnMove Sep 9, 2026
2615bb7
fix(tools): avoid global asset discovery for scoped file URLs
IAnMove Sep 9, 2026
1651fc6
feat(tools): add durable upscale UI command
IAnMove Sep 9, 2026
3e2ac38
feat(commands): add local studio music contract
IAnMove Sep 9, 2026
aa42e9b
fix(tools): guard workspace and seed snapshots
IAnMove Sep 9, 2026
7b348d7
feat(music): admit local Studio music through shared native commands
IAnMove Sep 9, 2026
d50cb1e
fix(tools): route quick upscale through durable panel
IAnMove Sep 9, 2026
3df3d81
Merge remote-tracking branch 'origin/development' into feat/shared-to…
IAnMove Sep 9, 2026
ccb9b5e
Merge commit '7b348d78' into feat/shared-tool-commands
IAnMove Sep 9, 2026
d62cd30
fix(commands): retain all native operations after integrating music
IAnMove Sep 9, 2026
d8fbec2
feat(ui): add Studio music command client
IAnMove Sep 9, 2026
667e42c
fix(music): keep catalog input fields in sync
IAnMove Sep 9, 2026
f308df2
feat(studio): present and recover native music requests
IAnMove Sep 9, 2026
87d3343
Merge commit 'd62cd30e' into feat/shared-music-commands
IAnMove Sep 9, 2026
e1ba86c
test(e2e): cover Tools upscale command gateway
IAnMove Sep 9, 2026
91b952a
test(music): cover Studio ACK and store defaults
IAnMove Sep 9, 2026
c0ce5e1
perf(ui): lazy load Tools sidebar panel
IAnMove Sep 9, 2026
246cb10
Merge commit '91b952a2' into feat/shared-sfx-commands
IAnMove Sep 9, 2026
c87aa96
fix(studio): honor native music duration bounds
IAnMove Sep 9, 2026
5ae350f
fix(studio): retain native duration fallback
IAnMove Sep 9, 2026
a9c27d9
fix(wizard): preserve native music form inputs
IAnMove Sep 9, 2026
6e181ff
Merge commit 'c0ce5e10' into feat/shared-music-commands
IAnMove Sep 9, 2026
4d3b2d7
docs(commands): describe shared Studio music workflow
IAnMove Sep 9, 2026
29a802b
feat(studio): add closed SFX command contract
IAnMove Sep 9, 2026
5265113
fix(wizard): reject invalid native music controls
IAnMove Sep 9, 2026
b30a573
style(studio): normalize shared audio helper endings
IAnMove Sep 9, 2026
755b006
test: cover SFX generation command presentation
IAnMove Sep 9, 2026
6f6b154
feat(studio): execute SFX through shared commands and visible receipts
IAnMove Sep 9, 2026
4d83633
fix(wizard): recognize explicit Studio music requests
IAnMove Sep 9, 2026
29060af
refactor: simplify SFX model dependency checks
IAnMove Sep 9, 2026
0578da9
refactor(wizard): isolate native audio parsing
IAnMove Sep 9, 2026
96bf7cd
Merge commit '4d836339b0d6d182139cffd3525ad5b9e84bc151' into feat/sha…
IAnMove Sep 9, 2026
eebea52
Merge commit '0578da9f' into feat/shared-sfx-commands
IAnMove Sep 9, 2026
36b4162
fix(studio): project disabled model defaults out of Music commands
IAnMove Sep 9, 2026
60019e5
fix(wizard): preserve bounded music literals through shared audio par…
IAnMove Sep 9, 2026
86f83f7
feat(wizard): prepare native SFX fields and isolate corrupt recovery …
IAnMove Sep 9, 2026
1e56661
Merge commit '60019e54' into feat/shared-sfx-commands
IAnMove Sep 9, 2026
dfed48b
fix(studio): admit empty Music style captions in generation.music
cursoragent Sep 9, 2026
95819f1
fix(sfx): validate Wizard fields and reject conflicting prompt aliases
IAnMove Sep 9, 2026
81a399d
refactor(sfx): separate admission identity, guide timing and content …
IAnMove Sep 9, 2026
74d29e4
docs(qa): classify SFX facade test with the canonical inventory reason
IAnMove Sep 9, 2026
0220c80
fix(sfx): discard stale video options when entering native audio
IAnMove Sep 9, 2026
abccd28
fix(sfx): mint a unique generation.sfx intent per pack clip
cursoragent Sep 9, 2026
91341e5
Merge reviewed Music caption fix from PR #284
IAnMove Sep 9, 2026
5148f1e
fix(studio): keep model-specific captions and report the admitted media
IAnMove Sep 9, 2026
9f78d1d
Merge SFX pack intent fix from PR #285 for recovery hardening
IAnMove Sep 9, 2026
756118c
fix(sfx): retain per-clip receipts and partial pack recovery
IAnMove Sep 9, 2026
2b8a81a
fix(wizard): retain explicit model and negative prompt in SFX packs
IAnMove Sep 9, 2026
94ab006
fix(wizard): preserve authored SFX packs and block video fallback
IAnMove Sep 9, 2026
2a848df
fix(sfx): ignore leftover Speech/Music lyrics when the SFX box is empty
cursoragent Sep 9, 2026
747875a
fix(studio): isolate audio references across submode changes
IAnMove Sep 9, 2026
1b9fcdd
fix(studio): align audio tabs and show selected voice references
IAnMove Sep 9, 2026
5cb8107
fix(studio): keep audio reference selectors after late model defaults
IAnMove Sep 9, 2026
aeea671
Merge reviewed SFX form residue fix from PR #286
IAnMove Sep 9, 2026
d1ef846
fix(studio): isolate SFX video_guide across audio tabs
cursoragent Sep 9, 2026
a3e3771
fix(studio): restore saved SFX controls and preserve audio duration
IAnMove Sep 9, 2026
094ef9d
fix(studio): report submission truthfully and guard repeat clicks
IAnMove Sep 9, 2026
af5362b
fix(studio): pin gallery restore and reroll to the clicked output
IAnMove Sep 9, 2026
07be48c
refactor(studio): share typed submission failure handling
IAnMove Sep 9, 2026
c5f235d
fix(studio): no enviar residuos de SFX en Speech y Music
cursoragent Sep 9, 2026
091c0ec
Merge reviewed SFX video-guide tab isolation from PR #287
IAnMove Sep 9, 2026
f146357
Merge reviewed SFX form isolation from PR #288
IAnMove Sep 9, 2026
3c9b7fe
Share audio form admission guards and project SFX-owned text once
IAnMove Sep 9, 2026
b18b444
Drop music-only form controls when preparing Speech
IAnMove Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 48 additions & 12 deletions app/_launch_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -10672,6 +10672,13 @@ def _run_generation_with_preparation(job_id: str) -> bool:
job = _jobs.get(job_id)
if not isinstance(job, dict):
return False
try:
native_worker = _image_generation_commands.native_worker(job)
except HTTPException as error:
finish_job(job, "failed", error=str(error.detail), message="Command recovery could not be verified")
return False
if native_worker is not None:
return bool(native_worker(job_id))
params = job.get("params") if isinstance(job.get("params"), dict) else {}
pending = params.get("_h3_window_plan_pending")
if not isinstance(pending, dict):
Expand Down Expand Up @@ -10870,7 +10877,9 @@ async def generate(request: Request):
prepare_generation_inputs(body, _generation_model_def, requested_workspace,
uploads_dir=os.path.join(os.getcwd(), "uploads"),
workspace_dir=_workspace_dir(requested_workspace),
prepared_images=getattr(request, "prepared_studio_images", False) is True)
prepared_images=getattr(request, "prepared_studio_images", False) is True,
prepared_speech=(getattr(request, "prepared_studio_speech", False) is True
or getattr(request, "prepared_studio_audio", False) is True))
except ValueError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
try:
Expand Down Expand Up @@ -22438,6 +22447,13 @@ def _run_sfx_generation(job: dict, raw_params: dict, start_time: float):
):
return False

from services.studio_sfx_execution import prepared_sfx_execution
from services.studio_sfx_commands import check_sfx_models
typed_sfx = prepared_sfx_execution(
job, raw_params, registry=_task_registry(job["workspace"]),
check_models=lambda variant: check_sfx_models(globals(), variant),
)

out_dir = job.get("out_dir") or wgp.save_path
os.makedirs(out_dir, exist_ok=True)
wgp.save_path = out_dir
Expand All @@ -22459,11 +22475,17 @@ def _run_sfx_generation(job: dict, raw_params: dict, start_time: float):
video_path = candidate

if video_path and not os.path.isfile(video_path):
if typed_sfx:
raise ValueError("The admitted SFX video is no longer available")
print(f"[SFX] Warning: video_guide not found: {video_path}, falling back to text-only")
video_path = None

# If video provided, derive duration from it
if video_path:
if typed_sfx:
# The admission records the inspected guide duration and exact
# source identity. Do not rederive it or silently cap the request.
pass
elif video_path:
try:
import decord
vr = decord.VideoReader(video_path)
Expand Down Expand Up @@ -22494,12 +22516,14 @@ def _run_sfx_generation(job: dict, raw_params: dict, start_time: float):
)
return False

# Download model files if needed
if not update_job(
job, message="Downloading MMAudio models...", phase="Downloading models",
):
return False
wgp.download_mmaudio(variant_override=variant)
# Typed commands require installed dependencies, checked again above.
# Keep legacy provisioning until its callers have been migrated.
if not typed_sfx:
if not update_job(
job, message="Downloading MMAudio models...", phase="Downloading models",
):
return False
wgp.download_mmaudio(variant_override=variant)
if is_cancel_requested(job):
return False

Expand Down Expand Up @@ -22552,10 +22576,10 @@ def _run_sfx_generation(job: dict, raw_params: dict, start_time: float):
elapsed = time.time() - start_time
for fname in new_files:
ext = os.path.splitext(fname)[1].lower()
if ext not in {".wav", ".mp3", ".flac"}:
if ext not in {".wav", ".mp3", ".flac", ".mp4"}:
continue
sidecar = {
"params": {
"params": copy.deepcopy(raw_params) if typed_sfx else {
"prompt": prompt,
"MMAudio_prompt": prompt,
"MMAudio_neg_prompt": neg_prompt,
Expand Down Expand Up @@ -23435,6 +23459,17 @@ async def tools_upscale(request: Request):
"workspace": workspace, "out_dir": output_dir,
"provenance": provenance,
}
admit_command = getattr(request, "admit_generation_command", None)
if callable(admit_command):
# Only the typed in-process command adapter can transfer admission.
# Preserve the existing tool's resolved inputs and native worker;
# canonical task/receipt persistence now owns its queue lifecycle.
job["params"].pop("_non_durable_tool", None)
provenance["capability"] = "tools.upscale"
command_collection = (body.get("provenance") or {}).get("workspace_id")
if command_collection is not None:
provenance["workspace_id"] = command_collection
return admit_command(job["params"], workspace, provenance)
_register_manual_generation_job(job)
worker = _run_generation if execution_mode.policy().simulated else _run_tool_upscale
threading.Thread(target=worker, args=(job_id,), daemon=False).start()
Expand Down Expand Up @@ -36223,7 +36258,7 @@ def _generation_task_fields(job: dict) -> dict:
}.get(mode, "Generation job")
if str(provenance.get("capability") or "") == "remove_background":
task_title = "Tools · Remove background"
elif str(provenance.get("capability") or "") == "upscale":
elif str(provenance.get("capability") or "") in {"upscale", "tools.upscale"}:
task_title = "Tools · Upscale"
elif str(provenance.get("capability") or "") == "revoice":
task_title = "Tools · Revoice"
Expand Down Expand Up @@ -36863,7 +36898,8 @@ def _classic_redirect():
"generate": generate, "recast": recast_endpoint, "upscale": tools_upscale,
**wangp_agent_handlers(api), **image_command_handlers(_image_generation_commands)},
journal_path=os.path.join(os.path.dirname(__file__), "settings", "wangp-mcp-requests.sqlite3"),
command_operations=[*workspace_command_catalog()["operations"], *image_command_catalog()],
command_operations=[*workspace_command_catalog()["operations"], *image_command_catalog(
adapter.catalog for adapter in _image_generation_commands.operations.values())],
))

# ============================================================================
Expand Down
27 changes: 17 additions & 10 deletions app/routers/image_generation_commands.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""HTTP and MCP projections of the executable image command contract."""
from __future__ import annotations
from typing import Literal

from fastapi import APIRouter, Request
from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator
Expand All @@ -11,6 +12,7 @@
class ReferenceResolutionInput(BaseModel):
model_config = ConfigDict(extra="forbid", strict=True)
references: list[StrictStr] = Field(min_length=1, max_length=64)
media_kind: Literal["image", "audio", "video"] = "image"


class UISubmissionContext(BaseModel):
Expand All @@ -37,7 +39,7 @@ def _ui_context(request):
raise command_error(422, "invalid_ui_context", "Use only exact workflowId and runId attribution") from error


def image_command_catalog():
def image_command_catalog(additional_operations=()):
spec = image_generation_schema()
studio = studio_image_schema()
studio_input = dict(studio["input"])
Expand All @@ -60,18 +62,20 @@ def image_command_catalog():
"description": "Admit an image job with an installed model and explicit output workspace. Version 1 is a single text-to-image request; version 2 accepts the complete typed Studio image parameters, canonical references, LoRAs and image processors. Preserve literal prompts and reuse intent_id only for retries. The receipt proves admission; inspect its task for completion.",
"inputSchema": envelope},
{"name": "generation.receipt", "version": 1, "domain": "studio", "mutation": False,
"description": "Read an immutable image admission and its current canonical task in the exact original output workspace.",
"description": "Read an immutable generation admission and its current canonical task in the exact original output workspace.",
"inputSchema": {"type": "object", "additionalProperties": False,
"properties": {"version": {"type": "integer", "const": 1},
"operation": {"const": "generation.receipt"}, "input": receipt_input},
"required": ["version", "operation", "input"]}}]
"required": ["version", "operation", "input"]}}, *additional_operations]


def image_command_handlers(service):
async def submit(arguments):
if not isinstance(arguments, dict) or set(arguments) != {"version", "intent_id", "input"}:
raise command_error(422, "invalid_command", "Use version, intent_id and input for the image tool")
return await service.submit({**arguments, "operation": "generation.image"}, trusted_tool="external_agent")
def submission_handler(operation):
async def submit(arguments):
if not isinstance(arguments, dict) or set(arguments) != {"version", "intent_id", "input"}:
raise command_error(422, "invalid_command", "Use version, intent_id and input for the generation tool")
return await service.submit({**arguments, "operation": operation}, trusted_tool="external_agent")
return submit

def receipt(arguments):
if (not isinstance(arguments, dict) or set(arguments) != {"version", "input"}
Expand All @@ -81,15 +85,17 @@ def receipt(arguments):
raise command_error(422, "invalid_command", "Use version 1 with workspace and intent_id")
return service.receipt(**arguments["input"])

return {"generation.image": submit, "generation.receipt": receipt}
operations = {"generation.image", *getattr(service, "operations", {})}
return {**{operation: submission_handler(operation) for operation in operations}, "generation.receipt": receipt}


def create_image_generation_commands_router(service):
router = APIRouter()

@router.get("/api/v1/generation/commands")
def catalog():
return {"version": 2, "operations": image_command_catalog()}
return {"version": 2, "operations": image_command_catalog(
adapter.catalog for adapter in getattr(service, "operations", {}).values())}

@router.post("/api/v1/generation/commands")
async def submit(request: Request):
Expand Down Expand Up @@ -118,7 +124,8 @@ def references(body: ReferenceResolutionInput):
if any(not 1 <= len(value) <= 8192 for value in body.references):
raise command_error(422, "invalid_reference", "An exact bounded media reference is required")
try:
return {"references": [resolve(value) for value in body.references]}
return {"references": [resolve(value) if body.media_kind == "image"
else resolve(value, media_kind=body.media_kind) for value in body.references]}
except (ValueError, OSError) as error:
raise command_error(422, "invalid_reference", str(error)) from error

Expand Down
40 changes: 40 additions & 0 deletions app/routers/studio_music_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Serializable catalog for the local Studio music operation."""

from services.studio_music_spec import studio_music_schema


def music_command_catalog():
"""Return the exact closed ``generation.music`` discovery entry."""
schema = studio_music_schema()
input_schema = dict(schema["input"])
definitions = input_schema.pop("$defs", {})
return {
"name": "generation.music",
"version": 2,
"supportedVersions": [2],
"domain": "studio",
"mutation": True,
"description": (
"Admit one local music generation with literal lyrics and caption "
"through the canonical generation queue. The selected ACE-Step or "
"MiniMax-Music3 model must already be installed; retries reuse the "
"same intent receipt."
),
"musicModelTypes": list(schema["music_model_types"]),
"guideRevision": schema["guide_revision"],
"inputSchema": {
"type": "object",
"additionalProperties": False,
"$defs": definitions,
"properties": {
"version": {"type": "integer", "const": 2},
"operation": {"const": "generation.music"},
"intent_id": schema["intent_id"],
"input": input_schema,
},
"required": ["version", "operation", "intent_id", "input"],
},
}


__all__ = ["music_command_catalog"]
20 changes: 20 additions & 0 deletions app/routers/studio_sfx_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Executable SFX command schema shared by local HTTP and external MCP."""
from services.studio_sfx_spec import studio_sfx_schema


def sfx_command_catalog():
schema = studio_sfx_schema()
input_schema = dict(schema["input"])
definitions = input_schema.pop("$defs", {})
return {
"name": "generation.sfx", "version": 2, "supportedVersions": [2],
"domain": "studio", "mutation": True,
"description": "Generate sound effects with installed MMAudio files in an explicit output workspace. Preserve literal prompts. Text-only requests produce audio with a duration up to 20 seconds; video-guided requests use the inspected video duration and produce a video with new audio. Sources require canonical references. Reuse intent_id only to recover an existing admission; follow its task for completion.",
"inputSchema": {
"type": "object", "additionalProperties": False, "$defs": definitions,
"properties": {"version": {"type": "integer", "const": 2},
"operation": {"const": "generation.sfx"},
"intent_id": schema["intent_id"], "input": input_schema},
"required": ["version", "operation", "intent_id", "input"],
},
}
20 changes: 20 additions & 0 deletions app/routers/studio_speech_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Serializable speech operation projected through the shared HTTP/MCP router."""
from services.studio_speech_spec import studio_speech_schema


def speech_command_catalog():
schema = studio_speech_schema()
input_schema = dict(schema["input"])
definitions = input_schema.pop("$defs", {})
return {
"name": "generation.speech", "version": 2, "supportedVersions": [2],
"domain": "studio", "mutation": True,
"description": "Admit speech using an installed speech model, literal text, voice settings and canonical audio references in an explicit output workspace. Preserve the original speaker text separately from the effective native prompt. Reuse intent_id only for retries; the receipt proves admission, and its task reports completion.",
"inputSchema": {
"type": "object", "additionalProperties": False, "$defs": definitions,
"properties": {"version": {"type": "integer", "const": 2},
"operation": {"const": "generation.speech"},
"intent_id": schema["intent_id"], "input": input_schema},
"required": ["version", "operation", "intent_id", "input"],
},
}
51 changes: 51 additions & 0 deletions app/routers/tools_upscale_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Pure catalog projection for the typed Tools upscale operation."""

from __future__ import annotations

from copy import deepcopy

from services.tools_upscale_spec import tools_upscale_schema


def tools_upscale_command_catalog() -> dict:
"""Return the exact operation entry consumed by HTTP/MCP discovery."""
schema = tools_upscale_schema()
input_schema = dict(schema["input"])
definitions = input_schema.pop("$defs", {})
return {
"name": "tools.upscale",
"version": 2,
"supportedVersions": [2],
"domain": "tools",
"mutation": True,
"description": (
"Upscale one exact image or video source in an explicit output "
"workspace with an installed local processor. Preserve the "
"source workspace, processor settings and intent_id; the shared "
"receipt proves admission and its canonical task reports completion."
),
"inputSchema": {
"type": "object",
"additionalProperties": False,
"$defs": definitions,
"properties": {
"version": {"type": "integer", "const": 2},
"operation": {"const": "tools.upscale"},
"intent_id": deepcopy(schema["intent_id"]),
"input": input_schema,
},
"required": ["version", "operation", "intent_id", "input"],
},
}


# Short aliases used by catalog exporters in adjacent command slices.
tools_upscale_catalog = tools_upscale_command_catalog
upscale_command_catalog = tools_upscale_command_catalog


__all__ = [
"tools_upscale_catalog",
"tools_upscale_command_catalog",
"upscale_command_catalog",
]
Loading
Loading