Add character bible to Movie Editor and rated badge on clips
Character bible stores name, appearance, body, wardrobe, always/never include tags, and reference images on the project. Text merges into the generation anchor; never_include augments the negative prompt; face ref can sync to the identity pin. Timeline clips show "Rated: …" when set.
This commit is contained in:
@@ -41,6 +41,86 @@ def normalize_guide_settings(raw: Optional[dict]) -> dict[str, bool]:
|
||||
}
|
||||
|
||||
|
||||
def normalize_character_bible(raw: Optional[dict]) -> dict[str, Any]:
|
||||
"""Structured character profile — text merges into the anchor; face ref can drive identity pin."""
|
||||
raw = raw or {}
|
||||
return {
|
||||
"name": str(raw.get("name", "")).strip(),
|
||||
"appearance": str(raw.get("appearance", "")).strip(),
|
||||
"body": str(raw.get("body", "")).strip(),
|
||||
"wardrobe": str(raw.get("wardrobe", "")).strip(),
|
||||
"always_include": str(raw.get("always_include", "")).strip(),
|
||||
"never_include": str(raw.get("never_include", "")).strip(),
|
||||
"face_ref": raw.get("face_ref") or None,
|
||||
"body_ref": raw.get("body_ref") or None,
|
||||
"detail_ref": raw.get("detail_ref") or None,
|
||||
"sync_identity_pin": bool(raw.get("sync_identity_pin", True)),
|
||||
}
|
||||
|
||||
|
||||
def build_character_bible_anchor(bible: Optional[dict]) -> str:
|
||||
"""Render bible fields into anchor prose prepended before the manual anchor."""
|
||||
b = normalize_character_bible(bible)
|
||||
parts: list[str] = []
|
||||
if b["name"]:
|
||||
parts.append(f"Character: {b['name']}.")
|
||||
if b["appearance"]:
|
||||
parts.append(f"Appearance: {b['appearance']}.")
|
||||
if b["body"]:
|
||||
parts.append(f"Body: {b['body']}.")
|
||||
if b["wardrobe"]:
|
||||
parts.append(f"Wardrobe: {b['wardrobe']}.")
|
||||
if b["always_include"]:
|
||||
text = b["always_include"].rstrip(".")
|
||||
parts.append(text + ".")
|
||||
return " ".join(parts).strip()
|
||||
|
||||
|
||||
def effective_anchor(project: Project) -> str:
|
||||
"""Character bible + manual anchor (project.anchor) sent to Studio."""
|
||||
bible_text = build_character_bible_anchor(project.character_bible)
|
||||
manual = (project.anchor or "").strip()
|
||||
if bible_text and manual:
|
||||
return f"{bible_text} {manual}"
|
||||
return bible_text or manual
|
||||
|
||||
|
||||
def effective_negative_prompt(project: Project) -> str:
|
||||
"""Project negative prompt plus bible never_include (generation only)."""
|
||||
manual = (project.negative_prompt or "").strip()
|
||||
never = normalize_character_bible(project.character_bible)["never_include"]
|
||||
if never and manual:
|
||||
return f"{manual}, {never}"
|
||||
return never or manual
|
||||
|
||||
|
||||
def resolve_identity_pin_ref(project: Project) -> Optional[str]:
|
||||
"""Face reference from the bible wins when sync_identity_pin is on."""
|
||||
b = normalize_character_bible(project.character_bible)
|
||||
if b["sync_identity_pin"] and b["face_ref"]:
|
||||
return b["face_ref"]
|
||||
return normalize_continuity_settings(project.continuity_settings).get("identity_pin_ref")
|
||||
|
||||
|
||||
def character_bible_media_refs(project: Project) -> list[str]:
|
||||
b = normalize_character_bible(project.character_bible)
|
||||
refs: list[str] = []
|
||||
for key in ("face_ref", "body_ref", "detail_ref"):
|
||||
ref = b.get(key)
|
||||
if ref:
|
||||
refs.append(ref)
|
||||
return refs
|
||||
|
||||
|
||||
def continuity_settings_for_run(project: Project) -> dict[str, Any]:
|
||||
"""Continuity knobs with bible-driven identity pin applied."""
|
||||
cs = normalize_continuity_settings(project.continuity_settings)
|
||||
pin = resolve_identity_pin_ref(project)
|
||||
if pin:
|
||||
cs = {**cs, "identity_pin_ref": pin}
|
||||
return cs
|
||||
|
||||
|
||||
def normalize_continuity_settings(raw: Optional[dict]) -> dict[str, Any]:
|
||||
"""Automated cross-scene stability — guides, mid-scene anchor, identity pin.
|
||||
|
||||
@@ -104,7 +184,7 @@ def _decayed_strength(base: float, scene_index: int, decay: float) -> float:
|
||||
|
||||
def build_auto_continuity_guides(full: Project, target: Project) -> Optional[dict]:
|
||||
"""Build per-run funpack_scene_guides when auto continuity is on."""
|
||||
cs = normalize_continuity_settings(full.continuity_settings)
|
||||
cs = continuity_settings_for_run(full)
|
||||
if not cs["auto_enabled"] or normalize_guide_settings(full.guide_settings)["stack_enabled"]:
|
||||
return None
|
||||
|
||||
@@ -163,8 +243,8 @@ def build_auto_continuity_guides(full: Project, target: Project) -> Optional[dic
|
||||
|
||||
def continuity_media_refs(full: Project, target: Project) -> list[str]:
|
||||
"""Extra media-bin ids to copy for auto continuity (pin + prior anchors)."""
|
||||
cs = normalize_continuity_settings(full.continuity_settings)
|
||||
refs: list[str] = []
|
||||
cs = continuity_settings_for_run(full)
|
||||
refs: list[str] = list(character_bible_media_refs(full))
|
||||
if cs["auto_enabled"] and cs["identity_pin_ref"]:
|
||||
refs.append(cs["identity_pin_ref"])
|
||||
payload = build_auto_continuity_guides(full, target)
|
||||
@@ -359,7 +439,8 @@ class Scene:
|
||||
class Project:
|
||||
id: str = field(default_factory=_new_id)
|
||||
name: str = "Untitled"
|
||||
anchor: str = "" # text prepended to every scene (character anchor)
|
||||
anchor: str = "" # extra anchor text (character bible prepends before this)
|
||||
character_bible: dict = field(default_factory=dict)
|
||||
# Optional master prompt in Studio combined syntax. When the user edits it and
|
||||
# hits Apply, the editor reparses it into anchor + scenes + transitions (normal
|
||||
# Studio behaviour). Stored verbatim so the field round-trips; not used at build.
|
||||
@@ -408,6 +489,7 @@ class Project:
|
||||
id=d.get("id") or _new_id(),
|
||||
name=str(d.get("name", "Untitled")),
|
||||
anchor=str(d.get("anchor", "")),
|
||||
character_bible=dict(d.get("character_bible") or {}),
|
||||
global_prompt=str(d.get("global_prompt", "")),
|
||||
negative_prompt=str(d.get("negative_prompt", "")),
|
||||
intro_transition=str(d.get("intro_transition", "")),
|
||||
@@ -519,7 +601,7 @@ def build_combined_prompt(project: Project, include_excluded: bool = False,
|
||||
scenes = [s for s in project.scenes if include_excluded or not s.excluded]
|
||||
units = group_generative_units(scenes)
|
||||
parts: list[str] = []
|
||||
anchor = (project.anchor or "").strip()
|
||||
anchor = effective_anchor(project)
|
||||
if anchor:
|
||||
parts.append(anchor)
|
||||
# Only inject separators for a MULTI-unit run: editorial subclips count as one unit.
|
||||
@@ -551,12 +633,13 @@ def generation_prompt_fingerprint(project: Project, target: Optional[Project] =
|
||||
gen_prompt = build_combined_prompt(tgt, for_generation=True)
|
||||
text_hash = hashlib.sha256(gen_prompt.encode("utf-8")).hexdigest()[:24]
|
||||
active = [s for s in tgt.scenes if not s.excluded]
|
||||
cs = normalize_continuity_settings(project.continuity_settings)
|
||||
cs = continuity_settings_for_run(project)
|
||||
gs = normalize_guide_settings(project.guide_settings)
|
||||
run_key = "\n".join([
|
||||
text_hash,
|
||||
"|".join(_scene_run_fingerprint(s) for s in active),
|
||||
cs.get("identity_pin_ref") or "",
|
||||
normalize_character_bible(project.character_bible).get("never_include") or "",
|
||||
"stack" if gs["stack_enabled"] else "",
|
||||
])
|
||||
run_hash = hashlib.sha256(run_key.encode("utf-8")).hexdigest()[:24]
|
||||
|
||||
@@ -283,6 +283,69 @@
|
||||
body.append(strip);
|
||||
}
|
||||
|
||||
function bibleRefPicker(st, label, refKey, bible) {
|
||||
const ref = bible[refKey] || null;
|
||||
const asset = ref ? (st.mediaBin || []).find((m) => m.id === ref) : null;
|
||||
const block = el("div", "bible-ref");
|
||||
const prev = el("div", "bible-ref-prev");
|
||||
if (asset && asset.kind === "image") {
|
||||
const img = el("img"); img.src = window.MovieEditorAPI.mediaUrl(asset.id); img.loading = "lazy"; prev.append(img);
|
||||
} else prev.append(el("span", "media-icon", asset ? "▶" : "◇"));
|
||||
block.append(prev);
|
||||
const info = el("div", "bible-ref-info");
|
||||
info.append(el("div", "bible-ref-name", asset ? asset.name : "No image selected"));
|
||||
const pick = el("select");
|
||||
pick.dataset.k = "bible-ref-" + refKey;
|
||||
pick.append(new Option("— none —", ""));
|
||||
(st.mediaBin || []).filter((m) => m.kind === "image").forEach((m) => {
|
||||
const o = new Option(m.name, m.id); if (m.id === ref) o.selected = true; pick.append(o);
|
||||
});
|
||||
pick.onchange = () => S.patchCharacterBible({ [refKey]: pick.value || null });
|
||||
info.append(field(label, pick));
|
||||
block.append(info);
|
||||
return block;
|
||||
}
|
||||
|
||||
function renderCharacterBible(st) {
|
||||
const bible = S.normCharacterBible(st.project.character_bible);
|
||||
const tag = el("div", "insp-tag"); tag.textContent = "Character bible"; body.append(tag);
|
||||
body.append(el("div", "insp-hint", "Structured character profile — merged into the anchor on generate. Face ref can drive the identity pin."));
|
||||
|
||||
const name = el("input"); name.value = bible.name; name.dataset.k = "bible-name";
|
||||
name.placeholder = "Character name";
|
||||
name.oninput = () => S.patchCharacterBible({ name: name.value }, true);
|
||||
body.append(field("Name", name));
|
||||
|
||||
const mkTa = (key, label, rows, placeholder) => {
|
||||
const ta = el("textarea"); ta.rows = rows; ta.value = bible[key] || ""; ta.dataset.k = "bible-" + key;
|
||||
ta.placeholder = placeholder;
|
||||
ta.oninput = () => S.patchCharacterBible({ [key]: ta.value }, true);
|
||||
body.append(field(label, ta));
|
||||
};
|
||||
mkTa("appearance", "Appearance", 2, "Face, hair, eyes, skin, distinguishing marks…");
|
||||
mkTa("body", "Body", 2, "Build, height, proportions…");
|
||||
mkTa("wardrobe", "Wardrobe", 2, "Default outfit or style…");
|
||||
mkTa("always_include", "Always include", 2, "Tags and phrases always kept in the anchor");
|
||||
mkTa("never_include", "Never include", 2, "Appended to the negative prompt on generate");
|
||||
|
||||
const refTag = el("div", "insp-tag sp-sub"); refTag.textContent = "Reference images"; body.append(refTag);
|
||||
const refs = el("div", "bible-refs");
|
||||
refs.append(bibleRefPicker(st, "Face (identity pin)", "face_ref", bible));
|
||||
refs.append(bibleRefPicker(st, "Body", "body_ref", bible));
|
||||
refs.append(bibleRefPicker(st, "Detail (tattoo / accessory)", "detail_ref", bible));
|
||||
body.append(refs);
|
||||
|
||||
const pinRow = el("label", "chk bible-pin");
|
||||
const pinCb = el("input"); pinCb.type = "checkbox"; pinCb.checked = bible.sync_identity_pin;
|
||||
pinCb.onchange = () => S.patchCharacterBible({ sync_identity_pin: pinCb.checked });
|
||||
pinRow.append(pinCb);
|
||||
pinRow.append(el("span", null, "Use face ref as identity pin"));
|
||||
body.append(pinRow);
|
||||
if (bible.sync_identity_pin && bible.face_ref) {
|
||||
body.append(el("div", "insp-hint", "Identity pin is synced from the face reference (Engine settings → Continuity)."));
|
||||
}
|
||||
}
|
||||
|
||||
function renderProject(st) {
|
||||
const p = st.project;
|
||||
title.textContent = "Project";
|
||||
@@ -305,9 +368,11 @@
|
||||
body.append(row3);
|
||||
body.append(el("div", "insp-hint", "Link FPS / Frames / Width / Height in Models → Linked inputs (Source = Project …)."));
|
||||
|
||||
renderCharacterBible(st);
|
||||
|
||||
const promptTag = el("div", "insp-tag"); promptTag.textContent = "Prompt"; body.append(promptTag);
|
||||
const anchor = el("textarea"); anchor.rows = 2; anchor.value = p.anchor || ""; anchor.dataset.k = "pj-anchor";
|
||||
anchor.placeholder = "Character / world description prepended to every scene";
|
||||
anchor.placeholder = "Extra world / setting context (character bible prepends above this)";
|
||||
anchor.oninput = () => S.patchProjectQuiet({ anchor: anchor.value });
|
||||
body.append(field("Anchor", anchor));
|
||||
body.append(field("Opening transition (anchor → scene 1)", transitionSelect(p.intro_transition || "",
|
||||
|
||||
@@ -177,6 +177,36 @@
|
||||
function patchProject(patch) { if (!state.project) return; Object.assign(state.project, patch); notify(); scheduleSave(); }
|
||||
function patchProjectQuiet(patch) { if (!state.project) return; Object.assign(state.project, patch); scheduleSaveSilent(); }
|
||||
|
||||
function normCharacterBible(raw) {
|
||||
const b = raw || {};
|
||||
return {
|
||||
name: b.name || "",
|
||||
appearance: b.appearance || "",
|
||||
body: b.body || "",
|
||||
wardrobe: b.wardrobe || "",
|
||||
always_include: b.always_include || "",
|
||||
never_include: b.never_include || "",
|
||||
face_ref: b.face_ref || null,
|
||||
body_ref: b.body_ref || null,
|
||||
detail_ref: b.detail_ref || null,
|
||||
sync_identity_pin: b.sync_identity_pin !== false,
|
||||
};
|
||||
}
|
||||
|
||||
function _characterBiblePinPatch(bible) {
|
||||
if (!bible.sync_identity_pin || !bible.face_ref) return null;
|
||||
return { ...(state.project.continuity_settings || {}), identity_pin_ref: bible.face_ref };
|
||||
}
|
||||
|
||||
function patchCharacterBible(patch, quiet) {
|
||||
if (!state.project) return;
|
||||
const next = { ...normCharacterBible(state.project.character_bible), ...patch };
|
||||
const updates = { character_bible: next };
|
||||
const pin = _characterBiblePinPatch(next);
|
||||
if (pin) updates.continuity_settings = pin;
|
||||
quiet ? patchProjectQuiet(updates) : patchProject(updates);
|
||||
}
|
||||
|
||||
function scene(id) { return state.project?.scenes.find((s) => s.id === id) || null; }
|
||||
|
||||
function genUnitId(sc) { return (sc && sc.gen_unit_id) || (sc && sc.id) || ""; }
|
||||
@@ -1179,7 +1209,7 @@
|
||||
window.Store = {
|
||||
get, set, subscribe, init,
|
||||
refreshProjectList, loadProject, newProject, deleteProject, downloadProject, importProject,
|
||||
patchProject, patchProjectQuiet, patchScene, patchSceneQuiet, flushSave, selectScene, addScene, removeScene, dismissGhost, moveScene, moveSceneTo, scene,
|
||||
patchProject, patchProjectQuiet, patchCharacterBible, normCharacterBible, patchScene, patchSceneQuiet, flushSave, selectScene, addScene, removeScene, dismissGhost, moveScene, moveSceneTo, scene,
|
||||
genUnitId, isGenSubclip, genUnitRoot, genUnitSceneIds,
|
||||
buildPreviewSegments, previewTotalSec, segmentDurationSec,
|
||||
addAudioTrack, updateAudioTrack, removeAudioTrack,
|
||||
|
||||
@@ -362,6 +362,15 @@ input[type=number] { font-family: var(--font-mono); }
|
||||
.insp-tag { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 1px; color: var(--amber); text-transform: uppercase; }
|
||||
.insp-hint { font-size: 11px; color: var(--faint); margin: -4px 0 12px; }
|
||||
.insp-block { margin-bottom: 16px; }
|
||||
.bible-refs { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; }
|
||||
.bible-ref { display: flex; gap: 10px; align-items: flex-start; padding: 8px; background: var(--ink-2);
|
||||
border: 1px solid var(--line); border-radius: 8px; }
|
||||
.bible-ref-prev { width: 52px; height: 52px; flex-shrink: 0; border-radius: 6px; overflow: hidden;
|
||||
background: var(--ink-0); border: 1px solid var(--line); display: flex; align-items: center; justify-content: center; }
|
||||
.bible-ref-prev img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.bible-ref-info { flex: 1; min-width: 0; }
|
||||
.bible-ref-name { font-size: 11px; color: var(--muted); margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.bible-pin { margin-bottom: 12px; }
|
||||
|
||||
/* global prompt — first entry in the inspector list (scrolls with content) */
|
||||
.insp-global { margin: 0 0 14px; padding: 10px 10px 8px;
|
||||
@@ -553,6 +562,9 @@ body.tl-reordering { cursor: grabbing; user-select: none; }
|
||||
.clip-text { font-size: 12px; line-height: 1.35; color: var(--text); overflow: hidden;
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||
.clip-text.empty { color: var(--faint); font-style: italic; }
|
||||
.clip-rated { font-size: 10px; font-family: var(--font-mono); color: var(--amber-2); line-height: 1.2;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-shrink: 0; }
|
||||
.clip.has-media .clip-rated { position: relative; z-index: 1; }
|
||||
.clip-actions { position: absolute; top: 5px; right: 8px; display: flex; gap: 2px; opacity: 0; transition: opacity .12s; }
|
||||
.clip:hover .clip-actions { opacity: 1; }
|
||||
.ic-btn { width: 18px; height: 18px; padding: 0; font-size: 11px; line-height: 1; border: 1px solid var(--line);
|
||||
|
||||
@@ -246,6 +246,14 @@
|
||||
const label = scene.text || (root && root.text) || (subclip ? "cut" : "empty scene");
|
||||
clip.append(el("div", "clip-text" + (label && label !== "empty scene" && label !== "cut" ? "" : " empty"), label));
|
||||
|
||||
const rating = ((root && root.rating) || scene.rating || "").trim();
|
||||
if (rating) {
|
||||
const rated = el("div", "clip-rated");
|
||||
rated.title = "Rated for FunPack Studio conditioning on next generation";
|
||||
rated.textContent = "Rated: " + rating;
|
||||
clip.append(rated);
|
||||
}
|
||||
|
||||
const actions = el("div", "clip-actions");
|
||||
const mk = (label, title, cls, fn) => { const b = el("button", "ic-btn" + (cls ? " " + cls : ""), label); b.title = title; b.onclick = (e) => { e.stopPropagation(); fn(); }; return b; };
|
||||
actions.append(mk("⟈", "Split clip at playhead", "", () => {
|
||||
|
||||
@@ -24,6 +24,7 @@ from .backend.timeline import (
|
||||
Project,
|
||||
build_combined_prompt,
|
||||
collapse_generative_units,
|
||||
effective_negative_prompt,
|
||||
gen_unit_id,
|
||||
group_generative_units,
|
||||
)
|
||||
@@ -235,15 +236,15 @@ def _run_sampler_inputs(target: Project, scene_count: int, full: Optional[Projec
|
||||
from .backend.timeline import (
|
||||
build_auto_continuity_guides,
|
||||
build_scene_guides_payload,
|
||||
continuity_settings_for_run,
|
||||
is_mixed_source,
|
||||
normalize_continuity_settings,
|
||||
normalize_guide_settings,
|
||||
)
|
||||
|
||||
proj = full or target
|
||||
samp = dict(target.sampler_inputs or {})
|
||||
active = [s for s in target.scenes if not s.excluded]
|
||||
cs = normalize_continuity_settings(proj.continuity_settings)
|
||||
cs = continuity_settings_for_run(proj)
|
||||
manual_stack = normalize_guide_settings(proj.guide_settings)["stack_enabled"]
|
||||
|
||||
auto_guides = build_auto_continuity_guides(proj, target) if cs["auto_enabled"] else None
|
||||
@@ -768,7 +769,7 @@ if web is not None and PromptServer is not None:
|
||||
"num_frames_per_scene": effective_frames,
|
||||
"frame_rate": target.frame_rate,
|
||||
"width": target.width, "height": target.height,
|
||||
"negative_prompt": target.negative_prompt or None,
|
||||
"negative_prompt": effective_negative_prompt(target) or None,
|
||||
"max_scenes": active_scene_count,
|
||||
"studio_inputs": _run_studio_inputs(target, active_scenes, prompt_changed=prompt_changed),
|
||||
"sampler_inputs": sampler_inputs,
|
||||
|
||||
@@ -15,14 +15,20 @@ from movie_editor.backend.timeline import ( # noqa: E402
|
||||
Project,
|
||||
Scene,
|
||||
STUDIO_DEFAULT_GUIDE,
|
||||
build_character_bible_anchor,
|
||||
build_combined_prompt,
|
||||
generation_prompt_fingerprint,
|
||||
build_auto_continuity_guides,
|
||||
build_mixed_solo_guides_payload,
|
||||
build_scene_anchors_payload,
|
||||
build_scene_guides_payload,
|
||||
continuity_media_refs,
|
||||
continuity_settings_for_run,
|
||||
effective_anchor,
|
||||
effective_negative_prompt,
|
||||
generation_prompt_fingerprint,
|
||||
normalize_character_bible,
|
||||
normalize_continuity_settings,
|
||||
resolve_identity_pin_ref,
|
||||
collapse_generative_units,
|
||||
group_generative_units,
|
||||
is_mixed_source,
|
||||
@@ -268,6 +274,68 @@ def test_guide_stack_accumulate_prior():
|
||||
assert scene3[1]["source"] == "scene" and scene3[1]["scene_index"] == 1
|
||||
|
||||
|
||||
def test_character_bible_merges_into_anchor():
|
||||
p = _project(
|
||||
anchor="dimly lit apartment",
|
||||
character_bible={
|
||||
"name": "Nicole",
|
||||
"appearance": "long red hair, green eyes",
|
||||
"always_include": "photorealistic",
|
||||
},
|
||||
scenes=[{"text": "walks in"}],
|
||||
)
|
||||
anchor = effective_anchor(p)
|
||||
assert anchor.startswith("Character: Nicole.")
|
||||
assert "Appearance: long red hair, green eyes." in anchor
|
||||
assert anchor.endswith("dimly lit apartment")
|
||||
prompt = build_combined_prompt(p)
|
||||
assert "Character: Nicole." in prompt
|
||||
|
||||
|
||||
def test_character_bible_never_include_merges_negative():
|
||||
p = _project(
|
||||
negative_prompt="blurry, low quality",
|
||||
character_bible={"never_include": "extra limbs, deformed hands"},
|
||||
scenes=[{"text": "a"}],
|
||||
)
|
||||
neg = effective_negative_prompt(p)
|
||||
assert "blurry" in neg
|
||||
assert "extra limbs" in neg
|
||||
|
||||
|
||||
def test_character_bible_face_ref_drives_identity_pin():
|
||||
p = _project(
|
||||
character_bible={"face_ref": "face1", "sync_identity_pin": True},
|
||||
continuity_settings={"identity_pin_ref": "other"},
|
||||
scenes=[
|
||||
{"id": "s1", "text": "a", "source": {"type": "carry"}},
|
||||
{"id": "s2", "text": "b", "source": {"type": "carry"}},
|
||||
],
|
||||
)
|
||||
assert resolve_identity_pin_ref(p) == "face1"
|
||||
cs = continuity_settings_for_run(p)
|
||||
assert cs["identity_pin_ref"] == "face1"
|
||||
guides = build_auto_continuity_guides(p, p)
|
||||
assert guides["scenes"][1][0]["media_ref"] == "face1"
|
||||
|
||||
|
||||
def test_character_bible_refs_in_continuity_media():
|
||||
p = _project(
|
||||
character_bible={"face_ref": "f1", "body_ref": "b1", "detail_ref": "d1"},
|
||||
scenes=[{"text": "a"}],
|
||||
)
|
||||
refs = continuity_media_refs(p, p)
|
||||
assert refs == ["f1", "b1", "d1"]
|
||||
|
||||
|
||||
def test_character_bible_changes_generation_hash():
|
||||
p = _project(scenes=[{"text": "walks"}])
|
||||
h1 = generation_prompt_fingerprint(p, p)["prompt_hash"]
|
||||
p.character_bible = {"name": "Alex"}
|
||||
h2 = generation_prompt_fingerprint(p, p)["prompt_hash"]
|
||||
assert h1 != h2
|
||||
|
||||
|
||||
def test_projects_store_crud(tmp_path, monkeypatch):
|
||||
from movie_editor.backend import config, projects
|
||||
monkeypatch.setattr(config, "PROJECTS_DIR", tmp_path / "projects")
|
||||
|
||||
Reference in New Issue
Block a user