Restore 18+ catalog control and document independent character preferences
This commit is contained in:
1 parent
06c0ceb47f
commit
c9ea53a9bc
5 files changed
+77
-36
No files matched your search
@@ -44,13 +44,18 @@ and not affiliated with it.
|
||||
Payments, channels (a character in a creator's Whop) and payouts to creators are Whop's. It is the factory's
|
||||
second Whop app; .mdeck goes first (#60, #61) so the factory learns Whop once.
|
||||
4. **BYOK, with moderation handled by Venice.** The human owner's direction on 2026-10-24 supersedes the earlier
|
||||
Studio-only adult policy and the restrictions built around it. Havre adds no content filters, age confirmation,
|
||||
adult-catalog hiding, text denylist, uploaded-picture gate or mandatory SFW/adults-only prompt. Image requests
|
||||
Studio-only adult policy and the restrictions built around it. Havre adds no content moderation, age confirmation,
|
||||
text denylist, uploaded-picture gate or mandatory SFW/adults-only prompt. Image requests
|
||||
set optional safe mode off. The adult flag remains portable card metadata and may select a saved model preference;
|
||||
it grants or denies no capability. Venice and the selected model handle their own moderation and return their own
|
||||
responses and errors. Remix similarity is advisory and never disables saving. Hosted page generation validates
|
||||
structure and escapes text; it does not reject a card for its adult label. Keep API-key isolation, file/URL
|
||||
validation, spending controls, payment confirmation and provenance intact.
|
||||
The user-requested **18+ catalog** checkbox selects a Venice search category: unchecked sends `isAdult=false`
|
||||
for the standard catalog; checked sends `isAdult=true` for the adult catalog. Both use `/characters`; the
|
||||
categories are exclusive, with no All option or age-confirmation gate. This is a search control, not Havre
|
||||
content moderation. Every saved character, including one from the standard Venice catalog, has an independent
|
||||
editable 18+ setting that chooses model preferences without granting or denying generation capabilities.
|
||||
5. **Venice is the engine.** No multi-provider build. The name, the story, the characters and the abilities are
|
||||
Venice's; a second provider would make Havre a generic card client, of which there are plenty.
|
||||
6. **Scenes wait.** Two characters taking turns is a feature for people who already come back. It stays on the
|
||||
|
||||
@@ -79,8 +79,10 @@
|
||||
<option value="highestRating">top rated</option>
|
||||
<option value="mostRecent">newest</option>
|
||||
</select></label>
|
||||
<label class="check"><input type="checkbox" id="vadult" aria-describedby="vcatalog-note"> 18+ catalog</label>
|
||||
<button type="submit" class="btn" id="vgo">SEARCH</button>
|
||||
</form>
|
||||
<p class="hint" id="vcatalog-note">unchecked: standard venice catalog; checked: 18+ catalog. a saved character's 18+ setting is separate.</p>
|
||||
<p id="vtags" class="tags" hidden></p>
|
||||
<p id="venstatus" class="status" role="status" aria-live="polite"></p>
|
||||
<ul id="vgrid" class="grid" aria-label="Venice characters"></ul>
|
||||
@@ -158,8 +160,8 @@
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="check"><input type="checkbox" name="short_form"> short-form: replies come as a few short texts</label>
|
||||
<label class="check"><input type="checkbox" name="adult"> adult content label (metadata)</label>
|
||||
<p class="hint" id="adult-note" role="status" hidden></p>
|
||||
<label class="check"><input type="checkbox" name="adult" aria-describedby="adult-note"> 18+</label>
|
||||
<p class="hint" id="adult-note" role="status">use 18+ model preferences for this character, including one from the standard catalog. this setting does not filter chats, prompts, or pictures; venice and the selected model handle content policy.</p>
|
||||
<label>HOW THEY TALK <input name="voice_notes" maxlength="2000" placeholder="clipped, wry, asks one question at a time"></label>
|
||||
<details class="group" id="flow-group">
|
||||
<summary>FLOW <span class="hint">optional stages for meeting someone new</span></summary>
|
||||
@@ -330,8 +332,8 @@
|
||||
<p>Characters and chats are saved in this browser, and so are the photos, clips and voice notes in them. Export a character to keep them anywhere else.</p>
|
||||
<p>What a character remembers of your chats is kept in this browser too, beside their card: a standard character-card export carries none of it; a full export or backup includes 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>Havre adds no content filtering. Characters, prompts, reference pictures and media requests go to Venice using your own API key. Venice and the model you choose handle moderation; Havre shows the response or error they return.</p>
|
||||
<p>The adult label is card metadata, carried between apps as an nsfw tag. It does not hide characters, gate chats or decide what you can generate. Images are requested with optional safe mode off; Venice's own policies and model limits still apply.</p>
|
||||
<p>Havre does not moderate your content. The catalog selector chooses Venice's standard or 18+ search catalog. Characters, prompts, reference pictures and media requests go to Venice using your own API key. Venice and the model you choose handle moderation; Havre shows the response or error they return.</p>
|
||||
<p>A saved character's 18+ setting is separate from the search catalog. You can change it for any saved character, including one from Venice's standard catalog. It selects your model preferences and travels between apps as an nsfw tag; it does not gate chats or generation. Images are requested with optional safe mode off; Venice's own policies and model limits still apply.</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>Character imports and remixes keep the source credit in their lineage. Remix can write from the ideas you choose; the similarity comparison is advisory, and you decide what to save.</p>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
Read skill.md for the card format and API workflow. card.js and venice.js are ES modules with no dependencies (Node 20+ or a browser).
|
||||
|
||||
Havre adds no content filtering. Requests use the person's own Venice key; Venice and the selected model handle moderation. Cards, chats and generated media stay in local browser storage; prompts, chat context and supplied media are sent to Venice for generation. The adult label is metadata and remix similarity is advisory.
|
||||
Havre adds no content moderation. Requests use the person's own Venice key; Venice and the selected model handle moderation. Cards, chats and generated media stay in local browser storage; prompts, chat context and supplied media are sent to Venice for generation. Remix similarity is advisory.
|
||||
|
||||
The 18+ catalog checkbox selects one of Venice's exclusive search catalogs on `/characters`: unchecked sends `isAdult=false` (standard), checked sends `isAdult=true` (adult). There is no All option or age-confirmation gate. Any saved character, including one from the standard catalog, has a separate editable 18+ setting for model preferences; that metadata does not gate content or generation.
|
||||
|
||||
## Files
|
||||
|
||||
|
||||
@@ -10,10 +10,15 @@ they are, how they talk, how they meet someone new, and where they came from. Pe
|
||||
public characters, in the browser with their own Venice API key; export them to other apps; and remix the ones they
|
||||
love into someone new, with credit.
|
||||
|
||||
Havre adds no content filtering. It sends requests through the person's own Venice API key; Venice and the chosen
|
||||
Havre adds no content moderation. It sends requests through the person's own Venice API key; Venice and the chosen
|
||||
model handle moderation. Cards, chats and generated media are stored in the browser, while prompts, chat context
|
||||
and supplied media are sent to Venice to carry out those requests.
|
||||
|
||||
Catalog search has two exclusive choices on Venice's `/characters` endpoint. The **18+ catalog** checkbox sends
|
||||
`isAdult=false` when unchecked (standard catalog) and `isAdult=true` when checked (adult catalog). There is no All
|
||||
option or age-confirmation gate. This search choice is separate from a saved character's own 18+ setting: any saved
|
||||
character, including one from the standard Venice catalog, can be marked 18+ independently.
|
||||
|
||||
Everything you need is at https://havre.yolopreneur.lol/:
|
||||
|
||||
- `card.schema.json`: the format, as JSON Schema.
|
||||
@@ -89,8 +94,8 @@ Save it as `<id>.havre.json`. Only `havre` (always 1), `name` and `persona` are
|
||||
`voice` one of its voices, `mood` a short delivery note ("warm, teasing") that Qwen 3 voices follow, and `speed`
|
||||
0.25 to 4.
|
||||
- **adult** (optional): content metadata, carried both ways as an `nsfw` tag in Character Cards. A remix preserves
|
||||
it. The label may select a saved model preference, but does not gate browsing, chatting, uploads or generation and
|
||||
does not inject content rules into the character's prompt.
|
||||
it. The person can edit the 18+ setting on any saved character. It selects saved model preferences independently
|
||||
of the search catalog, and does not gate chatting, uploads or generation or inject content rules into the prompt.
|
||||
- **license**: `remix-with-credit` (the default), `all-rights-reserved`, `cc-by-4.0` or `cc0`.
|
||||
- **lineage**: where the character came from, as `{ relation, name, creator, ref, at, note }` with relation
|
||||
`ported-from`, `inspired-by` or `forked-from` and ref `venice:<slug>`, a URL, or `sha256:<hex>` of the source text.
|
||||
|
||||
@@ -345,10 +345,11 @@ test("venice: models need no key; characters preserve content labels; errors are
|
||||
|
||||
const log2 = [];
|
||||
const k = new Venice("sk-test", { fetch: fakeFetch((p) => p.startsWith("/characters?")
|
||||
? { status: 200, body: { data: [{ slug: "a", name: "A", adult: false, photoUrl: "https://outerface.venice.ai/x", stats: { imports: 3 } }, { slug: "b", adult: true }] } }
|
||||
? { status: 200, body: { data: [{ slug: "a", name: "A", adult: false, photoUrl: "https://outerface.venice.ai/x", stats: { imports: 3 } }] } }
|
||||
: { status: 401, body: { error: "bad key" } }, log2) });
|
||||
const cs = await k.characters({ search: "sea" });
|
||||
assert.deepEqual(cs.map((c) => c.slug), ["a", "b"]);
|
||||
assert.deepEqual(cs.map((c) => c.slug), ["a"]);
|
||||
assert.equal(new URL(log2[0].url).searchParams.get("isAdult"), "false");
|
||||
assert.equal(cs[0].url, "https://venice.ai/c/a");
|
||||
assert.match(log2[0].url, /search=sea/);
|
||||
assert.equal(log2[0].init.headers.Authorization, "Bearer sk-test");
|
||||
@@ -375,41 +376,67 @@ test("venice: chat sends Havre's prompt alone, or speaks through a Venice charac
|
||||
assert.ok(bodies[2].response_format && !bodies[3].response_format, "retried without response_format after a 400");
|
||||
});
|
||||
|
||||
test("venice: a page of characters preserves all results, sorting and pagination", async () => {
|
||||
test("venice: the standard catalog is requested explicitly with sorting and pagination", async () => {
|
||||
const urls = [];
|
||||
const page = (n) => Array.from({ length: n }, (_, i) => ({ slug: "c" + i, name: "C" + i, adult: false, photoUrl: "http://elsewhere.example/x.png" }));
|
||||
const v = new Venice("sk", { fetch: fakeFetch((p) => ({ status: 200, body: { data: p.includes("offset=24") ? page(3) : [...page(23), { slug: "x", adult: true }] } }), urls) });
|
||||
const first = await v.characterPage({ sort: "imports", search: "#pirate", limit: 24 });
|
||||
const q = new URL(urls[0].url).searchParams;
|
||||
assert.equal(q.get("isAdult"), null);
|
||||
assert.equal(q.get("sortBy"), "imports");
|
||||
assert.equal(q.get("search"), "#pirate");
|
||||
assert.equal(first.list.length, 24, "all returned characters remain listed");
|
||||
assert.equal(first.list.at(-1).adult, true, "content labels are still available");
|
||||
const v = new Venice("sk", { fetch: fakeFetch((p) => {
|
||||
const q = new URL(p, "https://api.venice.ai").searchParams;
|
||||
// Venice's omitted/false selection is standard only; true is a separate catalog.
|
||||
return { status: 200, body: { data: q.get("isAdult") === "true" ? [] : page(q.get("offset") === "24" ? 3 : 24) } };
|
||||
}, urls) });
|
||||
const first = await v.characterPage({ sort: "imports", search: "#pirate", tags: "sea", limit: 24 });
|
||||
assert.deepEqual(Object.fromEntries(new URL(urls[0].url).searchParams),
|
||||
{ limit: "24", offset: "0", isAdult: "false", search: "#pirate", sortBy: "imports", tags: "sea" });
|
||||
assert.equal(first.list.length, 24);
|
||||
assert.ok(first.list.every((c) => c.adult === false));
|
||||
assert.equal(first.more, true, "a full page means there may be more");
|
||||
assert.equal(first.list[0].photo, "", "photos only from outerface.venice.ai");
|
||||
const last = await v.characterPage({ limit: 24, offset: 24 });
|
||||
assert.equal(last.list.length, 3);
|
||||
assert.equal(last.more, false);
|
||||
assert.equal(new URL(urls[1].url).searchParams.get("sortBy"), null);
|
||||
assert.deepEqual(Object.fromEntries(new URL(urls[1].url).searchParams), { limit: "24", offset: "24", isAdult: "false" });
|
||||
|
||||
const tries = [];
|
||||
const old = new Venice("sk", { fetch: fakeFetch((p) => (p.includes("sortBy") ? { status: 400, body: { error: "unknown parameter" } } : { status: 200, body: { data: page(2) } }), tries) });
|
||||
const plain = await old.characterPage({ sort: "featured" });
|
||||
assert.equal(plain.list.length, 2);
|
||||
assert.equal(tries.length, 2, "asked again without the sort");
|
||||
assert.ok(!tries[1].url.includes("sortBy") && !tries[1].url.includes("isAdult"));
|
||||
const old = new Venice("sk", { fetch: fakeFetch((p) => (p.includes("sortBy") ? { status: 400, body: { error: "unknown parameter" } }
|
||||
: { status: 200, body: { data: [{ slug: "adult", adult: true }] } }), tries) });
|
||||
const result = await old.characterPage({ adult: true, sort: "featured", search: "captain", tags: "sea", offset: 48, limit: 24 });
|
||||
assert.equal(result.list[0].adult, true);
|
||||
assert.equal(tries.length, 2, "unsupported optional parameters are retried");
|
||||
assert.deepEqual(Object.fromEntries(new URL(tries[1].url).searchParams),
|
||||
{ limit: "24", offset: "48", isAdult: "true", search: "captain" }, "retry must not switch the selected catalog");
|
||||
});
|
||||
|
||||
test("venice: legacy adult option cannot filter the catalog; tags narrow the API request", async () => {
|
||||
test("venice: 18+ is a separate catalog and keeps the selection while paging and searching", async () => {
|
||||
const urls = [];
|
||||
const data = [{ slug: "a", name: "A", adult: false, tags: ["sea"] }, { slug: "b", name: "B", adult: true, tags: [] }];
|
||||
const v = new Venice("sk", { fetch: fakeFetch(() => ({ status: 200, body: { data } }), urls) });
|
||||
const adult = await v.characterPage({ adult: true, tags: "sea" });
|
||||
const q = new URL(urls[0].url).searchParams;
|
||||
assert.equal(q.get("isAdult"), null, "no content-category filter is requested");
|
||||
assert.equal(q.get("tags"), "sea");
|
||||
assert.deepEqual(adult.list.map((c) => [c.slug, c.adult]), [["a", false], ["b", true]], "metadata is preserved for every result");
|
||||
assert.deepEqual((await v.characters()).map((c) => c.slug), ["a", "b"], "the default request is unfiltered too");
|
||||
const data = [{ slug: "standard", name: "Standard", adult: false, tags: ["sea"] },
|
||||
...[1, 2, 3].map((n) => ({ slug: "adult-" + n, name: "Adult " + n, adult: true, tags: ["sea"] }))];
|
||||
const v = new Venice("sk", { fetch: fakeFetch((p) => {
|
||||
const q = new URL(p, "https://api.venice.ai").searchParams;
|
||||
const start = Number(q.get("offset") || 0), limit = Number(q.get("limit") || 24);
|
||||
const rows = data.filter((c) => c.adult === (q.get("isAdult") === "true"));
|
||||
return { status: 200, body: { data: rows.slice(start, start + limit) } };
|
||||
}, urls) });
|
||||
const opts = { adult: true, tags: "sea", search: "captain", sort: "highestRating", limit: 2 };
|
||||
const first = await v.characterPage(opts);
|
||||
assert.deepEqual(Object.fromEntries(new URL(urls[0].url).searchParams),
|
||||
{ limit: "2", offset: "0", isAdult: "true", search: "captain", sortBy: "highestRating", tags: "sea" });
|
||||
assert.deepEqual(first.list.map((c) => [c.slug, c.adult]), [["adult-1", true], ["adult-2", true]]);
|
||||
assert.equal(first.more, true);
|
||||
const last = await v.characterPage({ ...opts, offset: 2 });
|
||||
assert.deepEqual(Object.fromEntries(new URL(urls[1].url).searchParams),
|
||||
{ limit: "2", offset: "2", isAdult: "true", search: "captain", sortBy: "highestRating", tags: "sea" });
|
||||
assert.deepEqual(last.list.map((c) => c.slug), ["adult-3"]);
|
||||
assert.equal(last.more, false);
|
||||
assert.deepEqual((await v.characters()).map((c) => c.slug), ["standard"]);
|
||||
assert.equal(new URL(urls.at(-1).url).searchParams.get("isAdult"), "false");
|
||||
});
|
||||
|
||||
test("venice: catalog selection does not add a local result filter", async () => {
|
||||
const data = [{ slug: "a", adult: false }, { slug: "b", adult: true }];
|
||||
const v = new Venice("sk", { fetch: fakeFetch(() => ({ status: 200, body: { data } }), []) });
|
||||
for (const adult of [false, true]) {
|
||||
assert.deepEqual((await v.characters({ adult })).map((c) => c.slug), ["a", "b"], "preserve exactly what the provider returned");
|
||||
}
|
||||
});
|
||||
|
||||
test("venice: models say what they can do and how private they are; beta-only ones are left out; the balance reads", async () => {
|
||||
|
||||
Reference in new issue
Block a user