Render: playing at the clips' real frame rate is an option (Settings), off by default
The final video keeps the project's frame rate unless 'Keep the clips' own frame rate' is switched on. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tw9fjXAUznBpkWSZm9vtPL
This commit is contained in:
@@ -22,7 +22,7 @@ export default {
|
||||
busy = true; draw();
|
||||
try {
|
||||
await p.flush();
|
||||
const state = await runJob(`${RENDER}/projects/${open.id}/render`, { clips });
|
||||
const state = await runJob(`${RENDER}/projects/${open.id}/render`, { clips, real_fps: p.pref("renderRealFps", false) === true });
|
||||
if (state.state !== "done") return tell(state.detail || "The render failed.");
|
||||
const { media } = await call("POST", `${RENDER}/import-clip`, { clip: state.media });
|
||||
if (app.say) app.say("media"); // the bin shows it now
|
||||
@@ -34,6 +34,10 @@ export default {
|
||||
if (node) node.replaceWith(next); else host.append(next);
|
||||
node = next;
|
||||
}
|
||||
app.editorSettings.push(() => c.region.stack({ gap: "md", children: [
|
||||
c.label.section({ text: "Render" }),
|
||||
c.toggle.default({ label: "Keep the clips' own frame rate", hint: "Off: the final video plays at the project's frame rate. On: it plays at the fastest rate the clips really have, for example after Interpolate, so no frames are dropped.",
|
||||
checked: p.pref("renderRealFps", false) === true, onChange: (v) => p.setPref("renderRealFps", v) })] }));
|
||||
draw();
|
||||
const offered = offer(app, { id: "render-final", label: "Render", icon: "⧉", run: () => { if (!busy && p.project) render(); } });
|
||||
const off = app.on(() => { if (Boolean(p.project) === node.disabled) draw(); });
|
||||
|
||||
@@ -117,12 +117,13 @@ def routes(table, base, web):
|
||||
@table.post(base + "/projects/{pid}/render")
|
||||
async def _render(req):
|
||||
proj = project_of(req)
|
||||
clips = (await body_of(req)).get("clips")
|
||||
body = await body_of(req)
|
||||
clips = body.get("clips")
|
||||
clips = clips if isinstance(clips, list) else []
|
||||
if not clips and not stitch.has_graphics(proj):
|
||||
return bad("Nothing to render: add overlays, audio, or generated clips.")
|
||||
job = jobs.new()
|
||||
asyncio.create_task(jobs.run(job, stitch.render, proj, clips, error=stitch.RenderError))
|
||||
asyncio.create_task(jobs.run(job, stitch.render, proj, clips, body.get("real_fps") is True, error=stitch.RenderError))
|
||||
return web.json_response({"job_id": job})
|
||||
|
||||
@table.get(base + "/projects/{pid}/render/{job}")
|
||||
|
||||
@@ -277,8 +277,9 @@ def _rate(text) -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
def render(project, clips: list[dict]) -> dict:
|
||||
"""Run the final render: -> {"media": {...temp file...}, "clips": n}. Blocking: call it off the event loop."""
|
||||
def render(project, clips: list[dict], real_fps: bool = False) -> dict:
|
||||
"""Run the final render: -> {"media": {...temp file...}, "clips": n}. Blocking: call it off the event loop.
|
||||
`real_fps`: play at the fastest rate the clip files really run at, not the project's frame rate."""
|
||||
clips = [c for c in clips if _clip_spec_ok(c)] if isinstance(clips, list) else []
|
||||
blank = not clips
|
||||
if blank and not has_graphics(project):
|
||||
@@ -294,8 +295,8 @@ def render(project, clips: list[dict]) -> dict:
|
||||
for c in clips:
|
||||
c["w"], c["h"] = cw, ch # build_filter reads the canvas off the clips
|
||||
fps = _f(clips[0].get("fps")) or fps
|
||||
real = [r for r in (_rate(files.frame_rate(p)) for p in paths) if r > 0]
|
||||
if real: # what the files really run at (interpolated renders are faster than the project's setting); the fastest wins so none is thinned
|
||||
real = [r for r in (_rate(files.frame_rate(p)) for p in paths) if r > 0] if real_fps else []
|
||||
if real: # what the files really run at (interpolated renders are faster than the project's setting); only when asked for; the fastest wins so none is thinned
|
||||
fps = max(real)
|
||||
for c in clips:
|
||||
c["fps"] = fps
|
||||
|
||||
@@ -127,11 +127,12 @@ def test_finished_jobs_are_forgotten_past_the_cap(monkeypatch):
|
||||
|
||||
|
||||
@needs_ffmpeg
|
||||
def test_render_keeps_the_real_frame_rate_of_interpolated_clips(comfy, server):
|
||||
def test_render_keeps_the_real_frame_rate_of_interpolated_clips_only_when_asked(comfy, server):
|
||||
make_clip(comfy.output / "fast.mp4", 1.0, rate=48)
|
||||
proj = projects.save(projects.Project(width=160, height=120))
|
||||
status, queued = call(server, "POST", f"{BASE}/projects/{proj.id}/render", {"clips": [clip("fast.mp4", fps=24)]}) # the setting says 24, the file runs at 48
|
||||
assert status == 200
|
||||
job = finish(server, f"{BASE}/projects/{proj.id}/render", queued["job_id"])
|
||||
assert job["state"] == "done"
|
||||
assert files.frame_rate(str(comfy.temp / job["media"]["filename"])) == "48/1"
|
||||
for body, expected in (({}, "24/1"), ({"real_fps": True}, "48/1")): # the setting says 24, the file runs at 48
|
||||
status, queued = call(server, "POST", f"{BASE}/projects/{proj.id}/render", {"clips": [clip("fast.mp4", fps=24)], **body})
|
||||
assert status == 200
|
||||
job = finish(server, f"{BASE}/projects/{proj.id}/render", queued["job_id"])
|
||||
assert job["state"] == "done"
|
||||
assert files.frame_rate(str(comfy.temp / job["media"]["filename"])) == expected
|
||||
|
||||
Reference in New Issue
Block a user