Files

207 lines
14 KiB
Markdown
Raw Permalink Normal View History

2026-10-16 02:45:51 +00:00
---
name: havre
description: Write AI characters as Havre cards (.havre.json) with a persona, a short-form voice, an optional flow, examples and lineage; export them as Character Card V3 for SillyTavern-style apps; talk to them through Venice's API; and remix someone else's character into an original one, with credit. Use when someone asks for a character, persona or roleplay companion for Havre, a character-card app, or Venice.
---
# Havre
Havre (https://havre.yolopreneur.lol/) is a harbor for characters. A character is a card file its person keeps: who
they are, how they talk, how they meet someone new, and where they came from. People talk to them, and to Venice's own
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.
2026-10-16 02:45:51 +00:00
Everything you need is at https://havre.yolopreneur.lol/:
- `card.schema.json`: the format, as JSON Schema.
- `card.js`: reading, checking and converting cards (an ES module, no dependencies; works in Node 20+ and browsers).
2026-10-16 07:45:57 +00:00
- `venice.js`: the Venice API calls Havre makes: chat, characters, models, photos, voice notes, hearing and clips.
- `abilities.js`: the prompts and rules behind photos, clips, voice notes, hearing and seeing.
2026-10-16 02:45:51 +00:00
2026-10-18 01:16:04 +00:00
Three characters live at the harbor, as worked examples of the format: Maren the harbormaster, a guide
(`cast/maren.havre.json`); Odile, the lighthouse keeper, a storyteller (`cast/odile.havre.json`); and Taskmaster, a
coach with a flow (`cast/taskmaster.havre.json`). Read one before writing your first card.
2026-10-19 01:48:10 +00:00
Each has a page too, at `/c/<id>/` (the list is `/c/`), with a link into the Studio, `/?meet=<id>`, and the same
character as a Character Card V3 at `/c/<id>/<id>.card.json`.
2026-10-18 01:16:04 +00:00
2026-10-16 02:45:51 +00:00
## A card
```json
{
"havre": 1,
"id": "juniper",
"name": "Juniper",
"tagline": "Night-shift radio host. Takes requests.",
"greeting": "You're on the air.\n\nWhat are we playing for you tonight?",
"persona": "You are Juniper, who hosts the 2 a.m. call-in show on a small coastal station...",
"voice": { "short_form": true, "notes": "Warm, unhurried, a little teasing. One question at a time." },
"flow": [],
"examples": [
{ "user": "Can you play something sad?", "character": "Sad, or sad-but-you-can-dance-to-it?\n\nBe honest." }
],
"tags": ["radio", "cozy"],
2026-10-16 16:42:20 +00:00
"adult": false,
2026-10-16 02:45:51 +00:00
"model": "zai-org-glm-5-2",
"engine": {},
"portrait": "",
2026-10-16 07:45:57 +00:00
"look": { "description": "Forties, short dark curls, a big knitted cardigan, headphones round her neck.", "references": [] },
"speech": { "model": "tts-kokoro", "voice": "af_bella", "mood": "warm, unhurried", "speed": 1 },
2026-10-16 02:45:51 +00:00
"creator": "",
"license": "remix-with-credit",
"lineage": [],
"created": "2026-10-16T00:00:00.000Z",
"updated": "2026-10-16T00:00:00.000Z"
}
```
Save it as `<id>.havre.json`. Only `havre` (always 1), `name` and `persona` are required; `normalizeCard()` in
`card.js` fills in the rest.
## Writing a good character
- **persona**: second person ("You are ..."), 120 to 300 words. Who they are, how they talk, what they want, what they
won't do. Concrete details beat adjectives. `{{char}}` and `{{user}}` are filled in at chat time.
- **greeting**: one to three short texts, separated by a blank line.
- **voice**: `short_form: true` makes replies arrive as a few short texts, the way people text. Put how they talk in
`notes` as traits ("clipped", "asks one question at a time"), never as catchphrases.
2026-10-16 12:26:40 +00:00
- **examples**: three to five exchanges. They teach the voice (the first four go into the system prompt), and they are
what a visitor without a key hears: the page answers with the example whose question is closest to what was said.
- **name**: keep it the same everywhere. The persona should open with "You are <name>", and the greeting and examples
shouldn't call them anything else: a model given two names drifts to the other one.
2026-10-16 02:45:51 +00:00
- **flow** (optional): stages for meeting someone new, taken in order. Each has a `stage`, a `goal` and `next` (when to
move on). A coach's flow might be assess, plan, execute:
2026-10-16 02:45:51 +00:00
```json
[{ "stage": "assess", "goal": "Ask what they're working on, the main blocker and the deadline.", "next": "you know all three." },
{ "stage": "plan", "goal": "Propose one plan and ask them to say go.", "next": "they say go." },
{ "stage": "execute", "goal": "Say: Switching to EXECUTION MODE. Then one concrete step at a time, each ending with when to report back." }]
```
2026-10-16 07:45:57 +00:00
- **look** (optional): how they look. `description` is a sentence or two (an adult; face, hair, build, clothes).
`references` holds up to three more pictures besides the `portrait`, best as data: URIs (PNG, JPEG, WebP or GIF)
so they travel with the card. Havre makes photos and clips of the character from the portrait and these, when
2026-10-16 19:21:37 +00:00
they're kept in the card (or the character's own photo on Venice): their face comes from the pictures, and
everything else from `description`, so a new description shows in the next photo. A new portrait is drawn from
`description` alone. Pictures of the character only: art, or pictures the person made. Never a photo of a real
person.
2026-10-16 07:45:57 +00:00
- **speech** (optional): how they sound in a voice note. `model` is a Venice voice model (`tts-kokoro` if empty),
`voice` one of its voices, `mood` a short delivery note ("warm, teasing") that Qwen 3 voices follow, and `speed`
0.25 to 4.
2026-10-16 16:42:20 +00:00
- **adult** (optional): `true` for a character for grown-ups only (18+). Everyone in their chats and pictures is an
adult, 18 or older, and Havre puts that in their system prompt as a hard rule. Character cards carry it as an `nsfw`
tag, both ways, and a remix of an adult character is adult too.
2026-10-16 02:45:51 +00:00
- **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.
Only ever add entries. Never remove or edit one you didn't add.
Rules: write original characters, or port characters the person made. Never base a character on a real, private person.
Characters never invent links, prices or payments (Havre adds that rule to every system prompt).
## Checking a card
```sh
curl -O https://havre.yolopreneur.lol/card.js
node --input-type=module -e '
import * as C from "./card.js";
import { readFileSync } from "node:fs";
const card = C.normalizeCard(JSON.parse(readFileSync(process.argv[1], "utf8")));
const problems = C.validateCard(card);
console.log(problems.length ? problems : "valid");' juniper.havre.json
```
## Other formats
- **Character Card V3** (SillyTavern, RisuAI, Chub and other card apps): `C.toCharacterCardV3(card)`. The whole Havre
card rides along in `data.extensions.havre`, so it comes back into Havre without loss. `C.toCharacterCardV2(card)`
for older apps.
- **A PNG card**: `C.toPngCard(pngBytes, card)` writes the card into a portrait PNG (`ccv3` and `chara` text chunks).
- **A system prompt**: `C.systemPrompt(card)`, for any OpenAI-compatible API.
- **In**: `C.fromAny(input)` reads Havre JSON, Character Card V1, V2 or V3 (JSON or PNG bytes) or plain text.
## Talking to a character through Venice
Havre uses Venice's OpenAI-compatible API with the person's own key. Build the messages with `C.chatMessages(card,
2026-10-16 12:26:40 +00:00
history)`: the system prompt (who they are, how they talk with their examples, their flow, their name and the house
rules), the greeting, the last 40 messages, and on the latest one a reminder of who is answering
(`[You are <name>. Stay in character.]`), since a model drifts from a prompt far above. `C.chatMessages(card, history,
{ memory })` adds what the character remembers of your chats (`memory.js`). Then send them:
2026-10-16 02:45:51 +00:00
```sh
curl https://api.venice.ai/api/v1/chat/completions \
-H "Authorization: Bearer $VENICE_API_KEY" -H "Content-Type: application/json" \
-d '{"model": "zai-org-glm-5-2", "messages": [{"role": "system", "content": "<C.systemPrompt(card)>"},
{"role": "user", "content": "hi"}],
"venice_parameters": {"include_venice_system_prompt": false, "strip_thinking_response": true}}'
```
Split the reply into texts with `C.splitBubbles(reply)`. To talk through a public Venice character instead (its own
prompt, which nobody else can read), leave out the system message and set `venice_parameters.character_slug`.
2026-10-16 07:45:57 +00:00
## Photos, clips and voice notes
In Havre a character can send more than texts. `abilities.js` has the prompts and `venice.js` the calls:
- **A photo**: a text model writes the shot from the conversation (`A.shotMessages(card, A.apiHistory(history), ask)`),
then `venice.editImage({ model, prompt: A.photoPrompt(card, shot, n), images })` edits the character's own
2026-10-16 19:21:37 +00:00
pictures (`/image/edit`, or `/image/multi-edit` for several). Safe mode is on, except for adult characters (below).
A character with no picture yet gets a first portrait from `A.portraitPrompt(card)` through `/image/generate`.
2026-10-16 07:45:57 +00:00
- **A clip**: the same kind of shot (`kind: "clip"`), priced first with `venice.quoteVideo()`, then
`venice.queueVideo()` with a reference-to-video model and the character's pictures, polled with
`venice.retrieveVideo()`. Always show the price before queueing.
- **A voice note**: `venice.speak({ model, voice, input: A.speakable(reply), mood, speed })` (`/audio/speech`).
- **Hearing and seeing** (only if the person switches them on): `venice.transcribe()` turns their voice note into
words; their photo goes to a model that can see (`A.withPicture(messages, dataUrl)`), or is described by one first.
- **On their own**: after a reply where it fits (they were asked to show or say something, or the reply is about
something worth showing), `A.moveMessages(card, history)` with `A.MOVE_SCHEMA` lets the character's model choose a
photo, a voice note, a clip or nothing. Havre sends photos and voice notes out of a daily allowance the person sets,
never two photos in a row, and only offers a clip, with Venice's price, for the person to accept. After a long gap
(`A.BACK_AFTER`), the character texts first (`A.comeBack(ms)`).
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.
2026-10-16 20:04:24 +00:00
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`;
2026-10-16 23:33:52 +00:00
`VeniceError.refused`), offer the next model (`A.another(list, tried, kind)`) instead of a dead end, and show what
was sent and what came back: the error carries Venice's answer as it came (`said`, `code`, `body`) and the call
(`sent`: endpoint, model, pictures, safe mode, prompt), and `A.filterWords(prompt)` lists the words in the prompt a
filter may weigh (ages, youth), since Venice's stock line never says which. Havre's photo panel shows all of it
under "what venice saw". A photo a character starts on their own tries one other model, then says in the chat that
it didn't come.
2026-10-16 20:04:24 +00:00
2026-10-16 16:42:20 +00:00
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.
2026-10-17 00:13:34 +00:00
`A.adultCheck(card, { of18, made, shot, said })` decides; they go to Venice's uncensored models with safe mode off.
The shot is written to the 18+ rule with the number in it (`A.EIGHTEEN`, in the shot writer's instructions, which
no picture filter reads), and every picture and clip prompt opens with the same rule in words a filter doesn't take
for an age or for anyone it guards (`A.ADULT_ONLY`, "Everyone shown is a grown adult."): a picture prompt sent with
safe mode off is read the hardest, and "18+", "body", "hard rule" or "identifiable person" in it got the plainest
photo turned down on every model. Never when the card, the shot or the chat mentions anyone under 18
(`A.underage`): then it's safe for work, as usual. The pictures made for their card go without safe mode
2026-10-16 17:28:25 +00:00
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.
2026-10-16 16:42:20 +00:00
2026-10-16 02:45:51 +00:00
## Remix with credit
To make someone new from a character the person didn't write, work like a clean room:
1. **Distill.** One model call reads the source and returns only ideas: `C.distillMessages(sourceText)` with
`C.DISTILL_SCHEMA`. No names, no quotations, no sentences from the source. For a Venice character, whose prompt
can't be read, ask it the questions in `C.INTERVIEW` and distill the answers.
2. **Edit.** Show the person the ideas. They remove what they don't want and add a twist. Their choices are part of
the authorship.
3. **Write.** A second call gets only the ideas and the twist, never the source: `C.writeMessages(ideas, twist)` with
`C.WRITE_SCHEMA`.
4. **Check.** `C.similarity(C.cardText(newCard), sourceText, { names: [sourceName], newName })` returns `fresh`,
`close` or `too-close`, with the reasons (shared five-word phrases, the longest shared run of words, reused or
look-alike names). If it isn't fresh, write again with `C.writeMessages(ideas, twist, C.avoidList(check,
[sourceName]))`. Never keep a `too-close` result.
5. **Credit.** `C.cardFromWrite(result, { source, check })` adds an `inspired-by` lineage entry with the numbers.
Names and broad ideas aren't protected by copyright; a character's written expression can be. The clean room keeps
the expression out, and the lineage keeps the credit in. When in doubt, ask the creator. This isn't legal advice.
## Coming next
A hosted harbor with a page per character, and an API and an MCP server to create, update and talk to characters
without a browser.