Reapply "Direct v2v saved before the video-only flag keeps taking its video from references; render polls survive one unanswered reply; LTX project HTTP test"
This reverts commit 5a2f9c5999.
This commit is contained in:
@@ -24,7 +24,7 @@ export async function buildInputs({ project, scene, slots, expand, nodeInfo, fra
|
||||
// a VIDEO first (it is then not also sent as a reference: nothing takes it there), else the first picture (which stays a
|
||||
// reference too, so an identity input still gets it). A video-only pipeline (video-to-video) looks at videos alone and is
|
||||
// never blanked by "From a prompt".
|
||||
const takesVideo = Boolean(source && source.role.video_frames), videoOnly = Boolean(source && source.role.video_only);
|
||||
const takesVideo = Boolean(source && source.role.video_frames), videoOnly = Boolean(source && (source.role.video_only || (slots || []).some((s) => s.node === "VAEEncode" && ((s.inputs || {}).pixels || [])[0] === source.slot.id))); // a pipeline that encodes the start itself is video-to-video, whether or not it was saved saying so
|
||||
const wanted = (id) => kinds.get(id) === "video" || (!videoOnly && kinds.get(id) === "image");
|
||||
const blank = project.generation_mode === "t2v" && !videoOnly;
|
||||
const fromRefs = takesVideo && !scene.source_image && !blank
|
||||
|
||||
@@ -183,3 +183,14 @@ test("an LTX image-to-video pipeline starts from the first reference picture whe
|
||||
assert.equal((await run({ references: ["pic"] }, { generation_mode: "t2v" })).inputs.src.media_id, ""); // "From a prompt": no picture
|
||||
learn([]);
|
||||
});
|
||||
|
||||
test("a direct video-to-video pipeline saved without the video-only flag is still video-to-video (an encoder is fed by the start slot)", async () => {
|
||||
const { learn } = await import("../../shell/bin.js");
|
||||
learn([{ id: "vid", name: "v.mp4", kind: "video" }, { id: "pic", name: "p.png", kind: "image" }]);
|
||||
const slots = [{ id: "src", node: "FunUILoadMedia", roles: [{ at: "assets.source_image", input: "media_id", video_frames: true }], inputs: {} },
|
||||
{ id: "enc", node: "VAEEncode", inputs: { pixels: ["src", 1] } }];
|
||||
const r = await buildInputs({ project: { video: {}, generation_mode: "t2v" }, scene: { text: "x", references: ["pic", "vid"] }, slots, expand: async () => null });
|
||||
assert.equal(r.inputs.src.media_id, "vid");
|
||||
assert.equal(r.inputs.src.full, true);
|
||||
learn([]);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,11 @@ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
export async function runJob(path, body) {
|
||||
const { job_id: job } = await call("POST", path, body);
|
||||
let state;
|
||||
do { await wait(1500); state = await call("GET", `${path}/${job}`); } while (state.state === "queued" || state.state === "running");
|
||||
let blips = 0;
|
||||
// A poll the server did not answer (a restart, a proxy's page) is asked again: the job runs on without the page.
|
||||
do {
|
||||
await wait(1500);
|
||||
try { state = await call("GET", `${path}/${job}`); blips = 0; } catch (err) { if (err.status && err.status < 500 || ++blips > 20) throw err; state = { state: "running" }; }
|
||||
} while (state.state === "queued" || state.state === "running");
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""An LTX project as Dex runs it, over HTTP on the real route table: the direct video-to-video preset (also as saved with
|
||||
the old wiring), a project with a video in References and Video input switched on, and every call the app makes at start-up
|
||||
and for a render. Each must answer JSON: a page that gets anything else fails on 'r is null'."""
|
||||
|
||||
import pytest
|
||||
|
||||
from core.tests.test_routes_pipeline import _request, registered, server # noqa: F401
|
||||
|
||||
|
||||
def _preset(pid):
|
||||
from modules.models.ltx.pipeline import presets
|
||||
return next(p["slots"] for p in presets() if p["id"] == pid)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pid", ["ltx23_video_to_video_direct", "ltx23_video_to_video", "ltx23_image_to_video", "ltx23_anchor_guide"])
|
||||
def test_every_startup_and_render_call_answers_json_on_an_ltx_project(server, pid):
|
||||
slots = _preset(pid)
|
||||
old = [dict(s, inputs={k: (["source_image", 0] if v == ["source_image", 1] else v) for k, v in s["inputs"].items()}) for s in slots]
|
||||
for saved in (slots, old):
|
||||
status, body = _request(server, "POST", "/fun/api/pipeline", {"slots": saved})
|
||||
assert status == 200 and "raw" not in body, (status, body)
|
||||
users = [(s["id"], k) for s in body["slots"] for k, v in s["inputs"].items() if isinstance(v, list) and v[0] == "source_image"]
|
||||
assert all(next(s for s in body["slots"] if s["id"] == i)["inputs"][k][1] == 1 for i, k in users), "the old wiring was not brought up to date"
|
||||
status, body = _request(server, "GET", "/fun/api/pipeline")
|
||||
assert status == 200 and "slots" in body, body
|
||||
|
||||
traits = "audio_stream,ltx_av"
|
||||
for path in ("/fun/api/story/markers", f"/fun/api/modules?traits={traits}", "/fun/api/modules", "/fun/api/shortcuts",
|
||||
"/fun/api/readiness", "/fun/api/system", "/fun/api/addons", "/fun/api/m/render/library", "/fun/api/pipeline/presets"):
|
||||
status, body = _request(server, "GET", path)
|
||||
assert status < 500 and "raw" not in body, (path, status, body)
|
||||
|
||||
status, body = _request(server, "GET", f"/fun/api/modules?traits={traits}")
|
||||
ids = [m["id"] for m in body.get("modules", [])]
|
||||
assert "conditioning_shot_camera" not in ids, "shot camera must be hidden for LTX"
|
||||
assert "conditioning_shot_camera" in [m["id"] for m in body.get("incompatible", [])]
|
||||
@@ -174,14 +174,24 @@ OPTIONAL_NODES = {"FunUIIdentityTransfer": {"pass": {"0": "model", "1": "positiv
|
||||
|
||||
|
||||
def upgrade(slots, _outputs=None):
|
||||
"""A pipeline saved while the picture inputs still read the loader's first output: a start slot that takes whole
|
||||
videos feeds them from the frames output now (output 0 is always one frame, so such a pipeline ran on a single frame)."""
|
||||
"""Pipelines saved by an earlier build, brought up to date:
|
||||
* the picture inputs read the loader's first output (always one frame): a start slot that takes whole videos feeds them
|
||||
from the frames output now;
|
||||
* the direct video-to-video pipeline, saved before its start slot said it takes videos only."""
|
||||
start = next((s for s in slots if s.get("id") == "source_image" and s.get("node") == "FunUILoadMedia"
|
||||
and any(r.get("video_frames") for r in s.get("roles") or [])), None)
|
||||
if not start:
|
||||
return slots
|
||||
return [dict(s, inputs={k: (["source_image", 1] if isinstance(v, (list, tuple)) and list(v) == ["source_image", 0] else v)
|
||||
for k, v in (s.get("inputs") or {}).items()}) for s in slots]
|
||||
encoded = any(s.get("node") == "VAEEncode" and (s.get("inputs") or {}).get("pixels") in (["source_image", 0], ["source_image", 1]) for s in slots)
|
||||
out = []
|
||||
for s in slots:
|
||||
inputs = {k: (["source_image", 1] if isinstance(v, (list, tuple)) and list(v) == ["source_image", 0] else v)
|
||||
for k, v in (s.get("inputs") or {}).items()}
|
||||
item = dict(s, inputs=inputs)
|
||||
if s is start and encoded:
|
||||
item["roles"] = [dict(r, video_only=True) if r.get("video_frames") else r for r in s.get("roles") or []]
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
|
||||
def optional_nodes():
|
||||
|
||||
@@ -232,3 +232,13 @@ def test_h3_only_features_are_not_offered_on_ltx(comfyui):
|
||||
have = ltx.probe_traits({"audio_adaln_single.linear.weight"})
|
||||
assert traits.missing_for(spec, have) == ["shot_prompts"]
|
||||
assert not set(H3_ONLY) & set(have)
|
||||
|
||||
|
||||
def test_a_direct_v2v_pipeline_saved_before_video_only_still_ignores_pictures_and_the_prompt_switch():
|
||||
from modules.models.ltx.pipeline import presets, upgrade
|
||||
slots = next(p["slots"] for p in presets() if p["id"] == "ltx23_video_to_video_direct")
|
||||
old = [dict(s, roles=[{k: v for k, v in r.items() if k != "video_only"} for r in s.get("roles") or []]) if s["id"] == "source_image" else s for s in slots]
|
||||
assert not any(r.get("video_only") for s in old for r in s.get("roles") or [])
|
||||
assert upgrade(old) == slots
|
||||
i2v = next(p["slots"] for p in presets() if p["id"] == "ltx23_image_to_video")
|
||||
assert not any(r.get("video_only") for s in upgrade(i2v) for r in s.get("roles") or []) # i2v keeps taking pictures
|
||||
|
||||
Reference in New Issue
Block a user