Frees v2/models/ to become the vendored-architecture namespace that mirrors fastvideo/models (part of making v2 self-contained / able to replace fastvideo). The v2 recipe layer (per-family card.py/loop.py/program.py + common.py + the build_*_engine re-exports) is the recipe, not the architectures, so it moves to v2/recipes/. The pure-numpy toy/parity implementations (ToyDiT etc.) move from v2/models/backend.py to v2/platform/backends/toy.py (alongside cpu.py/accel.py/torch_*). Mechanical: all imports are absolute, so v2.models.<x> -> v2.recipes.<x> and v2.models.backend -> v2.platform.backends.toy across v2/ + examples/ (89 files, 178 refs). No behavior change. CPU mini green (202 passed, 2 skipped); all v2 files compile.
66 lines
3.0 KiB
Python
66 lines
3.0 KiB
Python
"""Nested workflows — recursive cross-model composition (design_v3 §13; §9.13).
|
|
|
|
A workflow id is a servable, so a `WorkflowStage` can invoke a *workflow* exactly as it invokes a
|
|
model. These tests build a workflow whose first stage IS the T2I→I2V workflow (then extends its video),
|
|
register it on an engine that already serves the inner workflow, and assert: it runs end-to-end;
|
|
`requires` lists the inner workflow; validation recurses; and a self-referencing workflow is rejected
|
|
by the cycle guard (no infinite recursion).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from v2.recipes import build_image_video_engine, build_t2i_i2v_extend_workflow
|
|
from v2.program import Workflow, WorkflowStage
|
|
from v2.request import TaskType, make_request
|
|
from v2.runtime import Engine
|
|
|
|
|
|
def test_nested_workflow_runs_end_to_end():
|
|
eng = build_image_video_engine() # serves flux-t2i, wan-i2v, AND image_video.t2i_i2v
|
|
outer = build_t2i_i2v_extend_workflow()
|
|
eng.register_workflow(outer)
|
|
out = outer.run(eng, prompt="a red fox in snow", seed=2)
|
|
frames = np.asarray(out.artifacts["video"].frames)
|
|
assert frames.ndim == 4 and frames.shape[1] > 1 # the extended (I2V) video
|
|
|
|
|
|
def test_nested_workflow_requires_the_inner_workflow():
|
|
outer = build_t2i_i2v_extend_workflow()
|
|
assert outer.requires == ["image_video.t2i_i2v", "wan-i2v"] # a workflow id among its deps
|
|
# registering on an engine that does NOT serve the inner workflow fails fast
|
|
with pytest.raises(ValueError, match="requires unregistered"):
|
|
Engine().register_workflow(outer)
|
|
|
|
|
|
def test_nested_workflow_addressable_by_id():
|
|
eng = build_image_video_engine()
|
|
eng.register_workflow(build_t2i_i2v_extend_workflow())
|
|
assert eng.serves("image_video.t2i_i2v_extend")
|
|
out = eng.run(make_request(TaskType.T2V, "image_video.t2i_i2v_extend", "a lighthouse"))
|
|
assert out.artifacts["video"].frames is not None # engine.run routes the OUTER workflow,
|
|
# which routes the INNER workflow as its stage 1
|
|
|
|
|
|
def _selfref():
|
|
return Workflow("image_video.loop", [WorkflowStage(
|
|
"image_video.loop", lambda s: make_request(TaskType.T2V, "image_video.loop", s["prompt"]))])
|
|
|
|
|
|
def test_self_reference_caught_at_registration():
|
|
"""A workflow that requires itself can't even register — `requires` includes its own (unserved) id.
|
|
This is the primary cycle prevention (declared deps, validated)."""
|
|
eng = build_image_video_engine()
|
|
with pytest.raises(ValueError, match="requires unregistered"):
|
|
eng.register_workflow(_selfref())
|
|
|
|
|
|
def test_runtime_cycle_guard_is_defense_in_depth():
|
|
"""Even if a cycle slipped past validation (here forced by inserting directly), the run-time guard
|
|
raises rather than recursing forever."""
|
|
eng = build_image_video_engine()
|
|
eng._workflows["image_video.loop"] = _selfref() # bypass validate() to simulate a cycle
|
|
with pytest.raises(ValueError, match="cycle"):
|
|
eng.run(make_request(TaskType.T2V, "image_video.loop", "x"))
|