Keep global and scene prompts in sync; hide internal rating labels.
Scene edits rebuild the global prompt immediately; global prompt edits auto-parse back to scenes. Filter __funpack_* ratings from the editor dropdown.
This commit is contained in:
@@ -173,7 +173,18 @@ def recent_log(limit: int = 500) -> list:
|
||||
def rating_labels() -> dict:
|
||||
"""FunPack Studio V2 rating labels (for the Scene rating dropdown)."""
|
||||
labels = _funpack_attr("conditioning", "V2_RATING_LABELS")
|
||||
return {"labels": list(labels)}
|
||||
try:
|
||||
internal = {
|
||||
_funpack_attr("conditioning", "MOVIE_EDITOR_CONTINUE_RATING"),
|
||||
_funpack_attr("conditioning", "MOVIE_EDITOR_FRESH_PROMPT_RATING"),
|
||||
}
|
||||
except Exception:
|
||||
internal = {"__funpack_continue__", "__funpack_fresh_prompt__"}
|
||||
out = [
|
||||
str(l) for l in (labels or [])
|
||||
if l and str(l) not in internal and not str(l).startswith("__funpack_")
|
||||
]
|
||||
return {"labels": out}
|
||||
|
||||
|
||||
def transitions() -> dict:
|
||||
|
||||
@@ -573,11 +573,11 @@
|
||||
|
||||
const ta = el("textarea", "insp-global-ta"); ta.rows = 3; ta.value = val; ta.dataset.k = "global-prompt";
|
||||
ta.placeholder = "Anchor, scene texts, and split markers — one combined montage prompt for generation.";
|
||||
ta.oninput = () => { gpDraft = ta.value; apply.disabled = !ta.value.trim(); };
|
||||
ta.oninput = () => { gpDraft = ta.value; apply.disabled = !ta.value.trim(); S.scheduleGlobalPromptApply(ta.value); };
|
||||
sec.append(ta);
|
||||
sec.append(el("div", "insp-hint", dirty
|
||||
? "Edited — press Apply to (re)split onto the timeline."
|
||||
: "Primary way to control how long videos divide into scenes. Per-seam tweaks: Split dropdown on timeline seams (video blends are separate, on the same seam)."));
|
||||
? "Syncing to timeline…"
|
||||
: "Stays in sync with per-scene prompts. Per-seam tweaks: Split dropdown on timeline seams."));
|
||||
body.append(sec);
|
||||
}
|
||||
|
||||
@@ -638,11 +638,28 @@
|
||||
// manual render when there was nothing pending.
|
||||
setTimeout(() => {
|
||||
if (_editing) return;
|
||||
const k = t.dataset?.k;
|
||||
if (k === "global-prompt") {
|
||||
const draft = gpDraft;
|
||||
S.flushSave();
|
||||
if (draft != null) S.applyGlobalPromptQuiet(draft);
|
||||
gpDraft = null;
|
||||
render(S.get());
|
||||
return;
|
||||
}
|
||||
if (!S.flushSave()) render(S.get());
|
||||
}, 60);
|
||||
});
|
||||
|
||||
window.addEventListener("funpack-invalidate-global-prompt", () => { gpDraft = null; });
|
||||
window.addEventListener("funpack-global-prompt-updated", (e) => {
|
||||
if (_editing && document.activeElement?.dataset?.k === "global-prompt") return;
|
||||
gpDraft = null;
|
||||
const ta = body.querySelector('[data-k="global-prompt"]');
|
||||
if (ta && e.detail?.text != null) ta.value = e.detail.text;
|
||||
const hint = body.querySelector(".insp-global .insp-hint");
|
||||
if (hint) hint.textContent = "Stays in sync with per-scene prompts. Per-seam tweaks: Split dropdown on timeline seams.";
|
||||
});
|
||||
|
||||
if (window.ViewBus) window.ViewBus.subscribeInspector(render);
|
||||
else S.subscribe(render);
|
||||
|
||||
+157
-35
@@ -120,6 +120,142 @@
|
||||
try { window.dispatchEvent(new CustomEvent("funpack-invalidate-global-prompt")); } catch (_) {}
|
||||
}
|
||||
|
||||
function _dispatchGlobalPromptUpdated(text) {
|
||||
try {
|
||||
window.dispatchEvent(new CustomEvent("funpack-global-prompt-updated", { detail: { text: text || "" } }));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function _groupGenerativeUnits(scenes) {
|
||||
const units = [];
|
||||
for (const sc of scenes || []) {
|
||||
const uid = genUnitId(sc);
|
||||
if (units.length && units[units.length - 1][0] === uid) units[units.length - 1][1].push(sc);
|
||||
else units.push([uid, [sc]]);
|
||||
}
|
||||
return units;
|
||||
}
|
||||
|
||||
function _rootFromGroup(group) {
|
||||
return (group || []).find((s) => !isGenSubclip(s)) || group[0] || null;
|
||||
}
|
||||
|
||||
// Verbatim montage text rebuilt from timeline fields (display mode — transitions between scenes).
|
||||
function buildGlobalPromptFromTimeline(project) {
|
||||
if (!project) return "";
|
||||
const parts = [];
|
||||
const anchor = (project.anchor || "").trim();
|
||||
if (anchor) parts.push(anchor);
|
||||
const active = (project.scenes || []).filter((s) => !s.excluded);
|
||||
const units = _groupGenerativeUnits(active);
|
||||
units.forEach(([, group], i) => {
|
||||
const root = _rootFromGroup(group);
|
||||
if (!root) return;
|
||||
if (i === 0) {
|
||||
const intro = (project.intro_transition || "").trim();
|
||||
if (intro && anchor) parts.push(intro);
|
||||
} else {
|
||||
const prevRoot = _rootFromGroup(units[i - 1][1]);
|
||||
const tr = (prevRoot?.transition_to_next || "").trim();
|
||||
if (tr) parts.push(tr);
|
||||
}
|
||||
const text = (root.text || "").trim();
|
||||
if (text) parts.push(text);
|
||||
});
|
||||
return parts.join(" ").trim();
|
||||
}
|
||||
|
||||
function _patchAffectsCombinedPrompt(patch) {
|
||||
if (!patch || typeof patch !== "object") return false;
|
||||
return patch.text != null
|
||||
|| patch.transition_to_next != null
|
||||
|| patch.excluded != null
|
||||
|| patch.anchor != null
|
||||
|| patch.intro_transition != null
|
||||
|| patch.scenes != null;
|
||||
}
|
||||
|
||||
let _timelinePromptTimer = null;
|
||||
function syncGlobalPromptFromTimeline() {
|
||||
if (!state.project) return;
|
||||
const built = buildGlobalPromptFromTimeline(state.project);
|
||||
const prev = (state.project.global_prompt || "").trim();
|
||||
if (built === prev && (state.preview?.display_prompt || "") === built) return;
|
||||
state.project.global_prompt = built;
|
||||
state.preview = {
|
||||
...(state.preview || {}),
|
||||
display_prompt: built,
|
||||
combined_prompt: built,
|
||||
};
|
||||
_lastPreviewKey = _promptPreviewKey(state.project);
|
||||
_invalidateGlobalPromptDraft();
|
||||
_dispatchGlobalPromptUpdated(built);
|
||||
notify();
|
||||
clearTimeout(_timelinePromptTimer);
|
||||
_timelinePromptTimer = setTimeout(() => refreshPreview(true), 300);
|
||||
}
|
||||
|
||||
let _globalApplyTimer = null;
|
||||
function scheduleGlobalPromptApply(text) {
|
||||
clearTimeout(_globalApplyTimer);
|
||||
_globalApplyTimer = setTimeout(() => { applyGlobalPromptQuiet(text); }, 500);
|
||||
}
|
||||
|
||||
async function _distributeGlobalPrompt(text, res) {
|
||||
const v = res.parsed_verbatim || res.parsed_raw || res.parsed || {};
|
||||
if (!(v.scenes || []).length) return false;
|
||||
state.project.global_prompt = text;
|
||||
state.project.anchor = v.anchor || "";
|
||||
const old = state.project.scenes || [];
|
||||
const next = (v.scenes || []).map((ps, i) => {
|
||||
const base = old[i] ? JSON.parse(JSON.stringify(old[i])) : { source: { type: "carry" }, excluded: false };
|
||||
base.text = ps.text || "";
|
||||
base.transition_to_next = "";
|
||||
return base;
|
||||
});
|
||||
_applyDetectedTransitions(next, v.transitions, text);
|
||||
state.project.scenes = next;
|
||||
state.sceneGhosts = [];
|
||||
const firstId = next[0]?.id || null;
|
||||
state.selectedSceneId = firstId;
|
||||
state.selectedSceneIds = firstId ? [firstId] : [];
|
||||
_selectionAnchorId = firstId;
|
||||
state.preview = {
|
||||
...(state.preview || {}),
|
||||
combined_prompt: text,
|
||||
display_prompt: text,
|
||||
parsed: res.parsed,
|
||||
parsed_raw: res.parsed_raw,
|
||||
parsed_verbatim: res.parsed_verbatim,
|
||||
for_generation: false,
|
||||
};
|
||||
_lastPreviewKey = _promptPreviewKey(state.project);
|
||||
_invalidateGlobalPromptDraft();
|
||||
_dispatchGlobalPromptUpdated(text);
|
||||
notify();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function applyGlobalPromptQuiet(text) {
|
||||
if (!state.project) return false;
|
||||
const trimmed = String(text || "").trim();
|
||||
if (!trimmed) return false;
|
||||
if (trimmed === buildGlobalPromptFromTimeline(state.project)) return true;
|
||||
let res;
|
||||
try { res = await API.parsePrompt(state.project.id, trimmed); }
|
||||
catch (e) {
|
||||
console.warn("Global prompt parse failed:", e);
|
||||
return false;
|
||||
}
|
||||
if (!await _distributeGlobalPrompt(trimmed, res)) return false;
|
||||
clearTimeout(saveTimer);
|
||||
saveTimer = null;
|
||||
_localDirty = true;
|
||||
scheduleSaveSilent();
|
||||
refreshPreview(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
function _syncPromptPreview() {
|
||||
return (async () => {
|
||||
try {
|
||||
@@ -197,6 +333,7 @@
|
||||
_removedDuringGen.clear();
|
||||
if (window.EditorHistory) window.EditorHistory.clear();
|
||||
notify();
|
||||
syncGlobalPromptFromTimeline();
|
||||
refreshPreview();
|
||||
loadModels();
|
||||
_validateSceneRenders();
|
||||
@@ -344,10 +481,16 @@
|
||||
if (!state.project) return;
|
||||
_historyRecord();
|
||||
Object.assign(state.project, patch);
|
||||
if (_patchAffectsCombinedPrompt(patch)) syncGlobalPromptFromTimeline();
|
||||
notify();
|
||||
scheduleSave();
|
||||
}
|
||||
function patchProjectQuiet(patch) { if (!state.project) return; Object.assign(state.project, patch); scheduleSaveSilent(); }
|
||||
function patchProjectQuiet(patch) {
|
||||
if (!state.project) return;
|
||||
Object.assign(state.project, patch);
|
||||
if (_patchAffectsCombinedPrompt(patch)) syncGlobalPromptFromTimeline();
|
||||
scheduleSaveSilent();
|
||||
}
|
||||
|
||||
function scene(id) { return state.project?.scenes.find((s) => s.id === id) || null; }
|
||||
|
||||
@@ -429,6 +572,10 @@
|
||||
} else if (charsChanged) {
|
||||
notify();
|
||||
_syncPromptPreview();
|
||||
} else if (_patchAffectsCombinedPrompt(merged)) {
|
||||
syncGlobalPromptFromTimeline();
|
||||
if (quiet) scheduleSaveSilent();
|
||||
else { notify(); scheduleSave(); }
|
||||
} else if (quiet) scheduleSaveSilent();
|
||||
else { notify(); scheduleSave(); }
|
||||
}
|
||||
@@ -445,7 +592,10 @@
|
||||
if (anchorChanged) {
|
||||
clearTimeout(saveTimer); saveTimer = null;
|
||||
_localDirty = true; notify(); commit();
|
||||
} else scheduleSaveSilent();
|
||||
} else {
|
||||
if (_patchAffectsCombinedPrompt(merged)) syncGlobalPromptFromTimeline();
|
||||
scheduleSaveSilent();
|
||||
}
|
||||
}
|
||||
|
||||
function _sceneOrder() { return (state.project?.scenes || []).map((s) => s.id); }
|
||||
@@ -929,7 +1079,6 @@
|
||||
async function applyGlobalPrompt(text) {
|
||||
if (!state.project) return false;
|
||||
_historyRecord();
|
||||
state.project.global_prompt = text;
|
||||
let res;
|
||||
try { res = await API.parsePrompt(state.project.id, text); }
|
||||
catch (e) {
|
||||
@@ -937,37 +1086,10 @@
|
||||
alert("Could not parse the global prompt: " + msg);
|
||||
return false;
|
||||
}
|
||||
// The verbatim split is authoritative: correct (shortcut-aware) boundaries, scene
|
||||
// text kept exactly as typed, anchor + scenes reproduce the global prompt.
|
||||
const v = res.parsed_verbatim || res.parsed_raw || res.parsed || {};
|
||||
if (!(v.scenes || []).length) { alert("Nothing parsed — no scenes detected."); return false; }
|
||||
|
||||
state.project.anchor = v.anchor || "";
|
||||
const old = state.project.scenes || [];
|
||||
const next = (v.scenes || []).map((ps, i) => {
|
||||
const base = old[i] ? JSON.parse(JSON.stringify(old[i])) : { source: { type: "carry" }, excluded: false };
|
||||
base.text = ps.text || ""; // verbatim chunk of the global prompt
|
||||
base.transition_to_next = "";
|
||||
return base;
|
||||
});
|
||||
_applyDetectedTransitions(next, v.transitions, text);
|
||||
state.project.scenes = next;
|
||||
state.sceneGhosts = []; // scene ids changed — old ghosts no longer anchor correctly
|
||||
const firstId = next[0]?.id || null;
|
||||
state.selectedSceneId = firstId;
|
||||
state.selectedSceneIds = firstId ? [firstId] : [];
|
||||
_selectionAnchorId = firstId;
|
||||
// Refresh the inspector immediately — don't wait for save debounce + preview round-trip.
|
||||
state.preview = {
|
||||
...(state.preview || {}),
|
||||
combined_prompt: text,
|
||||
display_prompt: text,
|
||||
parsed: res.parsed,
|
||||
parsed_raw: res.parsed_raw,
|
||||
parsed_verbatim: res.parsed_verbatim,
|
||||
for_generation: false,
|
||||
};
|
||||
notify();
|
||||
if (!(await _distributeGlobalPrompt(text, res))) {
|
||||
alert("Nothing parsed — no scenes detected.");
|
||||
return false;
|
||||
}
|
||||
clearTimeout(saveTimer);
|
||||
saveTimer = null;
|
||||
state.saving = true;
|
||||
@@ -1766,7 +1888,7 @@
|
||||
addAudioTrack, updateAudioTrack, removeAudioTrack,
|
||||
resizeScene, splitScene, snapFrames, setSourceTrim, trimSceneLeft, slipScene,
|
||||
applyEnginePreset, ENGINE_PRESETS, undo, redo,
|
||||
refreshPreview, syncFromPreview, applyGlobalPrompt, generate, generateMontage, generateSelected, selectedSceneCount, renderFinal, exportSelected, interrupt, loadModels, loadImageTargets, setModelInput, setModelLink, clearNotice,
|
||||
refreshPreview, syncFromPreview, applyGlobalPrompt, applyGlobalPromptQuiet, scheduleGlobalPromptApply, buildGlobalPromptFromTimeline, syncGlobalPromptFromTimeline, generate, generateMontage, generateSelected, selectedSceneCount, renderFinal, exportSelected, interrupt, loadModels, loadImageTargets, setModelInput, setModelLink, clearNotice,
|
||||
setConditioningSlot, setSamplerSlot, setSamplerInput, setSamplerInputNow, unsetSamplerInput, setStudioInput, setStudioInputNow,
|
||||
loadMedia, uploadMedia, deleteMedia, previewMedia, clearMediaPreview, assignMediaToScene,
|
||||
loadShortcuts, saveShortcut, deleteShortcut, importShortcuts,
|
||||
|
||||
@@ -18,3 +18,20 @@ def test_ensure_funpack_path_adds_repo_root():
|
||||
sys.path[:] = [p for p in sys.path if p != root]
|
||||
bridge._ensure_funpack_path()
|
||||
assert root in sys.path
|
||||
|
||||
|
||||
def test_rating_labels_hide_internal_editor_values(monkeypatch):
|
||||
def _fake_attr(_mod, name):
|
||||
table = {
|
||||
"V2_RATING_LABELS": ["Perfect", "__funpack_continue__", "__funpack_fresh_prompt__"],
|
||||
"MOVIE_EDITOR_CONTINUE_RATING": "__funpack_continue__",
|
||||
"MOVIE_EDITOR_FRESH_PROMPT_RATING": "__funpack_fresh_prompt__",
|
||||
}
|
||||
return table[name]
|
||||
|
||||
monkeypatch.setattr(bridge, "_funpack_attr", _fake_attr)
|
||||
labels = bridge.rating_labels().get("labels") or []
|
||||
assert "__funpack_continue__" not in labels
|
||||
assert "__funpack_fresh_prompt__" not in labels
|
||||
assert all(not str(l).startswith("__funpack_") for l in labels)
|
||||
assert "Perfect" in labels
|
||||
|
||||
Reference in New Issue
Block a user