From 01f53e86917147b0f58b3e725eed038c8d0a2b52 Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Wed, 21 Oct 2026 19:15:00 +0000 Subject: [PATCH] =?UTF-8?q?havre:=20/whisper=20workflow=20=E2=80=94=20pres?= =?UTF-8?q?ets,=20script=20ask,=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ventures/havre/workflows.js | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/ventures/havre/workflows.js b/ventures/havre/workflows.js index 1e5d13e..18d50b3 100644 --- a/ventures/havre/workflows.js +++ b/ventures/havre/workflows.js @@ -33,6 +33,11 @@ export const WORKFLOWS = [ about: "a few photos of them from the pictures on their card, one shoot, each a different moment (3 unless you say)", usage: "/photoshoot [how many] [what the shoot is about]", }, + { + id: "whisper", command: "whisper", title: "a whisper", takes: "card", gives: "voice note", doors: ["chat"], + about: "a short ASMR voice note from them, whispered over soft rain (luna or eve; add what it's about)", + usage: "/whisper [luna|eve] [what it's about]", + }, { id: "recall", command: "recall", title: "recall", takes: "words", gives: "memory", doors: ["chat"], about: "what they remember about something (their notes, then the chats themselves), brought to mind for a while", @@ -128,6 +133,51 @@ export function cardFromIdea(o, { model } = {}) { return normalizeCard({ ...base, look: { ...base.look, description: look } }); } +// ---------- whisper: a short ASMR voice note, over soft rain ---------- + +// The voices that whisper well and cheaply (picked in the asmr.gandora.lol bake-off): Venice model, voice, how the +// words are wrapped so the model whispers, and the pace (0.9 of their usual reading speed sounded best). +export const WHISPER_PRESETS = { + luna: { model: "tts-inworld-1-5-max", voice: "Luna", lead: "[whispering] ", tail: "", speed: 0.77, format: "wav" }, + eve: { model: "tts-xai-v1", voice: "eve", lead: "", tail: "", speed: 0.77, format: "mp3" }, +}; +export const WHISPER_DEFAULT = "luna"; + +/** the words after /whisper: a preset name first if there is one, the rest what it's about */ +export function parseWhisper(arg = "") { + const m = /^\s*(\w+)(?:\s+|$)([\s\S]*)$/.exec(str(arg, 2000)); + const first = m ? m[1].toLowerCase() : ""; + if (WHISPER_PRESETS[first]) return { preset: first, brief: m[2].replace(/\s+/g, " ").trim() }; + return { preset: "", brief: str(arg, 2000).replace(/\s+/g, " ").trim() }; +} + +export const WHISPER_SCHEMA = { + type: "object", additionalProperties: false, required: ["script"], + properties: { script: { type: "string" } }, +}; + +export function whisperMessages(card, { brief = "", lines = [] } = {}) { + const system = `You are ${card.name}. Write a short ASMR voice note you whisper to the person you're talking with: ` + + "60 to 110 words, in your own voice and manner, intimate and calming, present tense, short phrases with pauses " + + "written as \"...\". Gentle sensory detail (breath, rain, a soft sound nearby) is welcome. No stage directions, no " + + "emoji, no markup, nothing in brackets: only the words to be whispered. Return one JSON object: script."; + const persona = card.persona ? clip(card.persona.replace(/\s+/g, " "), 900) : ""; + const user = [ + persona ? `Who you are: ${persona}` : "", + lines.length ? "The conversation so far, latest last:\n" + lines.join("\n") : "You haven't talked yet.", + "What it's about: " + (str(brief, 500).trim() || "winding down together; help them relax."), + ].filter(Boolean).join("\n\n"); + return [{ role: "system", content: system }, { role: "user", content: user }]; +} + +/** the script, cleaned of anything the model shouldn't have added (brackets, asterisks, emoji-ish markup) */ +export function cleanWhisper(o) { + return str(o && o.script, 1500).replace(/\[[^\]]*\]|\([^)]*\)|<[^>]*>|\*[^*]*\*/g, "").replace(/\s+/g, " ").trim(); +} + +/** what the voice model is sent: the script inside the preset's whisper markup */ +export const whisperInput = (script, preset) => (preset.lead || "") + script + (preset.tail || ""); + // ---------- photoshoot: a few photos of them, from the pictures on their card ---------- export const SHOOT_MIN = 1, SHOOT_MAX = 6, SHOOT_DEFAULT = 3;