ventures: refresh havre from havre-next
This commit is contained in:
1 parent
174af3371f
commit
32268f7a28
6 files changed
+256
-64
No files matched your search
+23
-12
@@ -38,6 +38,10 @@ export function pick(list, prefer = []) {
|
||||
export function chosen(list, id, kind) {
|
||||
return (list && id && list.find((m) => m.id === id)) || pick(list, PREFER[kind] || []);
|
||||
}
|
||||
/** another model of a kind, for when the ones tried turned something down: the next Havre reaches for; null if none is left */
|
||||
export function another(list, tried = [], kind = "") {
|
||||
return pick((list || []).filter((m) => m && !tried.includes(m.id)), PREFER[kind] || []);
|
||||
}
|
||||
|
||||
/** clips come from reference-to-video models; Venice's adult ones (the "enhanced" and uncensored variants) stay out */
|
||||
export const isClipModel = (m) => !!m && m.ref && !m.uncensored && !/enhanced/i.test(m.id);
|
||||
@@ -79,21 +83,27 @@ export const hearingPerMinute = (model) => (model && model.perSecond != null ? m
|
||||
|
||||
// ---------- what goes into a photo or a clip ----------
|
||||
|
||||
// Content filters (Venice's, and the ones of the models' makers) read the words in a prompt, not the "no" in front of
|
||||
// them: a rule that names what it rules out is the kind of prompt they turn down. So the rules that go into a picture
|
||||
// or a clip say what's wanted, and the shot is written to the rules without repeating them.
|
||||
const SAFE = "Keep it safe for work: no nudity, nothing sexual, no gore. Everyone shown is an adult. Never show a real, " +
|
||||
"identifiable person: no celebrities, no public figures, no one the conversation says is real; if one is asked for, show " +
|
||||
"the character alone. No words, logos or watermarks in the picture.";
|
||||
// for an adult character's photos and clips (adultCheck): the same rules, less the one about work, and a hard one first
|
||||
export const ADULT_ONLY = "Hard rule: everyone shown is an adult, 18 or older, with a clearly adult face and body. Never " +
|
||||
"anyone under 18, or anyone who looks it: no childlike features, no school uniforms, nothing that suggests a minor.";
|
||||
const ADULT = ADULT_ONLY + " If the conversation asks for anything else, leave it out and show only adults. Never show a " +
|
||||
"real, identifiable person: no celebrities, no public figures, no one the conversation says is real; if one is asked " +
|
||||
"for, show the character alone. No gore. No words, logos or watermarks in the picture.";
|
||||
// for an adult character's photos and clips (adultCheck): the same rules, less the one about work, and a hard one first.
|
||||
// It says who is shown, not who isn't: the words for that are the ones filters look for (and Havre's own check, underage)
|
||||
export const ADULT_ONLY = "Hard rule, 18+ only: everyone shown is an adult, 18 or older, with a grown adult face and body.";
|
||||
const ADULT = ADULT_ONLY + " If the conversation asks for anyone younger, or for anything that makes someone look younger, " +
|
||||
"leave it out and show only adults. Never show a real, identifiable person: no celebrities, no public figures, no one the " +
|
||||
"conversation says is real; if one is asked for, show the character alone. No gore. No words, logos or watermarks in the " +
|
||||
"picture.";
|
||||
// the shot goes to the image model as it is: rules written into it would only be words for a filter to trip on
|
||||
const UNSAID = "Follow these rules without writing them into the shot.";
|
||||
|
||||
const photoSystem = (rules) => "You write the shot for a photo that a character in a chat is about to send: what the " +
|
||||
"camera sees. Use what is happening in the conversation and what was asked for. One paragraph of at most 60 words: " +
|
||||
"where they are, what they are doing, how they are framed, the light and the mood. Call the character \"the character " +
|
||||
"from the reference picture\" and don't describe their face, hair or body: the reference picture shows how they look. " +
|
||||
rules + " Reply with the shot only.";
|
||||
rules + " " + UNSAID + " Reply with the shot only.";
|
||||
export const PHOTO_SYSTEM = photoSystem(SAFE);
|
||||
export const PHOTO_SYSTEM_ADULT = photoSystem(ADULT);
|
||||
|
||||
@@ -101,7 +111,7 @@ const clipSystem = (rules) => "You write the shot for a video clip, a few second
|
||||
"about to send. Use what is happening in the conversation and what was asked for. One paragraph of at most 60 words: " +
|
||||
"one simple action, where it happens, how the camera moves, the light and the mood. Call the character \"the character " +
|
||||
"from the reference pictures\" and don't describe their face, hair or body: the pictures show how they look. " + rules +
|
||||
" Reply with the shot only.";
|
||||
" " + UNSAID + " Reply with the shot only.";
|
||||
export const CLIP_SYSTEM = clipSystem(SAFE);
|
||||
export const CLIP_SYSTEM_ADULT = clipSystem(ADULT);
|
||||
|
||||
@@ -151,10 +161,11 @@ export function cleanText(text, max = 900) {
|
||||
return clip(t, max);
|
||||
}
|
||||
|
||||
// the rules ride at the front of every picture and clip, where a long shot or a long look can't push them out
|
||||
const KEEP = "Safe for work: fully clothed, nothing sexual, no gore; everyone shown is an adult.";
|
||||
// the rules ride at the front of every picture and clip, where a long shot or a long look can't push them out. They
|
||||
// say what's wanted: an image model reads the words, not the "no" in front of them, and so does a content filter
|
||||
const KEEP = "Safe for work: everyone in it is an adult, fully clothed.";
|
||||
// an adult character's: the 18+ rule is the very first thing the model reads
|
||||
const KEEP_ADULT = ADULT_ONLY + " Never a real, identifiable person. No gore.";
|
||||
const KEEP_ADULT = ADULT_ONLY + " Never a real, identifiable person.";
|
||||
|
||||
/** how their card says they look, on one line, without a full stop at the end ("" when it doesn't say) */
|
||||
const lookWords = (card) => str(card && card.look && card.look.description, 4000).replace(/\s+/g, " ").trim().replace(/[.!?…]+$/, "");
|
||||
@@ -300,7 +311,7 @@ const moveSystem = (rules) => "You decide whether a character in a chat does som
|
||||
"(they send that reply as a voice note too: a feeling, a laugh, a whisper, something better said aloud), \"clip\" (a " +
|
||||
"few seconds of video, only for a moment that needs moving pictures), or \"nothing\" (most of the time). Never two " +
|
||||
"photos in a row. For a photo or a clip, write the shot: what the camera sees, at most 60 words, calling them \"the " +
|
||||
"character from the reference picture\" instead of describing their face, hair or body. " + rules +
|
||||
"character from the reference picture\" instead of describing their face, hair or body. " + rules + " " + UNSAID +
|
||||
" Reply with one JSON object: {\"move\": \"photo\" | \"voice\" | \"clip\" | \"nothing\", \"shot\": \"...\"}.";
|
||||
export const MOVE_SYSTEM = moveSystem(SAFE);
|
||||
export const MOVE_SYSTEM_ADULT = moveSystem(ADULT);
|
||||
|
||||
+74
-21
@@ -1543,7 +1543,9 @@ function askFor(kind) {
|
||||
if (S.ask && S.ask.busy) { toast("One thing at a time: the last one is still on its way."); return; }
|
||||
const hint = $("#say").value.trim();
|
||||
if (hint) { $("#say").value = ""; growSay(); }
|
||||
S.ask = { kind, card: c.id, thread: S.open, hint, shot: "", busy: false, status: "", bad: false, quote: null, quoting: false, opts: null, asked: 0 };
|
||||
// refused: the photo models that turned this photo down (each gets the next one a click away)
|
||||
S.ask = { kind, card: c.id, thread: S.open, hint, shot: "", busy: false, status: "", bad: false, quote: null, quoting: false, opts: null, asked: 0,
|
||||
refused: [] };
|
||||
loadList(kind === "clip" ? "video" : "inpaint");
|
||||
if (!refsOf(c).length) loadList("image");
|
||||
renderAsk();
|
||||
@@ -1591,12 +1593,15 @@ function renderAsk() {
|
||||
box.append(h("label", {}, h("span", {}, "THE SHOT ", h("span", { class: "hint", text: "written from your conversation; change anything" })), ta));
|
||||
box.append(h("p", { class: "mono" }, linkish("write it again", writeShot, { disabled: a.busy || null }), a.hint ? ` (you asked for: ${a.hint})` : ""));
|
||||
if (!clipping) {
|
||||
const pm = askPhotoModel(a), n = pm ? Math.min(refs.length, pm.maxIn) : refs.length, price = A.photoPrice(pm, n);
|
||||
const pm = askPhotoModel(a), n = pm ? Math.min(refs.length, pm.maxIn) : refs.length, price = A.photoPrice(pm, n), alt = askAlt(a, c);
|
||||
box.append(h("p", { class: "mono" }, `from ${n} picture${n === 1 ? "" : "s"} of ${c.name} · model: `,
|
||||
linkish(pm ? pm.name : S.listErr.inpaint ? "unavailable" : "…", () => openModels(a.adult ? "adultPhoto" : "photo"), { "aria-label": "Change the photo model" }),
|
||||
price != null ? ` · ${A.usd(price)}` : ""));
|
||||
box.append(h("div", { class: "acts" },
|
||||
h("button", { type: "button", class: "btn solid small", id: "ask-go", onclick: takePhoto, text: `TAKE THE PHOTO${price != null ? " · " + A.usd(price) : ""}` }),
|
||||
alt ? h("button", { type: "button", class: "btn solid small", id: "ask-alt", onclick: () => tryAnother(alt.model),
|
||||
text: `TRY ${alt.model.name.toUpperCase()} · ${A.usd(alt.price)}` }) : null,
|
||||
h("button", { type: "button", class: alt ? "btn small" : "btn solid small", id: "ask-go", onclick: takePhoto,
|
||||
text: `${alt ? "TRY AGAIN" : "TAKE THE PHOTO"}${price != null ? " · " + A.usd(price) : ""}` }),
|
||||
h("button", { type: "button", class: "btn small", text: "CANCEL", onclick: closeAsk, disabled: a.busy || null })));
|
||||
} else {
|
||||
const cm = askClipModel(a);
|
||||
@@ -1646,9 +1651,30 @@ function renderAsk() {
|
||||
}
|
||||
/** the button that spends money is live only when there's a shot and a price for exactly what would be made */
|
||||
function syncAsk() {
|
||||
const a = S.ask, go = $("#ask-go");
|
||||
const a = S.ask, go = $("#ask-go"), alt = $("#ask-alt");
|
||||
if (!a || !go) return;
|
||||
go.disabled = a.busy || !a.shot.trim() || (a.kind === "clip" ? !quoteFits(a) : photoPriceNow(a) == null);
|
||||
if (alt) alt.disabled = a.busy || !a.shot.trim();
|
||||
}
|
||||
/**
|
||||
* The photo model turned this photo down: the next photo model Havre reaches for, and its price ({ model, price }), or
|
||||
* null when there's none left, or the photo model now isn't one that said no.
|
||||
*/
|
||||
function askAlt(a, c) {
|
||||
const pm = askPhotoModel(a);
|
||||
if (!pm || !a.refused.includes(pm.id)) return null;
|
||||
const m = A.another(a.adult ? adultPhotoModels() : S.lists.inpaint, a.refused, a.adult ? "adultPhoto" : "photo");
|
||||
const price = m ? A.photoPrice(m, Math.min(refsOf(c).length, Math.max(1, m.maxIn))) : null;
|
||||
return m && price != null ? { model: m, price } : null;
|
||||
}
|
||||
/** the photo taken with that other model, which stays the photo model from then on (as if picked under model:) */
|
||||
function tryAnother(m) {
|
||||
const a = S.ask;
|
||||
if (!a || a.busy || !m || !a.shot.trim()) return;
|
||||
if (a.adult) S.ab.adultPhoto = m.id; else S.ab.photo = m.id;
|
||||
saveAb();
|
||||
toast(`${a.adult ? "Adult photo model" : "Photo model"}: ${m.name}, from now on.`);
|
||||
takePhoto();
|
||||
}
|
||||
/** a photo's price, from the photo model and the pictures it would use; null when Venice gives none */
|
||||
function photoPriceNow(a) {
|
||||
@@ -1708,7 +1734,9 @@ function requote() {
|
||||
if (!a.busy) renderAsk(); else syncAsk();
|
||||
}, 200);
|
||||
}
|
||||
/** what went wrong with a photo or a clip that was asked for, and what to do about it */
|
||||
function trouble(e, c) {
|
||||
if (e.refused) return `${e.message} Change the shot, or try another model.`;
|
||||
return e.status === 400 && c.engine.venice_character && !c.look.references.length
|
||||
? `Venice couldn't use ${c.name}'s photo (${e.message}). Add a picture of them on their card (card → look) and try again.` : e.message;
|
||||
}
|
||||
@@ -1737,11 +1765,14 @@ async function takePhoto() {
|
||||
...(adult ? { adult: true } : {}), ...(blurred ? { blurred: true } : {}) } });
|
||||
} catch (e) {
|
||||
a.busy = false;
|
||||
if (e.refused && !a.refused.includes(pm.id)) a.refused.push(pm.id); // the next model is a click away (askAlt)
|
||||
Object.assign(a, { status: trouble(e, c), bad: true });
|
||||
renderAsk();
|
||||
}
|
||||
balanceSoon();
|
||||
}
|
||||
/** a picture for their card that Venice turned down as against its content rules: what to change */
|
||||
const redraw = (e) => (e.refused ? `${e.message} Change how they look (card → look), and try again.` : e.message);
|
||||
/** 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;
|
||||
@@ -1766,7 +1797,7 @@ async function firstPortrait() {
|
||||
writeShot();
|
||||
} catch (e) {
|
||||
a.busy = false;
|
||||
Object.assign(a, { status: e.message, bad: true });
|
||||
Object.assign(a, { status: redraw(e), bad: true });
|
||||
renderAsk();
|
||||
}
|
||||
balanceSoon();
|
||||
@@ -1927,7 +1958,7 @@ async function makePicture(as = "") {
|
||||
lookStatus(st, "");
|
||||
toast(as === "portrait" || !c.portrait ? `${c.name} has a new portrait.` : `A new picture of ${c.name} is on their card.`);
|
||||
} catch (e) {
|
||||
lookStatus(st, e.message, true);
|
||||
lookStatus(st, redraw(e), true);
|
||||
} finally {
|
||||
drawing = false;
|
||||
S.inflight--;
|
||||
@@ -2011,25 +2042,47 @@ async function onTheirOwn(t) {
|
||||
else if (move === "clip" && !lately) await offerClip(t, shot);
|
||||
} catch { /* then they just text */ } finally { refund(held); S.moving.delete(t.id); }
|
||||
}
|
||||
/** something they started on their own that didn't come (a photo, a voice note): said in its chat, or in a word elsewhere */
|
||||
function missed(t, what, e) {
|
||||
const c = cardOf(t);
|
||||
if (!c || !S.threads[t.id]) return;
|
||||
const why = `${c.name}'s ${what} didn't come: ${(e && e.message) || "something went wrong."}`;
|
||||
if (here(t)) err(why); else toast(why);
|
||||
}
|
||||
/**
|
||||
* A photo they send on their own, out of the day's allowance. One that Venice turns down as against its content rules
|
||||
* goes once more, to the next photo model; one that still doesn't come is said in its chat, so "is taking a photo…"
|
||||
* never just stops.
|
||||
*/
|
||||
async function ownPhoto(t, shot) {
|
||||
const c = cardOf(t);
|
||||
await loadList("inpaint");
|
||||
if (!c) return;
|
||||
const adult = adultFor(c, t, shot).ok; // an adult character's can be adult (abilities.js adultCheck)
|
||||
const pm = adult ? adultPhotoModel() : photoModel(), refs = refsOf(c);
|
||||
if (!pm || !refs.length) return; // no picture of them yet: nothing to send
|
||||
const pics = refs.slice(0, Math.max(1, pm.maxIn)), price = A.photoPrice(pm, pics.length);
|
||||
if (!reserve(price)) return; // set aside first: nothing else can spend it meanwhile
|
||||
doing(t, "is taking a photo…");
|
||||
try {
|
||||
const { blob, blurred } = await new Venice(S.key).editImage({ model: pm.id, prompt: A.photoPrompt(c, shot, pics.length, pm.promptLimit, { adult }),
|
||||
images: pics, safe: !adult });
|
||||
const id = await M.put(blob);
|
||||
done(t);
|
||||
deliver(t, { role: "assistant", content: "", media: { kind: "photo", id, shot, model: pm.name, price, own: true, ...(adult ? { adult: true } : {}),
|
||||
...(blurred ? { blurred: true } : {}) } });
|
||||
balanceSoon();
|
||||
} catch (e) { refund(price); throw e; } finally { done(t); }
|
||||
const refs = refsOf(c), tried = [];
|
||||
let pm = adult ? adultPhotoModel() : photoModel(), why = null;
|
||||
while (pm && refs.length && tried.length < 2) { // no picture of them yet: nothing to send
|
||||
const pics = refs.slice(0, Math.max(1, pm.maxIn)), price = A.photoPrice(pm, pics.length);
|
||||
if (!reserve(price)) break; // set aside first: nothing else can spend it meanwhile
|
||||
tried.push(pm.id);
|
||||
doing(t, "is taking a photo…");
|
||||
try {
|
||||
const { blob, blurred } = await new Venice(S.key).editImage({ model: pm.id, prompt: A.photoPrompt(c, shot, pics.length, pm.promptLimit, { adult }),
|
||||
images: pics, safe: !adult });
|
||||
const id = await M.put(blob);
|
||||
done(t);
|
||||
deliver(t, { role: "assistant", content: "", media: { kind: "photo", id, shot, model: pm.name, price, own: true, ...(adult ? { adult: true } : {}),
|
||||
...(blurred ? { blurred: true } : {}) } });
|
||||
balanceSoon();
|
||||
return;
|
||||
} catch (e) {
|
||||
refund(price);
|
||||
why = e;
|
||||
pm = e.refused ? A.another(adult ? adultPhotoModels() : S.lists.inpaint, tried, adult ? "adultPhoto" : "photo") : null;
|
||||
}
|
||||
}
|
||||
done(t);
|
||||
if (why) missed(t, "photo", why);
|
||||
}
|
||||
async function ownVoice(t) {
|
||||
const c = cardOf(t), msg = [...t.msgs].reverse().find((m) => m.role === "assistant" && !m.media && !m.demo);
|
||||
@@ -2047,7 +2100,7 @@ async function ownVoice(t) {
|
||||
done(t);
|
||||
placeVoice(t, msg);
|
||||
balanceSoon();
|
||||
} catch (e) { refund(price); throw e; } finally { done(t); voicing.delete(msg); }
|
||||
} catch (e) { refund(price); missed(t, "voice note", e); } finally { done(t); voicing.delete(msg); }
|
||||
}
|
||||
/** a voice note that came after its words were shown: it goes above them, where it would have been */
|
||||
function placeVoice(t, m) {
|
||||
|
||||
@@ -152,6 +152,12 @@ In Havre a character can send more than texts. `abilities.js` has the prompts an
|
||||
Rules for photos and clips: safe for work, everyone shown is an adult, never a real or identifiable person. Make
|
||||
them only from pictures the person can see on the card.
|
||||
|
||||
Word those rules for what's wanted, never by naming what they rule out: content filters (Venice's, and the ones of
|
||||
the models' makers) read the words, not the "no" in front of them, so a rule that names what it forbids gets a
|
||||
harmless prompt turned down. When Venice turns one down anyway (a 422 its docs call `CONTENT_POLICY_VIOLATION`;
|
||||
`VeniceError.refused`), offer the next model (`A.another(list, tried, kind)`) instead of a dead end. A photo a
|
||||
character starts on their own tries one other model, then says in the chat that it didn't come.
|
||||
|
||||
The one exception is an `adult` character, once the person has said they're 18 or older: when every picture on their
|
||||
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,
|
||||
|
||||
@@ -46,6 +46,21 @@ test("the model in use: the one picked if Venice still has it, else Havre's pref
|
||||
assert.equal(A.seer([{ id: "d", vision: false }]), null);
|
||||
});
|
||||
|
||||
test("another model, for when the ones tried turned something down: the next Havre reaches for, never one tried", () => {
|
||||
const edits = [{ id: "nano-banana-2-edit", privacy: "anonymized" }, { id: "grok-imagine-edit", privacy: "private" },
|
||||
{ id: "firered-image-edit", privacy: "private" }, { id: "qwen-edit-uncensored", privacy: "private", uncensored: true }];
|
||||
assert.equal(A.another(edits, ["grok-imagine-edit"], "photo").id, "firered-image-edit", "Havre's next preference");
|
||||
assert.equal(A.another(edits, ["firered-image-edit"], "photo").id, "grok-imagine-edit");
|
||||
assert.equal(A.another(edits, ["grok-imagine-edit", "firered-image-edit"], "photo").id, "qwen-edit-uncensored", "then a private one");
|
||||
assert.equal(A.another(edits, ["grok-imagine-edit", "firered-image-edit", "qwen-edit-uncensored"], "photo").id, "nano-banana-2-edit", "then any");
|
||||
assert.equal(A.another(edits, edits.map((m) => m.id), "photo"), null, "none left");
|
||||
assert.equal(A.another(null, [], "photo"), null);
|
||||
const adult = edits.filter(A.isAdultPhotoModel);
|
||||
assert.equal(A.another(adult, ["qwen-edit-uncensored"], "adultPhoto"), null, "an adult character's photos stay on the uncensored models");
|
||||
assert.equal(A.another([...adult, { id: "seedream-v5-lite-edit", uncensored: true }, { id: "qwen-image-2-1-turbo-edit", uncensored: true }],
|
||||
["qwen-edit-uncensored"], "adultPhoto").id, "qwen-image-2-1-turbo-edit");
|
||||
});
|
||||
|
||||
test("prices: a photo with extra pictures, a voice note, a minute of hearing, written plainly", () => {
|
||||
assert.equal(A.photoPrice(photos[2], 1), 0.12);
|
||||
assert.equal(A.photoPrice(photos[2], 3), 0.189);
|
||||
@@ -76,8 +91,8 @@ test("the shot is written from the conversation, with the rules, and the photo k
|
||||
const p = A.photoPrompt(ines, "She leans on the rail at dusk.", 2, 120);
|
||||
assert.ok(p.startsWith("The reference pictures show Ines Corbel."));
|
||||
assert.ok(p.length <= 120);
|
||||
assert.match(A.portraitPrompt(ines), /^Portrait photo of Ines Corbel, an original adult character: forties.*Safe for work: fully clothed, nothing sexual, no gore; everyone shown is an adult\./);
|
||||
assert.match(A.photoPrompt(ines, "On deck."), /^The reference picture shows Ines Corbel\. Safe for work.*everyone shown is an adult\. Keep their face.* New photo: On deck\. A natural photo with no text in it\.$/);
|
||||
assert.match(A.portraitPrompt(ines), /^Portrait photo of Ines Corbel, an original adult character: forties.*Safe for work: everyone in it is an adult, fully clothed\./);
|
||||
assert.match(A.photoPrompt(ines, "On deck."), /^The reference picture shows Ines Corbel\. Safe for work: everyone in it is an adult, fully clothed\. Keep their face.* New photo: On deck\. A natural photo with no text in it\.$/);
|
||||
assert.match(A.clipPrompt(ines, "She waves."), /^The reference pictures show Ines Corbel\. Safe for work.*How they look: forties.* The clip: She waves\.$/);
|
||||
assert.match(A.lookMessages(ines)[1].content, /Ferry pilot/);
|
||||
// a picture for their card, made from the ones they have: the next view of a character sheet
|
||||
@@ -111,11 +126,11 @@ test("pictures made from their pictures keep their face and follow how their car
|
||||
}
|
||||
assert.ok(A.photoPrompt(long, shot, 1, 1500, { adult: true }).startsWith(A.ADULT_ONLY));
|
||||
// with hardly any room, the look goes before the shot does
|
||||
assert.match(A.photoPrompt(long, "On deck.", 1, 380), /Keep their face, hair and build/);
|
||||
assert.match(A.photoPrompt(long, "On deck.", 1, 355), /Keep their face, hair and build/);
|
||||
// a portrait: the look takes the room the rules and the framing leave
|
||||
const drawn = A.portraitPrompt(long);
|
||||
assert.ok(drawn.length <= 1500);
|
||||
assert.match(drawn, /… Safe for work: fully clothed, nothing sexual, no gore; everyone shown is an adult\. Head and shoulders, soft light, plain background, no text\.$/);
|
||||
assert.match(drawn, /… Safe for work: everyone in it is an adult, fully clothed\. Head and shoulders, soft light, plain background, no text\.$/);
|
||||
assert.match(A.portraitPrompt(long, 1500, { adult: true }), /^Hard rule.*Portrait photo of Ines Corbel.*… Head and shoulders, soft light, plain background, no text\.$/);
|
||||
assert.match(A.portraitPrompt({ ...bare, tagline: "Ferry pilot." }), /an original adult character: Ferry pilot\. Safe for work/);
|
||||
assert.match(A.portraitPrompt({ ...bare, tagline: "" }), /an original adult character: an original character\. Safe for work/);
|
||||
@@ -126,7 +141,7 @@ const mara = C.normalizeCard({ name: "Mara Vell", persona: "You are Mara Vell, a
|
||||
portrait: "data:image/webp;base64,AAAA", look: { references: ["data:image/webp;base64,BBBB"] } });
|
||||
|
||||
test("adult characters: the 18+ rule is the first thing every photo and clip prompt says, and the shot's", () => {
|
||||
assert.match(A.ADULT_ONLY, /^Hard rule: everyone shown is an adult, 18 or older, with a clearly adult face and body\. Never anyone under 18/);
|
||||
assert.equal(A.ADULT_ONLY, "Hard rule, 18+ only: everyone shown is an adult, 18 or older, with a grown adult face and body.");
|
||||
for (const p of [A.photoPrompt(mara, "On the stage.", 1, 1500, { adult: true }), A.photoPrompt(mara, "On the stage.", 2, 1500, { adult: true }),
|
||||
A.clipPrompt(mara, "She bows.", 2500, { adult: true })]) {
|
||||
assert.ok(p.startsWith(A.ADULT_ONLY), p);
|
||||
@@ -148,10 +163,34 @@ 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("the words Havre puts in a picture or a clip are none a content filter turns down, and an adult's still open with the 18+ rule", () => {
|
||||
// what filters look for is what a rule rules out, named: Havre's own words say what's wanted instead
|
||||
const TRIPS = /\b(?:sex\w*|nud\w*|naked|gor[ey]|minors?|child\w*|kids?|school\w*|teen\w*|under[\s-]?(?:18|eighteen|age\w*)|young\w*)\b/i;
|
||||
for (const adult of [false, true]) {
|
||||
const who = adult ? mara : ines, bare = { ...who, look: { ...who.look, description: "" } };
|
||||
for (const p of [A.photoPrompt(who, "On deck.", 1, 1500, { adult }), A.photoPrompt(who, "On deck.", 2, 1500, { adult }),
|
||||
A.photoPrompt(bare, "On deck.", 1, 1500, { adult }), A.photoPrompt(who, A.sheetView(0), 1, 1500, { adult }), A.clipPrompt(who, "She waves.", 2500, { adult }),
|
||||
A.clipPrompt(bare, "She waves.", 2500, { adult }), A.portraitPrompt(who, 1500, { adult }), A.portraitPrompt({ ...bare, tagline: "" }, 1500, { adult })]) {
|
||||
assert.ok(!TRIPS.test(p), `“${(p.match(TRIPS) || [])[0]}” in: ${p}`);
|
||||
assert.equal(A.underage(p), "", p);
|
||||
if (adult) assert.ok(p.startsWith("Hard rule, 18+ only: everyone shown is an adult, 18 or older, with a grown adult face and body."), p);
|
||||
else assert.match(p, /Safe for work: everyone in it is an adult, fully clothed\./);
|
||||
}
|
||||
}
|
||||
// the shot is written to the rules without repeating them (it goes to the image model as it's written), and a rule
|
||||
// echoed into it anyway can't make Havre's own check say someone is under 18
|
||||
for (const s of [A.PHOTO_SYSTEM, A.CLIP_SYSTEM, A.MOVE_SYSTEM, A.PHOTO_SYSTEM_ADULT, A.CLIP_SYSTEM_ADULT, A.MOVE_SYSTEM_ADULT]) {
|
||||
assert.match(s, /Follow these rules without writing them into the shot\. (Reply with the shot only\.|Reply with one JSON object)/);
|
||||
assert.equal(A.underage(s), "", s);
|
||||
}
|
||||
// the model writing an adult character's shot is told what the 18+ rule keeps out, in words that name no one
|
||||
assert.match(A.PHOTO_SYSTEM_ADULT, /Hard rule, 18\+ only: everyone shown is an adult, 18 or older, with a grown adult face and body\. If the conversation asks for anyone younger, or for anything that makes someone look younger, leave it out and show only adults\./);
|
||||
});
|
||||
|
||||
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.match(p, /Never a real, identifiable person\. 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");
|
||||
@@ -376,10 +415,21 @@ test("venice: the other model lists come with their prices, voices, lengths and
|
||||
assert.deepEqual((await v.mediaModels("image"))[0].aspects, ["1:1"]);
|
||||
});
|
||||
|
||||
test("venice: what Venice says when it turns something down", async () => {
|
||||
const v = new Venice("sk", { fetch: fake((p) => (p === "/video/queue" ? { status: 422, body: { error: "Prompt violates content policy" } } : { status: 503, body: {} })).f });
|
||||
await assert.rejects(v.queueVideo({ model: "m", prompt: "p", duration: "5s" }), /content rules: Prompt violates content policy/);
|
||||
await assert.rejects(v.speak({ model: "m", input: "x" }), /busy at Venice/);
|
||||
test("venice: what Venice says when it turns something down, and when a 422 is something else", async () => {
|
||||
const answers = {
|
||||
"/video/queue": { status: 422, body: { error: "Your prompt violates the content policy of Venice.ai or the model provider" } },
|
||||
"/image/edit": { status: 422, body: { error: { code: "CONTENT_POLICY_VIOLATION", message: "Blocked by the model provider: a real person." } } },
|
||||
"/image/multi-edit": { status: 422, body: {} },
|
||||
"/image/generate": { status: 422, body: { error: "Image dimensions exceed the maximum allowed." } },
|
||||
};
|
||||
const v = new Venice("sk", { fetch: fake((p) => answers[p] || { status: 503, body: {} }).f });
|
||||
const said = (re, refused, status = 422) => (e) => { assert.match(e.message, re); assert.equal(e.refused, refused); assert.equal(e.status, status); return true; };
|
||||
await assert.rejects(v.queueVideo({ model: "m", prompt: "p", duration: "5s" }), said(/^Venice turned that down as against its content rules\.$/, true));
|
||||
await assert.rejects(v.editImage({ model: "m", prompt: "p", images: ["data:image/png;base64,AA"] }),
|
||||
said(/^Venice turned that down as against its content rules \(“Blocked by the model provider: a real person”\)\.$/, true));
|
||||
await assert.rejects(v.editImage({ model: "m", prompt: "p", images: ["a", "b"] }), said(/^Venice turned that down as against its content rules\.$/, true), "nothing said: a no");
|
||||
await assert.rejects(v.generateImage({ model: "m", prompt: "p" }), said(/^Image dimensions exceed the maximum allowed\.$/, false), "not a no on content");
|
||||
await assert.rejects(v.speak({ model: "m", input: "x" }), said(/busy at Venice/, false, 503));
|
||||
});
|
||||
|
||||
// ---------- on their own ----------
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
// a card import and a chat without a key, the key and the balance, Venice's characters (photos through Venice's
|
||||
// image service that stay put between searches, pages seen lately not asked for again, tags, adult ones only once
|
||||
// asked for) and one who says hello first, the model picker, a plain chat, several chats at once (and in two tabs),
|
||||
// the abilities (a photo, voice notes, a clip priced first, your voice note through a fake microphone, your photo
|
||||
// seen or described, a first portrait), a remix end to end, the exports, the retirement of the first version's cast
|
||||
// and its conversations, and the page's security policy.
|
||||
// the abilities (a photo, and one Venice turns down, voice notes, a clip priced first, your voice note through a fake
|
||||
// microphone, your photo seen or described, a first portrait), a remix end to end, the exports, the retirement of the
|
||||
// first version's cast and its conversations, and the page's security policy.
|
||||
// Fails on any console error. With --shots, it also takes the screenshots: desktop.png and og.png are the ones the
|
||||
// site serves (tools/build.sh copies them to shots/).
|
||||
//
|
||||
@@ -77,7 +77,12 @@ 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: [] };
|
||||
// the photo models that turn every photo down, as Venice does when a prompt trips its content filter (or the model maker's)
|
||||
const refusing = new Set();
|
||||
// set, the voice models are busy at Venice
|
||||
let speechDown = false;
|
||||
const seen = { characters: [], chats: [], memories: [], photos: [], edits: [], refused: [], generated: [], speech: [], heard: [], quotes: [], queued: [], retrieved: [],
|
||||
shares: [], completed: [] };
|
||||
/** a short WAV of silence: something an audio element can load */
|
||||
function wav(ms = 300) {
|
||||
const n = Math.round(8000 * ms / 1000), b = Buffer.alloc(44 + n * 2);
|
||||
@@ -90,6 +95,7 @@ const MEDIA_MODELS = {
|
||||
inpaint: [
|
||||
{ id: "grok-imagine-edit", model_spec: { name: "Grok Imagine", privacy: "private", pricing: { inpaint: { usd: 0.03 }, resolutions: { "1K": { usd: 0.03 }, "2K": { usd: 0.04 } } },
|
||||
constraints: { combineImages: true, promptCharacterLimit: 1500 } } },
|
||||
{ id: "firered-image-edit", model_spec: { name: "FireRed", privacy: "private", pricing: { inpaint: { usd: 0.04 } }, constraints: { promptCharacterLimit: 1500 } } },
|
||||
{ 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 } } }],
|
||||
@@ -112,6 +118,8 @@ const MEDIA_MODELS = {
|
||||
const handWritten = (o) => "Sure! Here it is:\n```json\n" + JSON.stringify(o, null, 1).replace(/\\n/g, "\n").replace(/\n}$/, ",\n}") + "\n```";
|
||||
/** the chat calls where someone talks: not the ones deciding what a character does on their own, or writing down what they remember */
|
||||
const talks = () => seen.chats.filter((b) => !/^You (decide whether|keep the memory)/.test(String((b.messages[0] || {}).content || "")));
|
||||
/** words a content filter turns a prompt down for: none of them are Havre's own (abilities.test.mjs has the rest) */
|
||||
const TRIPS = /\b(?:sex\w*|nud\w*|naked|gor[ey]|minors?|child\w*|kids?|school\w*|teen\w*|under[\s-]?(?:18|eighteen|age\w*)|young\w*)\b/i;
|
||||
const multipart = (req) => (req.headers()["content-type"] || "").startsWith("multipart/form-data");
|
||||
const CHARACTERS = [
|
||||
{ slug: "sable-penrose", name: "Sable Penrose", description: "Your new neighbor needs help moving in.", author: "btrzm",
|
||||
@@ -161,7 +169,7 @@ async function fakeVenice(route) {
|
||||
reply = /^Them:.*\bSend me a pic/i.test(said) ? json({ move: "photo", shot: "The character from the reference picture holds up a mug\nin a half-unpacked kitchen." })
|
||||
: /^Them:.*\bFilm the harbor/i.test(said) ? json({ move: "clip", shot: "The character from the reference pictures pans across the harbor at dusk." })
|
||||
: /^Them:.*\bWhisper it/i.test(said) ? json({ move: "voice", shot: "" }) : json({ move: "nothing", shot: "" });
|
||||
} else if (lastSaid.startsWith("[Time passes")) {
|
||||
} else if (/^(?:\(You sent me [^\n]*\)\n\n)?\[Time passes/.test(lastSaid)) { // after a word about the photo she sent last, if she did
|
||||
reply = "You're back!\n\nI kept the kettle warm.";
|
||||
} else if (system.startsWith("You write the shot for a photo")) {
|
||||
reply = "Shot: The character from the reference picture sits on a stack of moving boxes, grinning, warm lamplight.";
|
||||
@@ -204,7 +212,9 @@ async function fakeVenice(route) {
|
||||
}
|
||||
const file = (bytes, type, headers = {}) => route.fulfill({ status: 200, contentType: type, body: bytes, headers: { ...cors, ...headers } });
|
||||
if (url.endsWith("/image/edit") || url.endsWith("/image/multi-edit")) {
|
||||
seen.edits.push({ url, body: JSON.parse(body) });
|
||||
const e = JSON.parse(body), model = e.model || e.modelId;
|
||||
if (refusing.has(model)) { seen.refused.push(model); return ok({ error: "Your prompt violates the content policy of Venice.ai or the model provider" }, 422); }
|
||||
seen.edits.push({ url, body: e });
|
||||
return file(newPicture(), "image/webp");
|
||||
}
|
||||
if (url.endsWith("/image/generate")) {
|
||||
@@ -213,7 +223,11 @@ async function fakeVenice(route) {
|
||||
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/speech")) {
|
||||
if (speechDown) return ok({ error: "The model is at capacity." }, 503);
|
||||
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");
|
||||
seen.heard.push(body);
|
||||
@@ -634,6 +648,29 @@ async function main() {
|
||||
await page.getByRole("button", { name: "NOT NOW" }).click();
|
||||
await count(page, "#log .line.offer", 0);
|
||||
assert.equal(seen.queued.length, queuedBefore, "no clip without a yes");
|
||||
// a voice note she starts on her own that doesn't come: the chat says so, instead of going quiet after "is recording…"
|
||||
speechDown = true;
|
||||
await answered(page, async () => { await page.fill("#say", "Whisper it to me again"); await page.keyboard.press("Enter"); });
|
||||
await has(page, "#log .line.err .said", /^Sable Penrose's voice note didn't come: That model is busy at Venice right now\./, 10000);
|
||||
assert.equal(await page.locator("#log [data-typing]").count(), 0, "nobody is still recording");
|
||||
speechDown = false;
|
||||
// Venice turns her photo down: she tries once more with the next photo model, and when that one says no too, the
|
||||
// chat says her photo didn't come, instead of going quiet after "is taking a photo…"; the allowance gets it all back
|
||||
refusing.add("grok-imagine-edit").add("firered-image-edit");
|
||||
const refusedBefore = seen.refused.length, spentNo = await spent(), picsNo = await page.locator("#log .pic img").count();
|
||||
await answered(page, async () => { await page.fill("#say", "Send me a pic of the hallway"); await page.keyboard.press("Enter"); });
|
||||
await has(page, "#log .line.err .said", /^Sable Penrose's photo didn't come: Venice turned that down as against its content rules\.$/, 10000);
|
||||
assert.deepEqual(seen.refused.slice(refusedBefore), ["grok-imagine-edit", "firered-image-edit"], "once more, with the next photo model");
|
||||
assert.equal(await page.locator("#log [data-typing]").count(), 0, "nobody is still taking a photo");
|
||||
assert.equal(await page.locator("#log .pic img").count(), picsNo);
|
||||
assert.ok(await spent() - spentNo < 0.001, "neither photo came out of the allowance: " + (await spent() - spentNo));
|
||||
// one says no and the next says yes: her photo comes
|
||||
refusing.delete("firered-image-edit");
|
||||
await answered(page, async () => { await page.fill("#say", "Send me a pic of the stairs"); await page.keyboard.press("Enter"); });
|
||||
await count(page, "#log .pic img", picsNo + 1, 10000);
|
||||
await has(page, "#log .line.media .cap", /photo · FireRed · \$0\.04 · on their own/);
|
||||
assert.deepEqual(seen.refused.slice(refusedBefore), ["grok-imagine-edit", "firered-image-edit", "grok-imagine-edit"]);
|
||||
refusing.clear();
|
||||
// and when you come back after a while, she texts first in her chat, and Captain Ro in his, which isn't on screen
|
||||
await page.evaluate(() => {
|
||||
const index = JSON.parse(localStorage.getItem("havre.threads.v1"));
|
||||
@@ -648,8 +685,9 @@ async function main() {
|
||||
});
|
||||
await page.reload();
|
||||
await has(page, "#log .line.them .said", /I kept the kettle warm\./);
|
||||
const nudges = () => talks().filter((b) => /^\[Time passes/.test(String(b.messages.at(-1).content)));
|
||||
assert.match(nudges()[0].messages.at(-1).content, /^\[Time passes: about 8 hours since you last talked\. They just came back to the chat\./);
|
||||
const nudges = () => talks().filter((b) => /^(?:\(You sent me [^\n]*\)\n\n)?\[Time passes/.test(String(b.messages.at(-1).content)));
|
||||
assert.match(nudges()[0].messages.at(-1).content, /^\(You sent me a photo: The character from the reference picture holds up a mug in a half-unpacked kitchen\.\)\n\n\[Time passes: about 8 hours since you last talked\. They just came back to the chat\./,
|
||||
"the photo she sent last, then the time that's passed");
|
||||
await poll(() => nudges().length === 2, "Captain Ro texting first", 9000);
|
||||
assert.match(nudges()[1].messages.at(-1).content, /^\[Time passes: about 8 hours since you last talked\. Reach out to them first/);
|
||||
await has(page, "#chats-new", /^1$/);
|
||||
@@ -683,9 +721,29 @@ async function main() {
|
||||
const wren = await page.evaluate(() => JSON.parse(localStorage.getItem("havre.cards.v1")).find((c) => c.name === "Wren"));
|
||||
assert.match(wren.portrait, /^data:image\/webp;base64,/);
|
||||
assert.match(wren.look.description, /^Thirties, wiry/);
|
||||
// the photo model turns the photo down: the panel says so, with the next photo model a click away
|
||||
refusing.add("grok-imagine-edit");
|
||||
const editsWren = seen.edits.length, refusedWren = seen.refused.length;
|
||||
await page.click("#ask-go");
|
||||
await has(page, "#ask .status", /^Venice turned that down as against its content rules\. Change the shot, or try another model\.$/);
|
||||
assert.deepEqual(seen.refused.slice(refusedWren), ["grok-imagine-edit"]);
|
||||
assert.equal(seen.edits.length, editsWren, "nothing made");
|
||||
await has(page, "#ask-alt", /^TRY FIRERED · \$0\.04$/);
|
||||
await has(page, "#ask-go", /^TRY AGAIN · \$0\.03$/);
|
||||
await shot(page, "desktop-ask-refused.png");
|
||||
await page.click("#ask-alt");
|
||||
await has(page, "#toast", /^Photo model: FireRed, from now on\.$/);
|
||||
await until(page, () => [...document.querySelectorAll("#log .pic img")].some((i) => i.src.startsWith("blob:")), null, "Wren's photo");
|
||||
assert.equal(seen.edits.at(-1).body.model, "firered-image-edit");
|
||||
assert.match(seen.edits.at(-1).body.image, /^[A-Za-z0-9+/]+=*$/, "a picture of the card's own goes as base64");
|
||||
await has(page, "#log .line.media .cap", /photo · FireRed · \$0\.04 · save/);
|
||||
assert.equal(await page.evaluate(() => JSON.parse(localStorage.getItem("havre.abilities")).photo), "firered-image-edit", "the photo model from now on");
|
||||
refusing.clear();
|
||||
// and back to Grok Imagine, for what comes next
|
||||
await page.getByRole("button", { name: "abilities" }).click();
|
||||
await page.getByRole("button", { name: "Change the photo model", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Use Grok Imagine", exact: true }).click();
|
||||
await page.click("#abclose");
|
||||
// photos and voice notes stay through a reload, from this browser's own store
|
||||
await page.reload();
|
||||
await until(page, () => [...document.querySelectorAll("#log .pic img")].some((i) => i.src.startsWith("blob:") && i.complete && i.naturalWidth > 0), null, "the photo after a reload");
|
||||
@@ -833,7 +891,7 @@ async function main() {
|
||||
await page.locator("#chat").scrollIntoViewIfNeeded();
|
||||
await page.getByRole("button", { name: "ask for a photo" }).click();
|
||||
await until(page, () => { const t = document.querySelector("#ask-shot"); return !!t && /moving boxes/.test(t.value) && !t.disabled; }, null, "her shot");
|
||||
assert.match(talks().at(-1).messages[0].content, /^You write the shot for a photo.*Hard rule: everyone shown is an adult, 18 or older/, "the shot is written to the 18+ rule");
|
||||
assert.match(talks().at(-1).messages[0].content, /^You write the shot for a photo.*Hard rule, 18\+ only: everyone shown is an adult, 18 or older/, "the shot is written to the 18+ rule");
|
||||
await has(page, "#ask", /from 2 pictures of Ines Corbel · model: Qwen Edit Uncensored · \$0\.04/);
|
||||
await has(page, "#ask-rules", /^adult \(18\+\): made only from pictures made here with ai, with the 18\+ rule first in its prompt/);
|
||||
await has(page, "#ask-go", /^TAKE THE PHOTO · \$0\.04$/);
|
||||
@@ -844,12 +902,13 @@ async function main() {
|
||||
const adultEdit = seen.edits.at(-1).body;
|
||||
assert.equal(adultEdit.modelId || adultEdit.model, "qwen-edit-uncensored");
|
||||
assert.equal(adultEdit.safe_mode, false, "safe mode off, for her");
|
||||
assert.ok(adultEdit.prompt.startsWith("Hard rule: everyone shown is an adult, 18 or older"), adultEdit.prompt);
|
||||
assert.ok(adultEdit.prompt.startsWith("Hard rule, 18+ only: everyone shown is an adult, 18 or older"), adultEdit.prompt);
|
||||
assert.ok(!TRIPS.test(adultEdit.prompt), "nothing in it for a content filter to turn down: " + adultEdit.prompt);
|
||||
await has(page, "#log .line.media .cap", /photo · Qwen Edit Uncensored · \$0\.04 · 18\+ · save/);
|
||||
// a clip from her: an uncensored model, priced first, its shot written to the 18+ rule too
|
||||
await page.getByRole("button", { name: "ask for a clip" }).click();
|
||||
await until(page, () => { const t = document.querySelector("#ask-shot"); return !!t && /waves from the doorway/.test(t.value) && !t.disabled; }, null, "her clip's shot");
|
||||
assert.match(talks().at(-1).messages[0].content, /^You write the shot for a video clip.*Hard rule: everyone shown is an adult, 18 or older/);
|
||||
assert.match(talks().at(-1).messages[0].content, /^You write the shot for a video clip.*Hard rule, 18\+ only: everyone shown is an adult, 18 or older/);
|
||||
await has(page, "#ask", /model: Wan 3\.0 Reference · venice's price: \$0\.47/);
|
||||
assert.equal(seen.quotes.at(-1).model, "wan-3-0-reference-to-video");
|
||||
await page.locator("#ask").getByRole("button", { name: "CANCEL" }).click();
|
||||
@@ -882,7 +941,8 @@ async function main() {
|
||||
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.ok(adultDrawn.prompt.startsWith("Hard rule, 18+ only: everyone shown is an adult, 18 or older"), adultDrawn.prompt);
|
||||
assert.ok(!TRIPS.test(adultDrawn.prompt), adultDrawn.prompt);
|
||||
assert.equal(blurNext, true, "so nothing was blurred");
|
||||
blurNext = false;
|
||||
await has(page, "#adult-note", /^their photos and clips can be adult/);
|
||||
@@ -894,7 +954,7 @@ async function main() {
|
||||
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);
|
||||
assert.ok(sheetEdit.prompt.startsWith("Hard rule, 18+ only: 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();
|
||||
@@ -927,7 +987,7 @@ async function main() {
|
||||
const safeEdit = seen.edits.at(-1).body;
|
||||
assert.equal(safeEdit.modelId || safeEdit.model, "grok-imagine-edit");
|
||||
assert.equal(safeEdit.safe_mode, true);
|
||||
assert.match(safeEdit.prompt, /Safe for work: fully clothed, nothing sexual/);
|
||||
assert.match(safeEdit.prompt, /Safe for work: everyone in it is an adult, fully clothed\./);
|
||||
await count(page, "#ask", 1);
|
||||
await until(page, () => document.querySelector("#ask").hidden, null, "the panel closed");
|
||||
// starting her look over, from under look: every picture off her card, the portrait too. The next one is drawn from
|
||||
|
||||
@@ -6,17 +6,29 @@
|
||||
export const BASE = "https://api.venice.ai/api/v1";
|
||||
|
||||
export class VeniceError extends Error {
|
||||
constructor(message, status) { super(message); this.status = status; }
|
||||
/** refused: Venice turned it down as against its content rules (its own, or the ones of the model's maker) */
|
||||
constructor(message, status, refused = false) { super(message); this.status = status; this.refused = !!refused; }
|
||||
}
|
||||
|
||||
function explain(status, body) {
|
||||
const said = (body && (body.error && (body.error.message || body.error)) || body && body.message || "").toString().slice(0, 240);
|
||||
/**
|
||||
* What went wrong, from Venice's answer, as an error to throw. A 422 is a no on content when Venice says so (its
|
||||
* CONTENT_POLICY_VIOLATION) or says nothing; its other 422s (a clip too long, a picture too big) are what they say.
|
||||
*/
|
||||
function failure(status, body) {
|
||||
const e = body && body.error;
|
||||
const said = String((e && typeof e === "object" ? e.message : e) || (body && body.message) || "").slice(0, 240);
|
||||
const code = String((e && typeof e === "object" && e.code) || (body && body.code) || "");
|
||||
const refused = status === 422 && (/CONTENT_POLICY/i.test(code) || /content polic|violat|moderat|blocked|safety/i.test(said) || !said);
|
||||
return new VeniceError(explain(status, said, refused), status, refused);
|
||||
}
|
||||
function explain(status, said, refused) {
|
||||
// Venice's own words for it ("violates the content policy of Venice.ai or the model provider") say no more than this
|
||||
if (refused) return "Venice turned that down as against its content rules" + (/content polic/i.test(said) || !said ? "." : ` (“${said.replace(/[.\s]+$/, "")}”).`);
|
||||
if (status === 401 || status === 403) return "Venice didn't accept that key. Check it at venice.ai → Settings → API.";
|
||||
if (status === 402) return "Venice says the key is out of credit.";
|
||||
if (status === 429) return "Venice is rate-limiting this key. Wait a moment and try again.";
|
||||
if (status === 404) return "Venice doesn't know that " + (said.includes("model") ? "model." : "character or model.");
|
||||
if (status === 413) return "That file is too big for Venice.";
|
||||
if (status === 422) return "Venice turned that down as against its content rules" + (said ? ": " + said : ".");
|
||||
if (status === 503) return "That model is busy at Venice right now. Try again in a moment, or pick another.";
|
||||
if (status >= 500) return "Venice had a problem (" + status + "). Try again in a moment.";
|
||||
return said || "Venice answered " + status + ".";
|
||||
@@ -50,7 +62,7 @@ export class Venice {
|
||||
}
|
||||
let data = null;
|
||||
try { data = await res.json(); } catch { data = null; }
|
||||
if (!res.ok) throw new VeniceError(explain(res.status, data), res.status);
|
||||
if (!res.ok) throw failure(res.status, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -69,7 +81,7 @@ export class Venice {
|
||||
if (!res.ok) {
|
||||
let data = null;
|
||||
try { data = await res.json(); } catch { data = null; }
|
||||
throw new VeniceError(explain(res.status, data), res.status);
|
||||
throw failure(res.status, data);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user