From a5e2e353cd5f5d529ba5e809b87ba2f882930278 Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Wed, 21 Oct 2026 22:27:20 +0000 Subject: [PATCH 1/6] havre: the check for uploaded card pictures (art vs a real person's photo, grown up) and a clearer reason --- ventures/havre/abilities.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/ventures/havre/abilities.js b/ventures/havre/abilities.js index 443a385..ca2fba4 100644 --- a/ventures/havre/abilities.js +++ b/ventures/havre/abilities.js @@ -460,7 +460,7 @@ export function adultCheck(card, { of18 = false, on = true, made = () => false, if (!of18) return { ok: false, why: "adult photos and clips wait until you say you're 18 or older, under abilities." }; const pics = drawn ? [] : [card.portrait, ...((card.look && card.look.references) || [])].filter(Boolean); if (!drawn && !pics.length) return { ok: false, why: `${card.name} has no picture made here yet (card → look: make one with ai), ${sfw}` }; - if (!pics.every((p) => made(p))) return { ok: false, why: `a picture on their card wasn't made here with ai (it was uploaded, or came from venice), ${sfw}` }; + if (!pics.every((p) => made(p))) return { ok: false, uploaded: true, why: `a picture on their card was uploaded and hasn't been checked yet, ${sfw} If it's art you have the rights to, check it below.` }; const onCard = underage(cardWords(card), UNDERAGE_CARD); if (onCard) return { ok: false, why: `their card says “${onCard}”, which sounds like someone under 18, ${sfw}` }; const now = underage([shot, said]); @@ -468,6 +468,22 @@ export function adultCheck(card, { of18 = false, on = true, made = () => false, return { ok: true, why: "" }; } +/** + * Checking an uploaded picture for an adult character: is it art (drawn, painted, rendered, AI-made) or a photograph + * of a real person, and does everyone in it look grown up? A real person's photo never becomes the reference for an + * adult picture: that's how a deepfake of someone who never agreed is made. Art you have the rights to can. + */ +export const ART_PROMPT = "Look at this picture and answer with one JSON object, nothing else: " + + "{\"kind\": \"art\" or \"photo\", \"adult\": \"yes\", \"no\" or \"unsure\"}. kind is \"art\" for a drawing, painting, " + + "illustration, anime, 3D render, or an obviously AI-generated or stylised image, and \"photo\" for what looks like a real " + + "photograph of a real person. adult is \"yes\" only if everyone shown clearly looks 18 or older."; +/** the checker's answer: { art, adult }, false for anything it didn't clearly say */ +export function parseArt(text) { + const t = str(text, 2000).toLowerCase(); + const kind = /"kind"\s*:\s*"(art|photo)"/.exec(t), adult = /"adult"\s*:\s*"(yes|no|unsure)"/.exec(t); + return { art: !!kind && kind[1] === "art", adult: !!adult && adult[1] === "yes", answered: !!kind && !!adult }; +} + /** * A short fingerprint of a picture (the data: URI a card keeps it as), for the list of pictures this browser made with * AI. It only has to tell this browser's pictures apart; it guards nothing secret. -- 2.54.0 From cc3d8ee1a29088c3c91430d8325df2af0d477225 Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Wed, 21 Oct 2026 22:27:25 +0000 Subject: [PATCH 2/6] havre: /snap in the workflow list --- ventures/havre/workflows.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ventures/havre/workflows.js b/ventures/havre/workflows.js index 18d50b3..00028f3 100644 --- a/ventures/havre/workflows.js +++ b/ventures/havre/workflows.js @@ -33,6 +33,11 @@ export const WORKFLOWS = [ about: "a few photos of them from the pictures on their card, one shoot, each a different moment (3 unless you say)", usage: "/photoshoot [how many] [what the shoot is about]", }, + { + id: "snap", command: "snap", title: "a picture to send", takes: "words", gives: "photo", doors: ["chat"], + about: "make a picture with ai and send it as yours (say what's in it: the pizza you made)", + usage: "/snap [what's in the picture]", + }, { id: "whisper", command: "whisper", title: "a whisper", takes: "card", gives: "voice note", doors: ["chat"], about: "a short ASMR voice note from them, whispered over soft rain (luna or eve; add what it's about)", -- 2.54.0 From 00aecc51eb2aa6d19954d054ff5ddde29b2bd067 Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Wed, 21 Oct 2026 22:28:51 +0000 Subject: [PATCH 3/6] havre: check uploaded card pictures as art (attest once, a private model looks; real photos never feed adult pictures); send a photo offers upload or make one with ai; /snap --- ventures/havre/app.js | 88 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/ventures/havre/app.js b/ventures/havre/app.js index d11e559..671efa9 100644 --- a/ventures/havre/app.js +++ b/ventures/havre/app.js @@ -54,7 +54,7 @@ const K = { cards: "havre.cards.v1", sel: "havre.selected", key: "havre.key", mo memory: "havre.memory.v1", // the pictures this browser made with AI (their fingerprints): an adult character's photos and clips can be adult // only when every picture on their card is one of these - made: "havre.pictures.made.v1", + made: "havre.pictures.made.v1", art: "havre.pictures.art.v1", artOk: "havre.pictures.art-ok", // the pages of Venice's characters this tab saw lately (in sessionStorage, so they last as long as the tab) pages: "havre.venice.pages", // your chats: the list of threads, each thread's messages under its own key, the threads deleted (so another tab @@ -1816,7 +1816,11 @@ function renderTools() { } } if (S.ab.hear) items.push(linkish("record a voice note", startRecording)); - if (S.ab.see) items.push(linkish("send a photo", () => $("#photo-in").click())); + if (S.photoPick) { // send a photo: one of yours, or one made with ai + items.push(linkish("upload a photo", () => { S.photoPick = false; renderTools(); $("#photo-in").click(); }), + linkish("make one with ai", () => { S.photoPick = false; renderTools(); snap($("#say").value.trim()); }), + linkish("cancel", () => { S.photoPick = false; renderTools(); })); + } else if (S.ab.see) items.push(linkish("send a photo", () => { S.photoPick = true; renderTools(); })); items.push(linkish("abilities", openAbilities)); p.append(...slashed(items)); } @@ -2224,13 +2228,57 @@ function adultFor(c, t = null, shot = "", { drawn = false } = {}) { const said = t ? t.msgs.filter((m) => m && !m.demo).slice(-12).flatMap((m) => [typeof m.content === "string" ? m.content : "", m.media && m.media.shot]) : []; return A.adultCheck(c, { of18: of18(), on: S.ab.adult !== false, made: (p) => made.has(keyOf(p)) || OUTERFACE.test(p || ""), shot, said, drawn }); } +/** uploaded pictures that passed the check: art you have the rights to, everyone in it grown up (clearArt) */ +const artKeys = () => new Set(local.get(K.art, []) || []); +const artFlags = new Map(); // this session's "why not" per picture key +/** + * Uploaded pictures on an adult character's card, checked: you confirm once that they're art you have the rights to + * and that everyone in them is an adult; then a private model that can see looks at each one (A.ART_PROMPT). Art that + * looks grown up joins the cleared list (and counts as made here); a photo of a real person never does: it stays + * fine for safe-for-work pictures, and "make one with ai" redraws the character from words if it was flagged wrongly. + */ +async function clearArt(c) { + if (!c || c.plain || !S.key) return; + const made = madeKeys(), art = artKeys(); + const pics = [c.portrait, ...((c.look && c.look.references) || [])].filter(Boolean) + .filter((p) => !made.has(keyOf(p)) && !art.has(keyOf(p)) && !OUTERFACE.test(p)); + if (!pics.length) { adultChanged(); return; } + if (!local.get(K.artOk, false)) { + if (!confirm("Check these pictures for adult photos and clips?\n\nBy going on you confirm that each one is art of a fictional character (drawn, painted, rendered or made with AI), that you own it or have permission to use it, and that everyone in it is an adult, 18 or older.\n\nA private Venice model looks at each one. A photograph of a real person is never used for adult pictures.")) return; + local.set(K.artOk, true); + } + await loadModels(); + const sm = seeModel(); + if (!sm) { toast("No Venice model can see pictures right now."); return; } + toast(`${sm.name} is checking ${pics.length === 1 ? "the picture" : pics.length + " pictures"}…`); + let ok = 0, photo = 0, young = 0, unsure = 0; + for (const p of pics) { + try { + const url = p.startsWith("data:") ? await shrinkImage(dataBlob(p), 768, "image/jpeg", { svg: false }) : p; + const r = await new Venice(S.key).chat({ model: sm.id, maxTokens: 60, temperature: 0, + messages: [{ role: "user", content: [{ type: "text", text: A.ART_PROMPT }, { type: "image_url", image_url: { url } }] }] }); + const a = A.parseArt(r.text), key = keyOf(p); + if (!a.answered) { unsure++; continue; } + if (!a.art) { photo++; artFlags.set(key, "photo"); continue; } + if (!a.adult) { young++; artFlags.set(key, "young"); continue; } + local.set(K.art, [...(local.get(K.art, []) || []).filter((k) => k !== key), key].slice(-400)); + artFlags.delete(key); + ok++; + } catch { unsure++; } + } + const bits = [ok && `${ok} cleared`, photo && `${photo} looked like a photo of a real person (if it's AI art, card → look: make one with ai redraws them from words)`, + young && `${young} didn't clearly show an adult`, unsure && `${unsure} couldn't be checked; try again`].filter(Boolean); + toast(bits.join("; ") + "."); + adultChanged(); +} /** under the adult box on their card: whether their photos and clips can be adult, and if not, why */ function renderAdultNote(c) { const p = $("#adult-note"); const ad = c && !c.plain && c.adult ? adultFor(c) : null; p.hidden = !ad; - if (ad) p.textContent = ad.ok ? "their photos and clips can be adult: every picture on their card was made here with ai. never anyone under 18, never a real person." + if (ad) p.textContent = ad.ok ? "their photos and clips can be adult: every picture on their card was made here with ai or checked as art. never anyone under 18, never a real person." : ad.why; + if (ad && !ad.ok && ad.uploaded && S.key) p.append(" ", linkish("check them", () => clearArt(card()))); } /** the adult switch, or what you've said about your age, changed: what shows it catches up */ function adultChanged() { @@ -2345,6 +2393,7 @@ function runWorkflow(id, { door = "chat", idea = "", name = "" } = {}) { if (id === "photoshoot") return photoshoot(idea, { door }); if (id === "recall") return recall(idea); if (id === "whisper") return whisper(idea); + if (id === "snap") return snap(idea); } /** the chat a workflow's result lands in: the one of theirs on screen, else their latest, else a new one */ function threadFor(c) { @@ -3002,6 +3051,39 @@ async function hearVoice(blob, to) { } // ---------- abilities: your photos ---------- +/** + * /snap (or send a photo → make one with ai): a picture you describe, made with the portrait model (safe mode on), + * sent as yours. They're told what's in it in words, and it's marked as made with ai. + */ +let snapping = false; +async function snap(words = "") { + const c = card(); + if (!c) return; + if (!S.key) return needKey("pictures need your venice key."); + if (snapping) { toast("A picture is already being made."); return; } + let what = String(words || "").trim(); + if (!what) what = (prompt("What's in the picture? (the pizza you made, the view from your window…)") || "").trim(); + if (!what) return; + await loadList("image"); + const pm = portraitModel(); + if (!pm) { err("No Venice image model is available right now."); return; } + if (!confirm(`Make a picture of “${what.slice(0, 120)}” with ${pm.name} and send it as yours? About ${pm.price != null ? A.usd(pm.price) : "a few cents"} on your Venice key.`)) return; + const to = { card: S.sel, thread: S.open, at: Date.now() }; + snapping = true; + S.inflight++; + const note = put(line(`making your picture with ${pm.name}…`, "havre", "typing")); + scrollLog(true); + try { + const { blob, blurred } = await new Venice(S.key).generateImage({ model: pm.id, prompt: what.slice(0, pm.promptLimit || 1500), safe: true }); + note.remove(); + if (blurred) { err("Venice's safe mode blurred that picture, so it wasn't sent. Try describing it differently."); return; } + const id = await M.put(blob); + if ($("#say").value.trim() === what) { $("#say").value = ""; growSay(); } + await idle(); + if (stillWith(to, "photo")) await send("", { media: { kind: "photo-in", id, saw: what, seer: "made with ai", made: true, model: pm.name, price: pm.price } }); + balanceSoon(); + } catch (e) { note.remove(); err(e.message || String(e)); } finally { snapping = false; S.inflight--; } +} async function sendPhoto(file) { S.inflight++; try { await showPhoto(file); } finally { S.inflight--; } -- 2.54.0 From ed5d711914f08363a913d3753e9da0815fd96a7f Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Wed, 21 Oct 2026 22:29:02 +0000 Subject: [PATCH 4/6] havre: cleared art counts in the adult check; /snap pictures captioned as made with ai --- ventures/havre/app.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ventures/havre/app.js b/ventures/havre/app.js index 671efa9..eb4f36d 100644 --- a/ventures/havre/app.js +++ b/ventures/havre/app.js @@ -1423,6 +1423,7 @@ function mediaLine(c, m) { disabled: md.kept === "ref" || full || null, title: "one of the pictures their photos and clips are made from", onclick: () => keepFromChat(c, m, "") })); said.append(row); } + else if (md.saw && md.made) said.append(h("span", { class: "cap", text: `made with ai${md.model ? " (" + md.model + (md.price != null ? ", " + A.usd(md.price) : "") + ")" : ""}: ${md.saw}` })); else if (md.saw) said.append(h("span", { class: "cap", text: `${md.seer || "a model"} told them: ${md.saw}` })); } else if (md.kind === "clip" && !md.id) { // a clip this browser couldn't download: it plays from Venice's link until the link lapses @@ -2226,7 +2227,8 @@ function noteMade(pic, from = []) { function adultFor(c, t = null, shot = "", { drawn = false } = {}) { const made = madeKeys(); const said = t ? t.msgs.filter((m) => m && !m.demo).slice(-12).flatMap((m) => [typeof m.content === "string" ? m.content : "", m.media && m.media.shot]) : []; - return A.adultCheck(c, { of18: of18(), on: S.ab.adult !== false, made: (p) => made.has(keyOf(p)) || OUTERFACE.test(p || ""), shot, said, drawn }); + const art = artKeys(); + return A.adultCheck(c, { of18: of18(), on: S.ab.adult !== false, made: (p) => made.has(keyOf(p)) || art.has(keyOf(p)) || OUTERFACE.test(p || ""), shot, said, drawn }); } /** uploaded pictures that passed the check: art you have the rights to, everyone in it grown up (clearArt) */ const artKeys = () => new Set(local.get(K.art, []) || []); @@ -4229,7 +4231,7 @@ function bind() { else if (e.key === K.jobs || e.key === K.jobsDone) syncJobs(); else if (e.key === K.spent) { renderTools(); if ($("#abilitydlg").open) renderAbilities(); } else if (e.key === K.memory) { const c = card(); if (c && !c.plain) renderMemory(c); } - else if (e.key === K.made || e.key === K.adult18) adultChanged(); + else if (e.key === K.made || e.key === K.art || e.key === K.adult18) adultChanged(); else if (e.key === K.ab) { S.ab = { ...S.ab, ...(local.get(K.ab, {}) || {}) }; renderTools(); -- 2.54.0 From 527dfd039f6459cd80b6ed863dc5abee1fc28b67 Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Wed, 21 Oct 2026 22:29:34 +0000 Subject: [PATCH 5/6] havre tests: the uploaded-picture reason and parseArt --- ventures/havre/tests/abilities.test.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ventures/havre/tests/abilities.test.mjs b/ventures/havre/tests/abilities.test.mjs index 00d5c9f..fb33d88 100644 --- a/ventures/havre/tests/abilities.test.mjs +++ b/ventures/havre/tests/abilities.test.mjs @@ -245,7 +245,12 @@ test("adult characters: adult pictures only when marked adult, 18+ said, every p assert.match(A.adultCheck(mara, { ...ok, of18: false }).why, /until you say you're 18 or older/); assert.match(A.adultCheck(mara, { ...ok, on: false }).why, /switched off under abilities/); const uploaded = { ...mara, look: { ...mara.look, references: [...mara.look.references, "data:image/png;base64,UPLD"] } }; - assert.match(A.adultCheck(uploaded, ok).why, /wasn't made here with ai/, "one uploaded picture keeps it safe for work"); + assert.match(A.adultCheck(uploaded, ok).why, /was uploaded and hasn't been checked yet/, "one uploaded picture keeps it safe for work"); + assert.equal(A.adultCheck(uploaded, ok).uploaded, true, "and says it can be checked"); + assert.deepEqual(A.parseArt('{"kind": "art", "adult": "yes"}'), { art: true, adult: true, answered: true }, "art, grown up: cleared"); + assert.equal(A.parseArt('{"kind": "photo", "adult": "yes"}').art, false, "a photo of a real person never is"); + assert.equal(A.parseArt('{"kind": "art", "adult": "unsure"}').adult, false, "nor art that doesn't clearly show an adult"); + assert.equal(A.parseArt("I can't tell").answered, false, "no answer, no verdict"); assert.equal(A.adultCheck({ ...mara, portrait: "https://outerface.venice.ai/x.png" }, ok).ok, false, "nor is a Venice photo made here"); assert.match(A.adultCheck({ ...mara, portrait: "", look: { references: [] } }, ok).why, /no picture made here yet/); assert.match(A.adultCheck({ ...mara, persona: "You are Mara, 16 years old." }, ok).why, /their card says “16 years old”/); -- 2.54.0 From 803547d356ba4bcdebbf5552c78e0daaa06476b7 Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Wed, 21 Oct 2026 22:29:36 +0000 Subject: [PATCH 6/6] havre smoke: the uploaded-picture reason --- ventures/havre/tests/smoke.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ventures/havre/tests/smoke.mjs b/ventures/havre/tests/smoke.mjs index c4a8809..04a9b81 100644 --- a/ventures/havre/tests/smoke.mjs +++ b/ventures/havre/tests/smoke.mjs @@ -946,14 +946,14 @@ async function main() { await page.click("#ref-add"); await page.setInputFiles("#ref-file", { name: "upload.png", mimeType: "image/png", buffer: picture(200) }); await count(page, "#refs .ref", 3); - await has(page, "#adult-note", /^a picture on their card wasn't made here with ai \(it was uploaded, or came from venice\)/); + await has(page, "#adult-note", /^a picture on their card was uploaded and hasn't been checked yet/); await page.click("#ref-make"); await count(page, "#refs .ref", 4); assert.equal(seen.edits.at(-1).body.safe_mode, true, "a picture made from an uploaded one keeps safe mode on"); assert.equal(seen.edits.at(-1).body.modelId || seen.edits.at(-1).body.model, "grok-imagine-edit"); await page.getByRole("button", { name: "Remove picture 4" }).click(); await count(page, "#refs .ref", 3); - await has(page, "#adult-note", /wasn't made here with ai/); + await has(page, "#adult-note", /was uploaded and hasn't been checked yet/); await page.getByRole("button", { name: "Remove picture 2" }).click(); await count(page, "#refs .ref", 2); await has(page, "#adult-note", /^their photos and clips can be adult/); -- 2.54.0