fix: a removed scene's result says where it went; Inspector follows text edited elsewhere; own renders import past the upload limit; xformers warning only when it is on
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
837a4215a4
commit
2aa52b0eee
@@ -177,3 +177,11 @@ test("a refusal said while a shot is being queued names the scene", async () =>
|
||||
await app.runner.units(["c"]);
|
||||
assert.match(document.body.textContent.replace(before, ""), /Scene 3: Length is 5000/);
|
||||
});
|
||||
|
||||
test("a scene removed while it was being made: the result is not dropped in silence, it says where the file is", async () => {
|
||||
const { app, doc } = rig("idle", { generate: async () => { doc.scenes.splice(0, 2); return true; } });
|
||||
app.project.editFor = async (_id, fn) => fn(doc);
|
||||
const before = document.body.textContent;
|
||||
await app.runner.units(["a"]);
|
||||
assert.match(document.body.textContent.replace(before, ""), /was removed while it was being made.*a\.mp4/);
|
||||
});
|
||||
|
||||
@@ -109,7 +109,8 @@ export default {
|
||||
break;
|
||||
}
|
||||
if (!images.length) { tell("ComfyUI finished without a result. Try again."); break; }
|
||||
await record(pid, unit, images[images.length - 1], frames, g.run.state.promptId); // the next shot may continue from this one: it must be on the clip first
|
||||
const kept = await record(pid, unit, images[images.length - 1], frames, g.run.state.promptId); // the next shot may continue from this one: it must be on the clip first
|
||||
if (kept === false) tell(`Scene ${snap.scenes.findIndex((s) => s.id === root.id) + 1} was removed while it was being made, so its result is not on the timeline. It is in ComfyUI's output folder as ${images[images.length - 1].filename}.`);
|
||||
snap.scene_renders = structuredClone(p.project.scene_renders || {}); // ...and in what the next shot reads
|
||||
}
|
||||
if (!made && !stopped && !said) tell("Nothing to generate: every scene is left out or is a video clip.");
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { setupDom, teardownDom } from "../../composer/tests/_dom.js";
|
||||
|
||||
let hub;
|
||||
test.before(async () => { setupDom(); hub = (await import("./index.js")).default; });
|
||||
test.after(() => teardownDom());
|
||||
|
||||
test("a scene's text changed elsewhere (the Story box) shows in the Prompt box, but not under the caret", async () => {
|
||||
let fire;
|
||||
const doc = { id: "P", frame_rate: 25, num_frames_per_scene: 49, scenes: [{ id: "a", text: "old" }], scene_renders: {} };
|
||||
const p = { project: doc, get scenes() { return doc.scenes; }, get selected() { return doc.scenes[0]; }, selectedId: "a",
|
||||
setText: () => {}, setScene: () => {}, edit: (fn) => fn(doc) };
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
hub.setup({ host, app: { project: p, on: (fn) => { fire = fn; return () => {}; }, api: {}, say: () => {} } });
|
||||
const box = () => [...host.querySelectorAll("textarea")][0];
|
||||
assert.equal(box().value, "old");
|
||||
doc.scenes[0].text = "from story";
|
||||
fire("change");
|
||||
assert.equal(box().value, "from story");
|
||||
box().focus();
|
||||
doc.scenes[0].text = "while typing";
|
||||
fire("change");
|
||||
assert.equal(box().value, "from story"); // not rebuilt under the caret
|
||||
box().blur();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
assert.equal(box().value, "while typing"); // caught up once the focus left
|
||||
});
|
||||
@@ -24,9 +24,14 @@ export default {
|
||||
app.title && app.title("inspector", `Scene · ${p.scenes.indexOf(sc) + 1}${unit.length > 1 ? ` · cut ${unit.indexOf(sc) + 1}/${unit.length}` : ""}`);
|
||||
};
|
||||
const sections = app.sceneSections || []; // extra rows other features put under the scene's own: { rows(scene, project), key(scene, project) }
|
||||
// The scene's text is in the key, so an edit made elsewhere (the Story box) is shown -- but not while this panel's
|
||||
// own box is being typed in: that would rebuild it under the caret. It catches up when the focus leaves.
|
||||
let shownText;
|
||||
const typing = () => body.node.contains(document.activeElement) && /^(input|textarea)$/i.test(document.activeElement.tagName);
|
||||
body.node.addEventListener("focusout", () => setTimeout(() => { if (!typing()) draw(); }));
|
||||
function draw(force) { // not on every keystroke: that would rebuild the box being typed in
|
||||
const sc = p.selected, open = p.project;
|
||||
const next = `${tab}|${open && open.id}|${p.selectedId}|${tab === "project" && open ? [open.postfix_enabled !== false, open.generation_mode, open.export_size_from, open.width, open.height, open.scenes.map((s) => s.frames_mode === "custom"), Object.keys(open.scene_renders || {}), open.scenes.length] : ""}|${tab === "scene" && sc && open ? [effFrames(sc, open), effFps(sc, open), p.scenes.indexOf(sc), sc.frames_mode, sc.fps_mode, p.scenes.length, sc.source_image, (sc.references || []).join(), sc.source_in, sc.source_dur, sc.removed_from_plan, ((open.scene_renders || {})[sc.id] || {}).durationSec, Boolean(((open.scene_renders || {})[sc.id] || {}).media), bin.version, ...sections.map((s) => { try { return s.key(sc, open); } catch { return ""; } })] : ""}`;
|
||||
const next = `${tab}|${open && open.id}|${p.selectedId}|${tab === "project" && open ? [open.postfix_enabled !== false, open.generation_mode, open.export_size_from, open.width, open.height, open.scenes.map((s) => s.frames_mode === "custom"), Object.keys(open.scene_renders || {}), open.scenes.length] : ""}|${tab === "scene" && sc && open ? [effFrames(sc, open), effFps(sc, open), p.scenes.indexOf(sc), sc.frames_mode, sc.fps_mode, p.scenes.length, sc.source_image, (sc.references || []).join(), sc.source_in, sc.source_dur, sc.removed_from_plan, (sc.source || {}).type, sc.excluded, typing() ? shownText : (shownText = sc.text), ((open.scene_renders || {})[sc.id] || {}).durationSec, Boolean(((open.scene_renders || {})[sc.id] || {}).media), bin.version, ...sections.map((s) => { try { return s.key(sc, open); } catch { return ""; } })] : ""}`;
|
||||
titled();
|
||||
if (!force && next === key) return;
|
||||
key = next;
|
||||
|
||||
+4
-3
@@ -107,15 +107,16 @@ def get(mid: str) -> dict | None:
|
||||
return next((it for it in _load_index() if it.get("id") == mid), None)
|
||||
|
||||
|
||||
def save_upload(orig_name: str, data: bytes) -> dict:
|
||||
def save_upload(orig_name: str, data: bytes, limit=...) -> dict:
|
||||
"""Write one uploaded file and record it. Raises ValueError for anything
|
||||
this store refuses -- an empty upload, an extension not on the allowlist,
|
||||
or a file over MAX_BYTES -- so the route can turn that into a 400 naming
|
||||
why, rather than a file silently written and then never explained."""
|
||||
if not data:
|
||||
raise ValueError("that file is empty")
|
||||
if len(data) > MAX_BYTES:
|
||||
raise ValueError(f"that file is over the {MAX_BYTES // (1024 * 1024)}MB limit")
|
||||
limit = MAX_BYTES if limit is ... else limit # an upload's limit (read now); a file the server made itself has none
|
||||
if limit is not None and len(data) > limit:
|
||||
raise ValueError(f"that file is over the {limit // (1024 * 1024)}MB limit")
|
||||
ext = Path(orig_name or "").suffix.lower()
|
||||
if ext not in ALLOWED_EXT:
|
||||
raise ValueError(f"{ext or 'that file type'} is not something FunPack imports")
|
||||
|
||||
@@ -322,3 +322,15 @@ def test_a_photo_thumbnail_is_turned_upright_like_the_browser_shows_it(store):
|
||||
img.save(buf, "JPEG", exif=exif)
|
||||
entry = media.save_upload("p.jpg", buf.getvalue())
|
||||
assert Image.open(media.thumb_path_for(entry["id"])).size == (160, 320)
|
||||
|
||||
|
||||
def test_a_file_the_server_made_itself_is_not_held_to_the_upload_limit(store, monkeypatch):
|
||||
from core import media
|
||||
monkeypatch.setattr(media, "MAX_BYTES", 10)
|
||||
import pytest
|
||||
with pytest.raises(ValueError, match="limit"):
|
||||
media.save_upload("a.mp4", b"x" * 11)
|
||||
try:
|
||||
media.save_upload("a.mp4", b"x" * 11, limit=None)
|
||||
except ValueError as exc:
|
||||
assert "limit" not in str(exc)
|
||||
|
||||
@@ -139,7 +139,7 @@ def routes(table, base, web):
|
||||
name += ".mp4"
|
||||
try:
|
||||
data = await asyncio.to_thread(_clip_bytes, clip)
|
||||
entry = media.save_upload(name, data)
|
||||
entry = media.save_upload(name, data, limit=None) # our own render: a long one is over the upload limit
|
||||
except (files.ClipError, stitch.RenderError) as exc:
|
||||
return bad(str(exc), 400)
|
||||
except ValueError as exc:
|
||||
|
||||
Reference in New Issue
Block a user