Repository navigation
Description
Idea, not approved work. Recorded from a comparative review; needs a decision before any implementation.
Gap
Our video-to-video templates refuse frame counts off the model grid, and handle aspect ratio inconsistently: upscale-clip centre-crops while refine-clip stretches. Users must pre-conform their footage.
Idea
A fit_to_model / restore_to_source task pair:
- Fit:
- Resize to the model bucket by stretching, cropping to fill, or letterboxing, and record what was done.
- Pad the frame count to the grid.
- Restore:
- Crop out the letterbox and resize back to the exact source width and height.
- Reconcile the frame count: tolerate a small difference, and keep the original tail frames the model dropped.
- Optionally blend with the original (
orig*(1-s) + out*s).
Optional extension: guided enhancement anchors
- Pick every Nth frame (16 recommended), always including the first and last.
- Enhance each anchor with an image model (Z-Image img2img).
- Feed the anchors to LTX v2v as keyframe conditions; their strengths are cond 0.5 and guiding 0.2.
LTX2ConditionPipelinealready takesconditionswith latent indices. Theiri % 8 == 1index dodge is a ComfyUI quirk, not needed here.
Their files: VRGDG_VideoEnhanceNodes.py, VRGDG_StandaloneVideoEnhancerNodes.py.
Source: concept observed in vrgamegirl19/comfyui-vrgamedevgirl (assessed 2026-10-05). That repo is under a source-available license that is incompatible with our Apache-2.0: re-implement from the concept only, do not copy code, prompt text, or presets verbatim. File references below are to their repo, for understanding the mechanism.
Activity
dkackman commentedon Oct 5, 2026
Approved by Don (2026-10-05) at priority:2, for the fit_to_model / restore_to_source pair only. The guided-enhancement-anchors extension is split out to #613 to be assessed on its own. The plan should also make upscale-clip and refine-clip handle aspect the same way. Handing to the feature lead for a plan.
dkackman commentedon Oct 5, 2026
Plan v4:
fit_to_model/restore_to_source(#602)Plan by the lead agent, model
claude-opus-5-5viaanthropic. Scope is the task pair only, plus makingupscale-clipandrefine-cliphandle aspect ratio the same way, as Don's 2026-10-05 comment set it. The anchors extension is #613.Approved by Don (2026-10-05) as v1/v2; that approval was withdrawn when building stage B showed two parts of v2 don't hold against the code (#632's comment). v3 approved by Don (2026-10-06), with Q2′ as recommended. v4 records that decision (see Decisions); scope is unchanged from v3. Stage A (#631) is verified and stands; v3 adds two small changes to its tasks, built in stage B.
Verdict: build smaller, in two stages
What it gives. Today the caller has to set
width/heightto match the source clip. Both templates spend most of their descriptions on this, and the two get it wrong in opposite ways:upscale-clipsilently centre-crops a different aspect ratio. That's diffusers'preprocess_video(resize_mode="crop"),pipeline_ltx2_ic_lora.py:1424.refine-clipsilently stretches it (pipeline_ltx2_latent_upsample.py:363).loop_frames). In upscale-clip it gets no reference past its end.With the pair, the size variables become the model's working size. Any source fits into that size and comes back at exactly its own size (×2 for these templates) and its own frame count.
Value.
regression-model-specific. Neither is in any of Don's workspaces, there are no field reports, and the templates are two days old.resize_videotask) was declined. This plan stays narrower than that. It is a model-bound round trip with a record, not a general resize task.Build cost. About $9–14: stage A $4–6 (spent, verified), stage B $5–8 (v3).
Inertia.
list_tasks/get_task, onemodedomain indw/task_domains.py, and a record shape that templates come to depend on.SURFACE_BUDGETintests/test_mcp_server.pydoesn't count tasks.width/heightkeep their names and their meaning, the output size (Q2′), so no variable is removed.Reversibility. Good while the only consumers are templates. The record shape is internal to a fit→restore pair inside one workflow, so it can change without a
breaking-changeuntil a user's saved workflow names its fields.Cheaper alternatives (considered; Don chose the full pair).
Overlap.
orig*(1-s)+out*sblend. That is why the blend is cut here.Cut, and what brings each part back:
blendoption. idea: strength-selectable same-size LTX refine (sigma ladder) + per-segment strength decay in chains #606 covers strength natively. It comes back if idea: strength-selectable same-size LTX refine (sigma ladder) + per-segment strength decay in chains #606 is declined, or if a field report asks for a weaker refine that idea: strength-selectable same-size LTX refine (sigma ladder) + per-segment strength decay in chains #606 can't give.restore-deblur/restore-decompression. They have the same silent crop. A separate follow-up (Q3).Problem and non-goals
Problem. v2v templates make the caller conform the footage, and the two 2x templates disagree on how they do it.
Non-goals:
width/height/num_framesfrom the source at run time;The design
The proposal is corrected below where the code disagreed with it.
Task
fit_to_model(video, width, height, num_frames, mode="letterbox", downscale=1)What it does:
asset:oroutput:.width×heightusing bicubic resampling with antialiasing.modeis one of:stretch: resize to fill the frame exactly.crop: scale to fill and centre-crop.letterbox: scale to fit, then centre it on black.num_frames:num_framesframes;loop_framesdoes.downscale(v3, built in stage B): an optional positive integer, default 1. It fits intowidth/downscale × height/downscaleand records that as the model size.widthorheightisn't divisible by it.LTX2InContextPipelinetakeswidth/heightat the output size and derives its reference aswidth // reference_downscale_factor(pipeline_ltx2_ic_lora.py:1403-1404). Workflow JSON can't divide a variable, so the task does it. upscale-clip passesdownscale: 2, matching itsreference_downscale_factor: 2.What it returns: a dict, so later steps read it as
previous_result:<step>.videoandprevious_result:<step>.fit(the precedent isoutpaint.json'smask_image.bordered_image).video: the frames, float [0,1], with the source's fps.numpy.ndarraysubclass carrying.fps(indw/media_types.py, alongsideAudioVideo), instead of anAudioVideo.AudioVideohanded to a pipeline argument or a condition'sframes, andprevious_result:fit.video.framesdoesn't resolve (get_artifact_properties,dw/result.py:217, reads one level). An ndarray passes diffusers' ownisinstance(np.ndarray)checks as-is, and stays float, whichLTX2ReferenceConditionneeds (ltx2/reference-sheet: succeeds with no warnings but every frame is blocky mush; default asset:reference_sheet.png missing #444).fit: the record:mode;source_width,source_height,source_frames;model_width,model_height,model_frames;content_box(x, y, w, h): where the source sits in the model frame. In crop mode,source_boxis the part of the source that was kept.Correction to the proposal: it does not pick a bucket or a grid.
width,heightandnum_framesstay template variables that the task reads.variable_constraints(dw/variable_constraints.py:267,362),cost_driversand the VRAM estimate (dw/plan.py:318,dw/vram_estimate.py:57) only see variables. A size or frame count computed at run time would lose the 32n / 8n+1 refusals and the cost quote.num_frames. Restore then trims the padding back.snap: the existing 8n+1 refusal stays as it is.Refusals, before any work:
width/height/num_framesnot positive integers;mode;Task
restore_to_source(video, fit)What it does:
video.width / model_width. It must equalvideo.height / model_height, otherwise it refuses with both sizes named. So the 2x templates need noscaleargument. This is a correction: the proposal assumed restore always goes back to 1×.content_box × scale, then resizes tosource_width × scale×source_height × scale.source_boxat the source's pixel density (× scale). It is exact in density, smaller on the cropped axis, and the description says so.min(source_frames, frame count of video). Padding is removed. A pipeline that floored the count returns what it made, and the record says how many frames there should have been.Audio
normalize_audio) andpair_audio(fit="video")cuts it to the video.Templates (stage B)
Both get the same step sequence:
fit(mode from a newfitvariable, defaultletterbox, per Q1);previous_result:fit.video;restore(video: previous_result:<pipeline>,fit: previous_result:fit.fit);pair_audio.Per template:
upscale-clip: fit to the IC reference size (pipeline_ltx2_ic_lora.py:1403), half the output. Diffusers' crop is then a no-op.width/heightstay the output size (default 960×544, 32n), passed to the pipeline as today.fitgetswidth: variable:width,height: variable:height,downscale: 2, so the reference is 480×272. The reference is fed asprevious_result:fit.videoin place ofvariable:source_video.cost_driversand the VRAM estimate keep seeing the output size, as today.ref_width/ref_height(Q2) can't be wired: nothing could pass the pipeline2 × ref_width. A cross-variable constraint would be an engine change, andreference_downscale_factor: 1would give a 1× output from a 2× LoRA.refine-clip: fit towidth × height.loop_framesis replaced, and the upsampler's stretch becomes a no-op.Changed behaviour, stated in both descriptions:
intermediate/keeps the raw model output in the model frame.Surfaces:
fit_to_model'sdownscaleargument andvideotype (stage B, v3).fitvariable on both. No variable is removed or renamed, so nobreaking-change(Q2′).skills/ltx-2.5/SKILL.mdtext, so the plugin version is bumped.Stages
Stage A: the two tasks (server)
What it builds:
fit_to_modelandrestore_to_sourceindw/tasks/(video_utils.pyor a newfit.py);register_commandentries with summaries and parameter descriptions;modedomain indw/task_domains.py;docs/TASKS.mdVideo Processing row each, plus a short "round trip" paragraph.Unit tests:
tests/test_task_discovery.py, presence tests).Deploy: server restart. Estimate: $4–6.
Acceptance intent (over MCP):
Discovery:
list_taskslists both, andget_taskshows their arguments andmode's three values.Letterbox round trip: with an uploaded 640×480, 50-frame, 24 fps clip, a utility workflow runs
fit_to_model(width=512, height=288, num_frames=57, mode=letterbox).get_output_frames).Scale inferred: the same round trip with a 2× resize in between (an existing per-frame
resize_rescalestep) restores to 1280×960.Stretch: restores to 640×480.
Crop: restores to the kept region at source density. A 640×480 source into 512×288 gives 640×360.
Long source: a source longer than
num_framescomes back atnum_framesframes.Refusals:
fitrecord.Each names the argument, and fails at its step without loading a pipeline.
Stage B: rewire upscale-clip and refine-clip (server + plugin)
Needs stage A.
What it builds:
dw/tasks/fit.py(v3):fit_to_model'sdownscaleargument, with its refusal and itsget_taskdescription;videoas the fps-carrying float ndarray, soprevious_result:fit.videofeeds a pipeline argument or a condition'sframesdirectly;previous_result:fit.videointo a pipeline's arguments and a condition'sframesthrough the real argument path, not a mock;fitvariable (defaultletterbox);downscale: 2onfit, withwidth/heightunchanged;COMPACT_BUDGETintests/test_catalog_structure.py:411;workflows/templates/ltx2/README.md,plugins/dw/skills/ltx-2.5/SKILL.md(and a plugin version bump),docs/RECIPES_24GB.md:186.Tests to update: these pin today's shape.
tests/test_ltx2_ic_loras.py::212unpacks two steps;:174-185pins the reference tovariable:source_video. Only upscale-clip changes; the restore templates keep that pin;:216pinspair_audio's input.tests/test_catalog_structure.py:566-645(TestLtxRefineClippinsloop_frames).tests/test_step_cache.py:706-726hashes, if the configuration changes.Deploy: server, plus a plugin release. Estimate: $5–8 (v3 adds the task changes).
Acceptance intent (over MCP): two GPU runs, about 3 minutes each.
source_video.final/output is 1280×960, 50 frames, 24 fps, and its audio is the source's length.get_output_framescompared to the source shows no edge content lost.intermediate/shows the model-frame output with bars.downscale(v3):get_task fit_to_modelshows it. A utility run withwidth=512, height=288, downscale=2gives a 256×144 fitted video, and the record's model size is 256×144.downscale=3withwidth=512is refused, naming both.num_framesoff 8n+1 are still refused at validate;source_audio, before any pipeline loads (refine-clip).stretchwithin this stage.Order and dependencies
Risks
fit.videoand reference the file) doesn't work:output:names only earlier runs, and an h264 round trip would lose detail before a 2× re-render. v3 makesfit.videoan ndarray, the formrefine-clipalready feeds the upsampler, and stage B's unit test runs it through the real argument path first. If a condition'sframesstill won't take it, stage B stops and re-plans; it doesn't bend the engine..fpsthrough a numpy op that returns a plain array. Restore reads its fps from the pipeline output or the template'sresult.fps, as today..fpsonly matters for savingfit.videoitself, which stage B's tests pin.stretch(Q1).Follow-ups (filed to Don after stage B)
restore-deblurandrestore-decompression(Q3).loop_frameswith no template consumer, file its removal as a proposal.Decisions (Don)
mode(2026-10-05):letterboxfor both templates. Flip tostretchif stage B's runs show an edge halo.move toSuperseded by Q2′: it can't be wired (see Templates).ref_width/ref_heightwith the output at 2×, removingwidth/height. Labelledbreaking-change.width/heightas the output size, andfit_to_modelgainsdownscale(upscale-clip passes 2). No engine change, nobreaking-change. The two templates' size variables mean different things (output vs working size), and the descriptions say which.fit.videoas the fps-carrying float32 ndarray is approved.dkackman commentedon Oct 5, 2026
Plan v1 approved by Don (2026-10-05).
Q1-Q3 defaults accepted:
letterboxis the default mode. Flip it tostretchif stage B's runs show an edge halo.ref_width/ref_height, with the output at 2x. This is labelledbreaking-change.Follow-ups: after stage B's GPU runs confirm letterbox holds up under a generative model, file the Q3 issue. If stage B leaves
loop_frameswith no template consumer, file its removal as a proposal too. Both go to Don.29 remaining items
dkackman commentedon Oct 6, 2026
Spec for plan v4, revised for reshaped stage B (#632). Written from the plan alone by
claude-opus-5-5viaanthropic. Every new case carriespending: #632.#631, stage A: unchanged. C-F237–C-F244 and SE-F043 stand.
#632, stage B (reshaped), all in
regression-suite-complete.md:fitvariable. upscale-clip keepswidth/height(960×544, 32n) with noref_*, and its fit step hasdownscale: 2. Each pipeline input readsfit.videodirectly. The 32n/8n+1 refusals hold at validate, and #602 stage B: rewire upscale-clip and refine-clip through fit/restore #632 has nobreaking-change. Free.downscaleon CPU.get_taskshows it (optional, default 1).w=512, h=288, downscale=2gives a 256×144 fit, with model size 256×144 in the record andcontent_box{32,0,192,144}.downscale=32gives 16×9.fittedkeeps 24 fps, which is the ndarrayvideokeeping stage A's contract.downscalerefusals.downscale=3, width=512names both.downscale=5, 515×288."two"are refused.width1024 /height576 on the 512×288×121 fixture. GPU.ref_*text, and nothing called breaking.The superseded C-F245, C-F248 and C-F250 can't be edited from here. dkackman/harnest#60 asks to retire them. Until it is applied, a post-verify regression run would fail them on purpose-changed behaviour.
Notes (non-blocking):
previous_result:fit.videointo a condition'sframes. Over MCP that is covered only through C-F246/C-F247's GPU runs, since neither template exposes a condition the tester can feed separately..fps(a risk the plan names) would show asfittedlosing 24 fps in C-F319 and C-F238.Merge branch 'docs/602-closeout-fit-to-model' into develop (#602)
dkackman commentedon Oct 6, 2026
Close-out: both stages closed and verified. #631 (stage A, the
fit_to_model/restore_to_sourcetasks) and #632 (stage B, upscale-clip and refine-clip rewired through fit/restore, plusdownscaleand theFittedVideondarray) both verified 2026-10-06. Q1's letterbox default stands: C-F246 showed no edge halo.Design record:
docs/proposals/complete/fit-to-model-restore-complete.md(develop @ 14663be, commit ef7c25c). It covers what was built, bounces per stage (one each), and what was deferred: Q3 (the pair in restore-deblur/decompression), the blend and tail-splice cuts, and #613.todo.mdhas the shipped entry, and the two ltx2 clip records note the crop/stretch rule they describe is superseded.loop_frameskeeps a consumer (reference-sheet.json), so no removal follow-up.verify: run every case whose
source:names one of this feature's stages, and close the parent if they all pass. C-F245, C-F248 and C-F250 are plan-v2 cases whose retirement is requested in dkackman/harnest#60/#65; they are not expected to pass.— lead agent,
claude-opus-5-5viaanthropicdkackman commentedon Oct 6, 2026
Verified: feature parent #602. Tester, running as
claude-opus-5-5viaanthropic, against lem on develop @ 14663be, in workspaceregression-complete.I ran every case whose
source:names #631 or #632. All passed. I did not run or count C-F245, C-F248 or C-F250, which are superseded plan-v2 cases with retirement requested in harnest#60/#65.Templates (GPU)
reference_downscale_factor2, reference frames taken fromprevious_result:fit.video, and noref_*.src-512x288-121f, and upscale-clip with the same source at 1024×576×121.src-640x480-50f-silent.mp4failed in 1.2 s at step 0,source_audio, with the error "...carries no audio track...". No pipeline phase was reached.Tasks (round trip)
content_box/source_boxand model/source sizes.mode/width/height/num_frames/downscalearm was refused at validate, with its path and value named. That covers the 3-mode list, "above zero", "whole number", "to be a number", and the divisible-by-downscale message.restore, namingfitand the field:source_widthNone got "not a whole number above zero".model_width'abc' got "'abc', not a whole number above zero".mode'zoom' got "its 'mode' is 'zoom'".fitset toprevious_result:fit.videogot "got a FittedVideo".arguments.width,arguments.heightandarguments.num_frames, plusfit_to_model needs 'mode' as one of [...]. #602 stage B: rewire upscale-clip and refine-clip through fit/restore #632 carries nobreaking-changelabel.list_workflowssummaries read "width/height = the working size" for refine-clip and "= the render size" for upscale-clip.ref_*or calls the change breaking.dw:ltx-2-5skill says the same.Security
fit.video(set throughsrc) and a literalrestore.videowere both tried with/etc/passwd,/nonexistent-dw-probe/x.mp4and../../../../etc/passwd, andsrcwas also set tofile:///etc/passwd.valid: falseat that step'sarguments.video...andfile:probes got their own refusals, and no message echoed anything beyond the string sent.run_workflow(acknowledged_cost=true)on the/etc/passwdarm raised the same refusal and queued no job.Cleanup: I deleted all 12 runs this session made with
delete_output(job_id=...).docs(proposals): dkackman#602 close-out - design record for fit_to_mo…