ventures: refresh havre from havre-next

This commit is contained in:
github-actions[bot] committed 2026-10-16 17:28:25 +00:00
1 parent 6a31ce7c91
commit 0b2a402e9d
7 files changed
+159 -44

No files matched your search

+15 -8
View File
@@ -22,9 +22,10 @@ export const PREFER = {
clip: ["grok-imagine-1-5-reference-to-video-private", "grok-imagine-reference-to-video-private"],
voice: ["tts-kokoro"],
hearing: ["nvidia/parakeet-tdt-0.6b-v3", "openai/whisper-large-v3"],
// an adult character's photos and clips: Venice's uncensored models, the private ones first
// an adult character's photos, clips and portraits: Venice's uncensored models, the private ones first
adultPhoto: ["qwen-edit-uncensored", "qwen-image-2-1-turbo-edit", "seedream-v5-lite-edit"],
adultClip: ["minimax-h3-max-reference-to-video", "wan-3-0-reference-to-video", "wan-2-7-reference-to-video"],
adultPortrait: ["krea-2-turbo", "chroma", "qwen-image-3"],
};
export const DEFAULT_VOICE = "af_sky";
@@ -42,7 +43,7 @@ export function chosen(list, id, kind) {
export const isClipModel = (m) => !!m && m.ref && !m.uncensored && !/enhanced/i.test(m.id);
/** an adult character's clips come from the uncensored reference-to-video models, and only from them */
export const isAdultClipModel = (m) => !!m && m.ref && !!m.uncensored;
/** and their photos from the uncensored photo models: the others refuse, or blur */
/** and their photos and portraits from the uncensored image models: the others refuse, or blur */
export const isAdultPhotoModel = (m) => !!m && !!m.uncensored;
/** a model that describes your photo when the character's own model can't see: the cheapest private one that can */
@@ -170,9 +171,14 @@ export function clipPrompt(card, shot, limit = 2500, { adult = false } = {}) {
if (adult) return clip(`${KEEP_ADULT} The reference pictures show ${card.name}. Keep how they look exactly. ${str(shot).trim()}`, limit);
return clip(`The reference pictures show ${card.name}. Keep how they look exactly. ${KEEP} ${str(shot).trim()}`, limit);
}
/** a first portrait, when a character has no picture at all */
export function portraitPrompt(card, limit = 1500) {
/**
* A portrait drawn from how they look (a first one, or a new one for their card). adult: an adult character's
* (adultCheck, drawn), which keeps every rule but the one about work, and opens with the 18+ rule.
*/
export function portraitPrompt(card, limit = 1500, { adult = false } = {}) {
const look = ((card.look && card.look.description) || card.tagline || "an original character").trim().replace(/[.!?…]+$/, "");
if (adult) return clip(`${KEEP_ADULT} Portrait photo of ${card.name}, an original adult character: ${look}. Head and shoulders, ` +
"soft light, plain background, no text.", limit);
return clip(`Portrait photo of ${card.name}, an original adult character: ${look}. ${KEEP} Head and shoulders, soft light, ` +
"plain background, no text.", limit);
}
@@ -371,15 +377,16 @@ const cardWords = (card) => [card.name, card.tagline, card.greeting, card.person
* Whether an adult character's photos and clips can be adult, and if not, why (a sentence for the page; "" for a
* character who isn't marked adult). They can when you've said you're 18 or older (of18), adult pictures are on (on),
* every picture on their card was made here with AI (made(picture): a picture someone uploaded could be of anyone), and
* nothing on their card, in the shot or in the last few messages (said) says anyone is under 18.
* nothing on their card, in the shot or in the last few messages (said) says anyone is under 18. drawn: a picture for
* their card drawn from words, made from none of the pictures on it, so those don't count.
*/
export function adultCheck(card, { of18 = false, on = true, made = () => false, shot = "", said = [] } = {}) {
export function adultCheck(card, { of18 = false, on = true, made = () => false, shot = "", said = [], drawn = false } = {}) {
if (!card || !card.adult || card.plain) return { ok: false, why: "" };
const sfw = "so their photos and clips stay safe for work.";
if (!on) return { ok: false, why: "adult photos and clips are switched off under abilities." };
if (!of18) return { ok: false, why: "adult photos and clips wait until you say you're 18 or older, under abilities." };
const pics = [card.portrait, ...((card.look && card.look.references) || [])].filter(Boolean);
if (!pics.length) return { ok: false, why: `${card.name} has no picture made here yet (card → look: make one with ai), ${sfw}` };
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}` };
const onCard = underage(cardWords(card), UNDERAGE_CARD);
if (onCard) return { ok: false, why: `their card says “${onCard}”, which sounds like someone under 18, ${sfw}` };
+51 -28
View File
@@ -1297,6 +1297,9 @@ const adultPhotoModels = () => (S.lists.inpaint ? S.lists.inpaint.filter(A.isAdu
const adultPhotoModel = () => A.chosen(adultPhotoModels(), S.ab.adultPhoto, "adultPhoto");
const adultClipModels = () => (S.lists.video ? S.lists.video.filter(A.isAdultClipModel) : null);
const adultClipModel = () => A.chosen(adultClipModels(), S.ab.adultClip, "adultClip");
const adultPortraitModels = () => (S.lists.image ? S.lists.image.filter(A.isAdultPhotoModel) : null);
// an adult character's portrait: an uncensored model if Venice has one, else the usual one (without safe mode either way)
const adultPortraitModel = () => A.chosen(adultPortraitModels(), "", "adultPortrait") || portraitModel();
const voiceModel = (c) => A.chosen(S.lists.tts, c && c.speech ? c.speech.model : "", "voice");
const hearModel = () => A.chosen(S.lists.asr, S.ab.hearModel, "hearing");
const seeModel = () => A.seer(S.models, S.ab.seeModel);
@@ -1316,7 +1319,7 @@ const KINDS = {
clip: { title: "CLIP MODEL", type: "video", head: ["MODEL", "PRIVACY", "LENGTHS", "SIZES", "SOUND"], search: "search clip models…",
note: "reference-to-video models: they keep the character the way their pictures show them. venice prices each clip before it's made. " + TEXT_NOTE },
adultPhoto: { title: "ADULT PHOTO MODEL", type: "inpaint", head: ["MODEL", "PRIVACY", "PICTURES", "$ A PHOTO"], search: "search uncensored photo models…",
note: "for adult characters' photos: an edit of pictures made here with ai, with venice's safe mode off and the 18+ rule first in every prompt. only venice's uncensored photo models are here. " + TEXT_NOTE },
note: "for adult characters' photos, and the pictures made for their card from the ones they have: an edit of pictures made here with ai, with venice's safe mode off and the 18+ rule first in every prompt. only venice's uncensored photo models are here. " + TEXT_NOTE },
adultClip: { title: "ADULT CLIP MODEL", type: "video", head: ["MODEL", "PRIVACY", "LENGTHS", "SIZES", "SOUND"], search: "search uncensored clip models…",
note: "for adult characters' clips: venice's uncensored reference-to-video models, from pictures made here with ai, with the 18+ rule first in every prompt. venice prices each clip before it's made. " + TEXT_NOTE },
voice: { title: "VOICE MODEL", type: "tts", head: ["MODEL", "PRIVACY", "VOICES", "$ A MILLION CHARACTERS"], search: "search voice models…",
@@ -1476,14 +1479,14 @@ function renderTools() {
function openAbilities() {
if (!$("#abilitydlg").open) $("#abilitydlg").showModal();
renderAbilities();
for (const t of ["inpaint", "video", "tts", "asr"]) loadList(t);
for (const t of ["inpaint", "image", "video", "tts", "asr"]) loadList(t);
loadModels();
}
function renderAbilities() {
const tb = $("#ablist"), c = card(), real = !!c && !c.plain;
tb.textContent = "";
const pm = photoModel(), cm = clipModel(), hm = hearModel(), sm = seeModel(), vm = real ? voiceModel(c) : null;
const apm = adultPhotoModel(), acm = adultClipModel();
const apm = adultPhotoModel(), acm = adultClipModel(), aim = adultPortraitModel();
const change = (kind, m) => linkish(m ? m.name : "…", () => openModels(kind), { "aria-label": `Change the ${KINDS[kind].title.toLowerCase()}` });
const tick = (label, on, set) => {
const i = h("input", { type: "checkbox" });
@@ -1512,8 +1515,8 @@ function renderAbilities() {
S.ab.adult = on;
saveAb();
adultChanged();
}), ["photos: ", change("adultPhoto", apm), " · clips: ", change("adultClip", acm),
" · only from pictures made here with ai, with the 18+ rule first in every prompt; never anyone under 18, never a real person"]),
}), ["photos: ", change("adultPhoto", apm), " · clips: ", change("adultClip", acm), ` · portraits: ${aim ? aim.name : "…"}`,
" · without safe mode, only from pictures made here with ai, with the 18+ rule first in every prompt; never anyone under 18, never a real person"]),
row("VOICE NOTES", real ? tick(`${c.name} replies with voice notes`, !!S.voiceOn[c.id], (on) => setVoiceNotes(c, on))
: "pick a character: their voice and mood are on their card.",
real ? ["voice: ", change("voice", vm), ` (${vm ? voiceOf(c, vm) : c.speech.voice || A.DEFAULT_VOICE})`, vm && vm.perMChars != null ? ` · about ${A.usd(A.voicePrice(vm, 200))} a reply` : ""] : ""),
@@ -1568,7 +1571,7 @@ function renderAsk() {
h("button", { type: "button", class: "x", "aria-label": "Close", text: "×", disabled: a.busy || null, onclick: closeAsk })));
const refs = refsOf(c);
if (!refs.length) {
const pm = portraitModel();
const pm = drawPlan(c).model;
box.append(h("p", { class: "mono", text: (c.portrait || c.look.references.length
? `${c.name}'s pictures can't be used for ${what}s: a drawing kept as an SVG, or a link to another site. add a png, jpeg or webp picture on their card (card → look), or make a first portrait from how they look.`
: `${c.name} has no picture yet, and ${what}s are made from their pictures. add one on their card (card → look), or make a first portrait from how they look.`) }));
@@ -1741,10 +1744,10 @@ async function takePhoto() {
}
/** a character with no picture at all: how they look, in words (if their card doesn't say), then a portrait from that */
async function firstPortrait() {
const a = S.ask, pm = portraitModel();
if (!a || a.busy || !pm || pm.price == null) return;
const c = S.cards.find((x) => x.id === a.card);
if (!c) return;
const a = S.ask;
if (!a || a.busy) return;
const c = S.cards.find((x) => x.id === a.card), pm = c && drawPlan(c).model;
if (!c || !pm || pm.price == null) return;
if (refsOf(c).length) { renderAsk(); writeShot(); return; } // they've had a picture since: no portrait to pay for
a.busy = true;
Object.assign(a, { status: c.look.description ? `drawing ${c.name} with ${pm.name}…` : "imagining how they look…", bad: false });
@@ -1753,7 +1756,7 @@ async function firstPortrait() {
const v = new Venice(S.key), look = await lookOf(c, v);
a.status = `drawing ${c.name} with ${pm.name}…`;
renderAsk();
const portrait = await drawPicture(c, look, pm, v);
const portrait = await drawPicture(c, look, v);
const now = S.cards.find((x) => x.id === c.id);
if (now && !refsOf(now).length) keepPicture(now, portrait, look);
a.busy = false;
@@ -1799,12 +1802,13 @@ function noteMade(pic, from = []) {
}
/**
* Whether a character's photos and clips can be adult now, and if not, why ({ ok, why }; see abilities.js adultCheck):
* t is the chat it's for (its last few messages are read too), shot what the camera will see.
* t is the chat it's for (its last few messages are read too), shot what the camera will see. drawn: a picture for
* their card drawn from words, which is made from none of the pictures on it.
*/
function adultFor(c, t = null, shot = "") {
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)), shot, said });
return A.adultCheck(c, { of18: of18(), on: S.ab.adult !== false, made: (p) => made.has(keyOf(p)), shot, said, drawn });
}
/** under the adult box on their card: whether their photos and clips can be adult, and if not, why */
function renderAdultNote(c) {
@@ -1817,23 +1821,38 @@ function renderAdultNote(c) {
/** the adult switch, or what you've said about your age, changed: what shows it catches up */
function adultChanged() {
const c = card();
if (c && !c.plain) renderAdultNote(c);
if (c && !c.plain) { renderAdultNote(c); renderMakers(); }
if ($("#abilitydlg").open) renderAbilities();
if (S.ask && !S.ask.busy) { if (S.ask.kind === "clip") { S.ask.opts = null; requote(); } else renderAsk(); }
}
// ---------- pictures for a character's card, made with AI ----------
const BLURRED = "Venice's safe mode blurred that picture, so it wasn't kept. Change how they look on their card (look), then try again.";
/** a picture for their card that safe mode blurred: for a character marked adult, why safe mode was on */
const blurredWhy = (ad) => (ad && ad.why ? `Venice's safe mode blurred that picture, so it wasn't kept. It was on because ${ad.why}` : BLURRED);
/** how they look, in words: what their card says, else a description their model writes now */
async function lookOf(c, v) {
if (c.look.description) return c.look.description;
return A.cleanText((await v.chat({ model: c.model || S.model, messages: A.lookMessages(c), maxTokens: 200, temperature: 0.9 })).text, 1500);
}
/** a picture of them drawn from how they look, by an image model, as a data: URI (one safe mode blurred isn't kept) */
async function drawPicture(c, look, pm, v) {
const { blob, blurred } = await v.generateImage({ model: pm.id, prompt: A.portraitPrompt({ ...c, look: { ...c.look, description: look } }, pm.promptLimit),
aspect: pm.aspects.includes("1:1") ? "1:1" : "" });
if (blurred) throw new Error(BLURRED);
/**
* What draws a picture of them from how they look: { model, adult, ad }. An adult character's (abilities.js
* adultCheck, drawn) goes to an uncensored model, without safe mode.
*/
function drawPlan(c, look = c.look.description) {
const ad = adultFor({ ...c, look: { ...c.look, description: look } }, null, "", { drawn: true });
return { model: ad.ok ? adultPortraitModel() : portraitModel(), adult: ad.ok, ad };
}
/**
* A picture of them drawn from how they look (checked too, if it was written just now), as a data: URI. An adult
* character's opens with the 18+ rule and goes without safe mode; any other one that safe mode blurs isn't kept.
*/
async function drawPicture(c, look, v) {
const { model: pm, adult, ad } = drawPlan(c, look);
if (!pm) throw new Error(S.listErr.image || "Venice has no model for that right now.");
const { blob, blurred } = await v.generateImage({ model: pm.id, prompt: A.portraitPrompt({ ...c, look: { ...c.look, description: look } }, pm.promptLimit, { adult }),
aspect: pm.aspects.includes("1:1") ? "1:1" : "", safe: !adult });
if (blurred) throw new Error(blurredWhy(ad));
return shrinkImage(blob, 512, "image/webp", { svg: false });
}
/**
@@ -1855,13 +1874,15 @@ let drawing = false;
function lookStatus(sel, text, bad = false) { const s = $(sel); s.textContent = text; s.classList.toggle("bad", bad); }
/**
* What a picture made for their card would be: a portrait is drawn from how they look; another picture is made from
* the pictures they have (so it's still them), or drawn too when they have none. { model, pictures, price, list }.
* the pictures they have (so it's still them), or drawn too when they have none. An adult character's goes without
* safe mode, on an uncensored model: one drawn from words always, one made from their pictures only when every one of
* those was made here (abilities.js adultCheck). { model, pictures, price, list, adult }.
*/
function makerPlan(c, as) {
const refs = as === "portrait" ? [] : refsOf(c);
if (!refs.length) { const m = portraitModel(); return { model: m, pictures: [], price: m ? m.price : null, list: "image" }; }
const m = photoModel(), pictures = refs.slice(0, Math.max(1, m ? m.maxIn : 1));
return { model: m, pictures, price: m ? A.photoPrice(m, pictures.length) : null, list: "inpaint" };
if (!refs.length) { const { model: m, adult } = drawPlan(c); return { model: m, pictures: [], price: m ? m.price : null, list: "image", adult }; }
const adult = adultFor(c).ok, m = adult ? adultPhotoModel() : photoModel(), pictures = refs.slice(0, Math.max(1, m ? m.maxIn : 1));
return { model: m, pictures, price: m ? A.photoPrice(m, pictures.length) : null, list: "inpaint", adult };
}
/** the two links that make a picture for their card, with what the next one costs */
function renderMakers() {
@@ -1880,7 +1901,7 @@ async function makePicture(as = "") {
if (!S.key) return needKey("pictures need your venice key.");
if (as !== "portrait" && c.portrait && c.look.references.length >= C.MAX_REFERENCES) return;
await loadList(makerPlan(c, as).list);
const { model: pm, pictures, price, list } = makerPlan(c, as);
const { model: pm, pictures, price, list, adult } = makerPlan(c, as);
if (!pm || price == null) { lookStatus(st, S.listErr[list] || "Venice has no model for that right now.", true); return; }
if (as === "portrait" && c.portrait && !confirm(`Replace ${c.name}'s portrait with a new one, drawn from how they look? It costs ${A.usd(price)} on your Venice key.`)) return;
drawing = true;
@@ -1891,14 +1912,15 @@ async function makePicture(as = "") {
let look = "", pic;
if (pictures.length) {
lookStatus(st, `making a picture of ${c.name} with ${pm.name}…`);
const { blob, blurred } = await v.editImage({ model: pm.id, prompt: A.photoPrompt(c, A.sheetView(c.look.references.length), pictures.length, pm.promptLimit), images: pictures });
if (blurred) throw new Error(BLURRED);
const { blob, blurred } = await v.editImage({ model: pm.id, prompt: A.photoPrompt(c, A.sheetView(c.look.references.length), pictures.length, pm.promptLimit, { adult }),
images: pictures, safe: !adult });
if (blurred) throw new Error(blurredWhy(adultFor(c)));
pic = await shrinkImage(blob, 768, "image/webp", { svg: false });
} else {
if (!c.look.description) lookStatus(st, "imagining how they look…");
look = await lookOf(c, v);
lookStatus(st, `drawing ${c.name} with ${pm.name}…`);
pic = await drawPicture(c, look, pm, v);
pic = await drawPicture(c, look, v);
}
const now = S.cards.find((x) => x.id === c.id);
if (now) keepPicture(now, pic, look, as, pictures);
@@ -2491,6 +2513,7 @@ function saveSheet() {
$("#rx-from").textContent = now.name;
renderNameNote(now);
renderAdultNote(now);
renderMakers();
}, 120);
}
+1 -1
View File
@@ -307,7 +307,7 @@
<p>What a character remembers of your chats is kept in this browser too, beside their card and never in it: a card you export or share carries none of it. Deleting a chat doesn't make them forget; their card does. Switch memory off under abilities.</p>
<p>Your key stays in this browser and goes only to api.venice.ai. The page's security policy blocks every other address, except the download links Venice hands out for clips from its private video models, which are fetched without your key. Conversations run on your Venice account, under Venice's terms.</p>
<p>Venice's adult characters stay hidden unless you switch them on and confirm you're 18 or older, and marking a character of yours adult asks the same. Photos and clips are safe for work: every one asks for that, and for adults only, and photos go through Venice's safe mode, which blurs anything adult. They're made only from the pictures on a card, which are for pictures of the character, never a photo of a real person.</p>
<p>The one exception is a character marked adult, once you've said you're 18 or older. When every picture on their card was made here with AI, their photos and clips can be adult: they're made on Venice's uncensored models, and every prompt opens with a hard rule that everyone shown is an adult, 18 or older. Never from a picture someone uploaded, which could be of anyone, and never when their card, the shot or the chat mentions anyone under 18: then it stays safe for work. Switch it off under abilities.</p>
<p>The one exception is a character marked adult, once you've said you're 18 or older. When every picture on their card was made here with AI, their photos and clips can be adult: they're made on Venice's uncensored models, and every prompt opens with a hard rule that everyone shown is an adult, 18 or older. Never from a picture someone uploaded, which could be of anyone, and never when their card, the shot or the chat mentions anyone under 18: then it stays safe for work. The pictures made for their card skip safe mode too, with the same rule first, unless one is made from an uploaded picture. Switch it off under abilities.</p>
<p>What characters do on their own stays within the daily allowance you set under abilities, and you can switch it off there. A clip is never made without your yes.</p>
<p>Hearing and seeing are off until you switch them on. Your voice note goes to Venice to be written down, and your photo goes to the model you talk to, or to the one that describes it.</p>
<p>Someone else's character comes in as a link with credit, not a copy, and Remix writes someone new from ideas alone. Names and broad ideas aren't protected by copyright; a character's written expression can be. When in doubt, ask the creator. This isn't legal advice.</p>
+4 -1
View File
@@ -154,7 +154,10 @@ The one exception is an `adult` character, once the person has said they're 18 o
card was made with AI (never one that was uploaded, which could be of anyone), their photos and clips can be adult.
`A.adultCheck(card, { of18, made, shot, said })` decides; they go to Venice's uncensored models with safe mode off,
and every prompt opens with the 18+ rule (`A.ADULT_ONLY`). Never when the card, the shot or the chat mentions anyone
under 18 (`A.underage`): then it's safe for work, as usual.
under 18 (`A.underage`): then it's safe for work, as usual. The pictures made for their card go without safe mode
too: a portrait drawn from words (`A.portraitPrompt(card, limit, { adult: true })`, when
`A.adultCheck(card, { of18, drawn: true })` says so), and one made from their pictures when every one of those was
made with AI.
## Remix with credit
+27
View File
@@ -112,6 +112,28 @@ test("adult characters: the 18+ rule is the first thing every photo and clip pro
assert.equal(A.shotMessages(mara, [], "")[0].content, A.PHOTO_SYSTEM, "safe for work unless asked");
});
test("adult characters: a portrait for their card opens with the 18+ rule too, and is drawn whatever pictures they have", () => {
const p = A.portraitPrompt(mara, 1500, { adult: true });
assert.ok(p.startsWith(A.ADULT_ONLY), p);
assert.match(p, /Never a real, identifiable person\. No gore\. Portrait photo of Mara Vell, an original adult character: /);
assert.ok(!/Safe for work/.test(p));
assert.ok(A.portraitPrompt({ ...mara, look: { description: "x ".repeat(3000) } }, 400, { adult: true }).startsWith(A.ADULT_ONLY), "a long look can't push it out");
assert.match(A.portraitPrompt(mara), /Safe for work/, "without adult, as before");
// drawn from words, it's made from none of the pictures on their card: those don't count, everything else does
const drawn = { of18: true, made: () => false, drawn: true };
assert.deepEqual(A.adultCheck({ ...mara, look: { references: ["data:image/png;base64,UPLD"] } }, drawn), { ok: true, why: "" });
assert.deepEqual(A.adultCheck({ ...mara, portrait: "", look: { references: [] } }, drawn), { ok: true, why: "" });
assert.match(A.adultCheck({ ...mara, look: { description: "a schoolgirl", references: [] } }, drawn).why, /“schoolgirl”/);
assert.match(A.adultCheck(mara, { ...drawn, of18: false }).why, /until you say you're 18 or older/);
assert.match(A.adultCheck(mara, { ...drawn, on: false }).why, /switched off under abilities/);
assert.deepEqual(A.adultCheck({ ...mara, adult: false }, drawn), { ok: false, why: "" });
assert.match(A.adultCheck({ ...mara, portrait: "", look: { references: [] } }, { ...drawn, drawn: false }).why, /no picture made here yet/);
// and it's drawn on an uncensored image model, the private ones first
const images = [{ id: "grok-imagine-image", privacy: "private" }, { id: "qwen-image-3", uncensored: true, privacy: "anonymized" },
{ id: "krea-2-turbo", uncensored: true, privacy: "private" }];
assert.equal(A.chosen(images.filter(A.isAdultPhotoModel), "", "adultPortrait").id, "krea-2-turbo");
});
test("adult characters: their clips and photos come from Venice's uncensored models, and nobody else's do", () => {
const video = [{ id: "minimax-h3-max-reference-to-video", ref: true, uncensored: true, privacy: "private" },
{ id: "grok-imagine-1-5-reference-to-video-private", ref: true, privacy: "private" }, { id: "wan-3-0-image-to-video", ref: false, uncensored: true }];
@@ -232,6 +254,11 @@ test("venice: an adult character's photo goes with safe mode off, and only when
assert.equal(calls[1].body.safe_mode, false);
for (const safe of [undefined, true, 0, "no"]) await v.editImage({ model: "m", prompt: "p", images: ["data:image/webp;base64,QUJD"], safe });
assert.deepEqual(calls.slice(2).map((c) => c.body.safe_mode), [true, true, true, true], "anything else: on");
// and a portrait for their card, drawn from words
await v.generateImage({ model: "krea-2-turbo", prompt: "p", aspect: "1:1", safe: false });
assert.deepEqual(calls.at(-1).body, { model: "krea-2-turbo", prompt: "p", safe_mode: false, format: "webp", return_binary: true, aspect_ratio: "1:1" });
for (const safe of [undefined, true, 0, "no"]) await v.generateImage({ model: "m", prompt: "p", safe });
assert.deepEqual(calls.slice(-4).map((c) => c.body.safe_mode), [true, true, true, true], "anything else: on");
});
test("venice: a voice note sends the mood as the style prompt; hearing sends the recording as a form", async () => {
+55 -3
View File
@@ -75,6 +75,8 @@ function picture(n) {
}
let pictures = 0;
const newPicture = () => picture(++pictures);
// Venice's safe mode judges the picture, not the prompt: set, the next one drawn with safe mode on comes back blurred
let blurNext = false;
const seen = { characters: [], chats: [], memories: [], photos: [], edits: [], generated: [], speech: [], heard: [], quotes: [], queued: [], retrieved: [], shares: [], completed: [] };
/** a short WAV of silence: something an audio element can load */
function wav(ms = 300) {
@@ -91,7 +93,9 @@ const MEDIA_MODELS = {
{ id: "nano-banana-2-edit", model_spec: { name: "Nano Banana 2", privacy: "anonymized", pricing: { inpaint: { usd: 0.1 } }, constraints: { combineImages: true, maxInputImages: 6 } } },
{ id: "qwen-edit-uncensored", model_spec: { name: "Qwen Edit Uncensored", privacy: "private", uncensored: true, pricing: { inpaint: { usd: 0.04 } },
constraints: { combineImages: true, promptCharacterLimit: 1500 } } }],
image: [{ id: "grok-imagine-image", model_spec: { name: "Grok Imagine", privacy: "private", pricing: { resolutions: { "1K": { usd: 0.03 } } }, constraints: { aspectRatios: ["1:1", "9:16"] } } }],
image: [{ id: "grok-imagine-image", model_spec: { name: "Grok Imagine", privacy: "private", pricing: { resolutions: { "1K": { usd: 0.03 } } }, constraints: { aspectRatios: ["1:1", "9:16"] } } },
{ id: "krea-2-turbo", model_spec: { name: "Krea 2 Turbo", privacy: "private", uncensored: true, pricing: { resolutions: { "1K": { usd: 0.04 } } },
constraints: { aspectRatios: ["1:1", "9:16"], promptCharacterLimit: 5000 } } }],
tts: [{ id: "tts-kokoro", model_spec: { name: "Kokoro", privacy: "private", pricing: { input: { usd: 3.5 } }, voices: ["af_bella", "af_sky", "am_adam"], default_format: "mp3" } },
{ id: "tts-qwen3-1-7b", model_spec: { name: "Qwen 3 TTS 1.7B", privacy: "private", pricing: { input: { usd: 112.5 } }, voices: ["Serena", "Dylan"], default_format: "mp3" } }],
asr: [{ id: "nvidia/parakeet-tdt-0.6b-v3", model_spec: { name: "Parakeet", privacy: "private", pricing: { per_audio_second: { usd: 0.0001 } } } }],
@@ -203,7 +207,12 @@ async function fakeVenice(route) {
seen.edits.push({ url, body: JSON.parse(body) });
return file(newPicture(), "image/webp");
}
if (url.endsWith("/image/generate")) { seen.generated.push(JSON.parse(body)); return file(newPicture(), "image/webp"); }
if (url.endsWith("/image/generate")) {
const g = JSON.parse(body), blur = blurNext && g.safe_mode !== false;
seen.generated.push(g);
if (blur) blurNext = false;
return file(newPicture(), "image/webp", blur ? { "x-venice-is-blurred": "true", "access-control-expose-headers": "x-venice-is-blurred" } : {});
}
if (url.endsWith("/audio/speech")) { seen.speech.push(JSON.parse(body)); return file(wav(), "audio/wav"); }
if (url.endsWith("/audio/transcriptions")) {
assert.ok(multipart(req), "a voice note goes as a form");
@@ -564,7 +573,7 @@ async function main() {
await count(page, "#ablist tr", 8);
// adult photos and clips: on for someone who said they're 18 or older (for Venice's adult characters, above)
await has(page, "#ablist tr", /^ADULT characters marked adult send adult photos and clips/);
await has(page, "#ablist tr", /photos: Qwen Edit Uncensored · clips: Wan 3\.0 Reference · only from pictures made here with ai/);
await has(page, "#ablist tr", /photos: Qwen Edit Uncensored · clips: Wan 3\.0 Reference · portraits: Krea 2 Turbo · without safe mode, only from pictures made here with ai/);
assert.ok(await page.getByLabel("characters marked adult send adult photos and clips").isChecked());
await page.getByLabel("let them hear my voice notes").check();
await page.getByLabel("let them see my photos").check();
@@ -849,12 +858,55 @@ async function main() {
await has(page, "#adult-note", /^a picture on their card wasn't made here with ai \(it was uploaded, or came from venice\)/);
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 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/);
// the pictures for her card skip safe mode too, with the 18+ rule first: a portrait drawn from words, on an
// uncensored model (safe mode would blur this one), then one made from the pictures she has
await has(page, "#portrait-make", /^make one with ai · \$0\.04$/);
const portraitWas = (await inesNow()).portrait, drawnBefore = seen.generated.length;
blurNext = true;
page.once("dialog", (d) => d.accept());
await page.click("#portrait-make");
await until(page, (p) => JSON.parse(localStorage.getItem("havre.cards.v1")).find((c) => c.id === "ines-corbel").portrait !== p, portraitWas, "her new portrait, kept");
const adultDrawn = seen.generated.at(-1);
assert.equal(seen.generated.length, drawnBefore + 1);
assert.equal(adultDrawn.model, "krea-2-turbo");
assert.equal(adultDrawn.safe_mode, false, "no safe mode for an adult character's portrait");
assert.ok(adultDrawn.prompt.startsWith("Hard rule: everyone shown is an adult, 18 or older"), adultDrawn.prompt);
assert.equal(blurNext, true, "so nothing was blurred");
blurNext = false;
await has(page, "#adult-note", /^their photos and clips can be adult/);
await has(page, "#ref-make", /^make one with ai · \$0\.04$/);
const editedBefore = seen.edits.length;
await page.click("#ref-make");
await count(page, "#refs .ref", 3);
const sheetEdit = seen.edits.at(-1).body;
assert.equal(seen.edits.length, editedBefore + 1);
assert.equal(sheetEdit.modelId || sheetEdit.model, "qwen-edit-uncensored");
assert.equal(sheetEdit.safe_mode, false);
assert.ok(sheetEdit.prompt.startsWith("Hard rule: everyone shown is an adult, 18 or older"), sheetEdit.prompt);
await has(page, "#adult-note", /^their photos and clips can be adult/);
// with adult pictures switched off, safe mode is back on, and a blur says why
await page.getByRole("button", { name: "abilities" }).click();
await page.getByLabel("characters marked adult send adult photos and clips").uncheck();
await page.click("#abclose");
await has(page, "#portrait-make", /^make one with ai · \$0\.03$/);
blurNext = true;
page.once("dialog", (d) => d.accept());
await page.click("#portrait-make");
await has(page, "#portrait-status", /^Venice's safe mode blurred that picture, so it wasn't kept\. It was on because adult photos and clips are switched off under abilities\.$/);
assert.equal(seen.generated.at(-1).safe_mode, true);
assert.equal(blurNext, false, "it was blurred");
await page.getByRole("button", { name: "abilities" }).click();
await page.getByLabel("characters marked adult send adult photos and clips").check();
await page.click("#abclose");
await has(page, "#adult-note", /^their photos and clips can be adult/);
// a shot that mentions someone under 18 stays safe for work: the other model, its price, and safe mode on
await page.locator("#chat").scrollIntoViewIfNeeded();
await page.getByRole("button", { name: "ask for a photo" }).click();
+6 -3
View File
@@ -98,9 +98,12 @@ export class Venice {
return { blob: await res.blob(), blurred: res.headers && res.headers.get ? res.headers.get("x-venice-is-blurred") === "true" : false };
}
/** a picture from words alone (a first portrait), with safe mode on. Returns { blob, blurred }. */
async generateImage({ model, prompt, aspect = "", signal } = {}) {
const body = { model, prompt, safe_mode: true, format: "webp", return_binary: true };
/**
* A picture from words alone (a portrait for a character's card). Venice's safe mode is on unless safe is false,
* which Havre passes only for an adult character (abilities.js adultCheck, drawn). Returns { blob, blurred }.
*/
async generateImage({ model, prompt, aspect = "", safe = true, signal } = {}) {
const body = { model, prompt, safe_mode: safe !== false, format: "webp", return_binary: true };
if (aspect) body.aspect_ratio = aspect;
const res = await this.raw("/image/generate", { body, signal });
return { blob: await res.blob(), blurred: res.headers && res.headers.get ? res.headers.get("x-venice-is-blurred") === "true" : false };