Confirm vision rejections, keep anatomy-only survivors, fix duplicate log lines

This commit is contained in:
justin committed 2026-09-21 16:18:31 -07:00
1 parent ff2cfa6dcf
commit c28f3bd86a
2 files changed
+67 -19

No files matched your search

+63 -16
View File
@@ -1057,6 +1057,8 @@ def is_english_word(w, extra):
return True return True
if w in vocab: if w in vocab:
return True return True
if re.fullmatch(r"[a-z]{4,}(tion|sion|ment|ness|ity|ship|able|ible|ful|less|ous|ive|ize|ise)", w):
return True
for suffix in ("s", "es", "'s", "ers", "er", "ing", "ed", "ies", "ly"): for suffix in ("s", "es", "'s", "ers", "er", "ing", "ed", "ies", "ly"):
if w.endswith(suffix) and len(w) > len(suffix) + 2: if w.endswith(suffix) and len(w) > len(suffix) + 2:
stem = w[: -len(suffix)] stem = w[: -len(suffix)]
@@ -1280,15 +1282,33 @@ INCONCLUSIVE = ("don't know", "do not know", "not sure", "cannot", "can't", "una
VISION_QUESTIONS = [ VISION_QUESTIONS = [
("extra ears", "Look closely at every animal and character. Does any single animal or character have more than two ears? Answer only yes or no."), ("extra ears", "anatomy",
("extra eyes", "Does any single animal or character have more than two eyes? Answer only yes or no."), "Look closely at every animal and character. Does any single animal or character have more than two ears? Answer only yes or no.",
("extra heads", "Does any single animal or character have more than one head, or are two bodies merged together? Answer only yes or no."), "Count the ears on each animal one at a time. Is there any single animal with three or more ears? Answer only yes or no."),
("extra limbs", "Count the legs and arms on each animal and character. Does any of them have more legs or arms than that kind of creature should have? Answer only yes or no."), ("extra eyes", "anatomy",
("realistic person", "Is there a realistic, photograph-like human being in this image, rather than a cartoon? Answer only yes or no."), "Does any single animal or character have more than two eyes? Answer only yes or no.",
("unsuitable", "Is there anything scary, violent, creepy, or inappropriate for a 4 year old child in this image? Answer only yes or no."), "Count the eyes on each face one at a time. Is there any single face with three or more eyes? Answer only yes or no."),
("extra heads", "anatomy",
"Does any single animal or character have more than one head, or are two bodies merged together? Answer only yes or no.",
"Is there any single body in this image with two heads attached to it? Answer only yes or no."),
("extra limbs", "anatomy",
"Count the legs and arms on each animal and character. Does any of them have more legs or arms than that kind of creature should have? Answer only yes or no.",
"Is there any creature with an extra leg or arm that clearly should not be there? Answer only yes or no."),
("realistic person", "safety",
"Is there a realistic, photograph-like human being in this image, rather than a cartoon? Answer only yes or no.",
"Does any person in this image look like a real photograph rather than a drawing? Answer only yes or no."),
("unsuitable", "safety",
"Is there anything scary, violent, creepy, or inappropriate for a 4 year old child in this image? Answer only yes or no.",
"Would a parent be upset to see this image in a video for small children? Answer only yes or no."),
] ]
class VisionReject(ValueError):
def __init__(self, message, safety):
super().__init__(message)
self.safety = safety
def vision_image(s, clip_path): def vision_image(s, clip_path):
src = clip_path.with_suffix(".png") src = clip_path.with_suffix(".png")
out = clip_path.with_name(f"{clip_path.stem}_check.jpg") out = clip_path.with_name(f"{clip_path.stem}_check.jpg")
@@ -1327,24 +1347,35 @@ def kids_vision_check(s, clip_path):
unclear = [] unclear = []
try: try:
desc = ollama_call(s, "Describe this image in one short sentence.", temperature=0.1, model=model, images=images, num_predict=80, as_json=False).strip() desc = ollama_call(s, "Describe this image in one short sentence.", temperature=0.1, model=model, images=images, num_predict=80, as_json=False).strip()
for key, question in VISION_QUESTIONS: def ask(q):
check()
answer = None
for _ in range(2): for _ in range(2):
reply = ollama_call(s, question, temperature=0.0, model=model, images=images, num_predict=10, as_json=False) a = yes_no(ollama_call(s, q, temperature=0.0, model=model, images=images, num_predict=10, as_json=False))
answer = yes_no(reply) if a is not None:
if answer is not None: return a
break return None
for key, kind, question, confirm in VISION_QUESTIONS:
check()
answer = ask(question)
if answer:
second = ask(confirm)
if second is False:
logger.debug("Ignored '%s' for %s, the confirming question disagreed", key, clip_path.name)
answer = False
elif second is None:
answer = None
if answer is None: if answer is None:
unclear.append(key) unclear.append(key)
elif answer: elif answer:
problems.append(key) problems.append((key, kind))
except Cancelled: except Cancelled:
raise raise
except Exception as e: except Exception as e:
raise VisionUnavailable(f"vision model '{model}' could not run: {e}") from e raise VisionUnavailable(f"vision model '{model}' could not run: {e}") from e
if problems: if problems:
raise ValueError(f"Vision check rejected {clip_path.name}: {', '.join(problems)} ({desc[:120]})") safety = any(kind == "safety" for _, kind in problems)
names = ", ".join(k for k, _ in problems)
raise VisionReject(f"Vision check rejected {clip_path.name}: {names} ({desc[:120]})", safety)
if unclear: if unclear:
raise VisionUnavailable(f"vision model '{model}' gave no clear answer about {', '.join(unclear)}") raise VisionUnavailable(f"vision model '{model}' gave no clear answer about {', '.join(unclear)}")
logger.info("Vision check passed for %s: %s", clip_path.name, desc[:160]) logger.info("Vision check passed for %s: %s", clip_path.name, desc[:160])
@@ -1722,6 +1753,8 @@ def run_job(s):
build_scene(i, scene, path) build_scene(i, scene, path)
clips.append(path) clips.append(path)
anatomy_note = ""
safety_hits = set()
if kids: if kids:
pending = list(range(1, n + 1)) pending = list(range(1, n + 1))
for rnd in range(1, 4): for rnd in range(1, 4):
@@ -1745,10 +1778,20 @@ def run_job(s):
logger.warning("Scene %d rejected (round %d): %s", i, rnd, e) logger.warning("Scene %d rejected (round %d): %s", i, rnd, e)
vision_log.append({"clip": i, "round": rnd, "result": str(e)[:300]}) vision_log.append({"clip": i, "round": rnd, "result": str(e)[:300]})
rejected.append(i) rejected.append(i)
if getattr(e, "safety", True):
safety_hits.add(i)
else:
safety_hits.discard(i)
if not rejected: if not rejected:
break break
if rnd == 3: if rnd == 3:
raise RuntimeError(f"Scenes {rejected} still failed the vision check after 3 rounds") unsafe = [i for i in rejected if i in safety_hits]
if unsafe:
raise RuntimeError(f"Scenes {unsafe} still failed the safety check after 3 rounds")
flagged = ", ".join(str(i) for i in rejected)
logger.warning("Scenes %s may still have anatomy glitches after 3 redraws, keeping them. Check them before publishing", flagged)
anatomy_note = f"Possible anatomy glitches kept in scenes {flagged}"
break
for i in rejected: for i in rejected:
set_stage(f"regenerating scene {i}/{n}", step=2 + i) set_stage(f"regenerating scene {i}/{n}", step=2 + i)
clips[i - 1].unlink(missing_ok=True) clips[i - 1].unlink(missing_ok=True)
@@ -1756,6 +1799,10 @@ def run_job(s):
pending = rejected pending = rejected
if kids: if kids:
compliance["vision_checks"] = vision_log compliance["vision_checks"] = vision_log
if anatomy_note:
compliance["anatomy_note"] = anatomy_note
prior = "; ".join(compliance.get("reviewer_notes", []))
update_history(job_id, notes=(prior + "; " if prior else "") + anatomy_note)
set_stage("stitching video", step=3 + n * 2) set_stage("stitching video", step=3 + n * 2)
final = job_dir / "final.mp4" final = job_dir / "final.mp4"
concat_clips(clips, final, s["aspect_ratio"]) concat_clips(clips, final, s["aspect_ratio"])
+4 -3
View File
@@ -461,14 +461,15 @@ return `<tr><td class="nowrap">${esc(fmtStart(h.started))}</td><td class="nowrap
$("#jobs").innerHTML=`<thead><tr><th>Started</th><th>Time</th><th>Video</th><th>Inspired by</th><th>Status</th><th></th></tr></thead><tbody>${rows||`<tr><td colspan="6" class="empty">No jobs yet. Press GO and finished videos will show up here.</td></tr>`}</tbody>`; $("#jobs").innerHTML=`<thead><tr><th>Started</th><th>Time</th><th>Video</th><th>Inspired by</th><th>Status</th><th></th></tr></thead><tbody>${rows||`<tr><td colspan="6" class="empty">No jobs yet. Press GO and finished videos will show up here.</td></tr>`}</tbody>`;
}catch(e){}} }catch(e){}}
let since=0; let since=0,polling=false;
async function pollLogs(){ async function pollLogs(){
if(polling)return;polling=true;
try{ try{
const r=await api("/api/logs?since="+since);const box=$("#log"); const r=await api("/api/logs?since="+since);const box=$("#log");
for(const [seq,level,line] of r.lines){const d=document.createElement("div");d.className=level;d.textContent=line;box.appendChild(d);since=seq} for(const [seq,level,line] of r.lines){if(seq<=since)continue;const d=document.createElement("div");d.className=level;d.textContent=line;box.appendChild(d);since=seq}
while(box.childElementCount>3000)box.firstChild.remove(); while(box.childElementCount>3000)box.firstChild.remove();
if(r.lines.length&&$("#autoscroll").checked)box.scrollTop=box.scrollHeight if(r.lines.length&&$("#autoscroll").checked)box.scrollTop=box.scrollHeight
}catch(e){}} }catch(e){}finally{polling=false}}
document.querySelectorAll(".prof-select").forEach(sel=>sel.onchange=e=>activate(e.target.value)); document.querySelectorAll(".prof-select").forEach(sel=>sel.onchange=e=>activate(e.target.value));
$("#new-profile").onclick=async()=>{ $("#new-profile").onclick=async()=>{