{"release_id":"01a1998d-9802-0000-b0f7-33a23307f4ed","chunk_id":"01a1998d-9907-75d1-b185-18d7b1831283","file":"CBo7aAPg.mjs","mappings":"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","names":["SCENARIOS","SCENARIO_ORDER","CHIP_EFFECT","TONES","ROLES","HOUSE","housePortrait","id","voiceFor","description","d","NAMES","herName","taken","free","n","pool","READY","SESSION_TOKENS","EXTEND_TOKENS","EXTEND_MINUTES","chaptersFor","minutes","BOT","SEARCH","hostOf","url","shouldOnboard","v","q","fmtTime","s"],"sources":["../../../../../../src/lib/studio/session/shared.ts"],"sourcesContent":["/** Guided sessions: what the page and the server both need to know.\n *\n *  A session is the studio's second door. A clip is a scene the user writes; a\n *  session is ten or twenty minutes she leads, written for them and spoken in\n *  her voice, with her on screen lip-synced to it. Nothing to type: a scenario,\n *  a few taps of setup, and it plays.\n *\n *  The five scenarios are the clip stores' own best sellers (iWantClips' top\n *  categories, 2026-10): Gooning #1, Mind Fuck #2 with Captivate #5, JOI #3,\n *  Humiliation #6, FemDom #7. Findom is left out on purpose — the payment\n *  processor's and the voice provider's rules both bar money demands.\n *\n *  Client-safe: no imports.\n */\n\nexport type Scenario = 'gooning' | 'mind' | 'joi' | 'humiliation' | 'femdom';\n\nexport interface ScenarioInfo {\n\tname: string;\n\t/** One line under the name on the card. */\n\tline: string;\n\t/** What the card says about length. */\n\tbest: string;\n\t/** The length the setup opens on. */\n\tminutes: 10 | 20;\n\t/** Whether she keeps a beat the listener follows — the tempo dots. */\n\ttempo: boolean;\n\t/** The beat she opens on, per minute, before the writer sets one. */\n\tbpm: number;\n\t/** The four one-tap replies, in the order they sit above the controls.\n\t *  Taken from what people type to humiliation characters in roleplay: the\n\t *  median message is two words, and these are the commonest. */\n\tchips: string[];\n}\n\nexport const SCENARIOS: Record<Scenario, ScenarioInfo> = {\n\tgooning: {\n\t\tname: 'Gooning',\n\t\tline: 'Long and slow, on her tempo.',\n\t\tbest: 'Best at 20 min',\n\t\tminutes: 20,\n\t\ttempo: true,\n\t\tbpm: 40,\n\t\tchips: ['Slower', 'More', 'I’m close', 'Let me finish']\n\t},\n\tmind: {\n\t\tname: 'Mind games',\n\t\tline: 'Trance, tease and conditioning.',\n\t\tbest: 'Best at 20 min',\n\t\tminutes: 20,\n\t\ttempo: false,\n\t\tbpm: 0,\n\t\tchips: ['Again', 'Slower', 'More', 'I came']\n\t},\n\tjoi: {\n\t\tname: 'Timed JOI',\n\t\tline: 'She counts. You follow.',\n\t\tbest: '10 min',\n\t\tminutes: 10,\n\t\ttempo: true,\n\t\tbpm: 60,\n\t\tchips: ['I’m close', 'Slower', 'Faster', 'Let me finish']\n\t},\n\thumiliation: {\n\t\tname: 'Humiliation',\n\t\tline: 'Every word chosen to sting.',\n\t\tbest: '10 min',\n\t\tminutes: 10,\n\t\ttempo: false,\n\t\tbpm: 0,\n\t\tchips: ['More', 'Meaner', 'Beg', 'I came']\n\t},\n\tfemdom: {\n\t\tname: 'Femdom',\n\t\tline: 'Her rules, her voice.',\n\t\tbest: '10 min',\n\t\tminutes: 10,\n\t\ttempo: false,\n\t\tbpm: 0,\n\t\tchips: ['Yes, Mistress', 'Beg', 'More', 'I came']\n\t}\n};\n\nexport const SCENARIO_ORDER: Scenario[] = ['gooning', 'mind', 'joi', 'humiliation', 'femdom'];\n\nexport function isScenario(v: unknown): v is Scenario {\n\treturn typeof v === 'string' && v in SCENARIOS;\n}\n\n/** What a tapped reply does besides her answering it. */\nexport interface ChipEffect {\n\t/** Multiplies the beat she keeps. */\n\ttempo?: number;\n\t/** Ends the session on her terms: a countdown, or praise for having come. */\n\tfinish?: 'countdown' | 'came';\n}\n\nexport const CHIP_EFFECT: Record<string, ChipEffect> = {\n\t'I’m close': { tempo: 0.75 },\n\tSlower: { tempo: 0.75 },\n\tFaster: { tempo: 1.3 },\n\t'Let me finish': { finish: 'countdown' },\n\t'I came': { finish: 'came' }\n};\n\n/** How she talks to you. Multi-select: the voice fields of humiliation\n *  characters carry about twelve traits each, never one. Ordered by how often\n *  they appear there (crude 85%, mocking 78%, commanding 45%, cruel 26%…). */\nexport const TONES = [\n\t'Mocking',\n\t'Commanding',\n\t'Dirty-mouthed',\n\t'Cruel',\n\t'Cold',\n\t'Sweet',\n\t'Playful',\n\t'Whispering'\n] as const;\n\n/** Who she is to you. Girlfriend/wife/ex 13.6% of humiliation chats, a\n *  partner's friend 11.6%, gym trainer 8.4%, boss 1.9%. Step-family (10.1%) is\n *  left out deliberately: processors and clip stores ban incest themes. */\nexport const ROLES = [\n\t'Just her',\n\t'Your girlfriend',\n\t'Your ex',\n\t'Your girlfriend’s friend',\n\t'Your trainer',\n\t'Your boss'\n] as const;\n\nexport interface HouseHer {\n\tid: string;\n\tname: string;\n\t/** The two words under her picture. */\n\tsub: string;\n\t/** What she looks like, for when he asks her. */\n\tlook: string;\n\t/** xAI voice. Chosen by measured pitch: luna is the lowest of the seven\n\t *  female voices (151 Hz), carina the highest (219), celeste between. */\n\tvoice: string;\n}\n\nexport const HOUSE: HouseHer[] = [\n\t{\n\t\tid: 'vera',\n\t\tname: 'Vera',\n\t\tsub: 'low, slow',\n\t\tvoice: 'luna',\n\t\tlook: 'early thirties, dark brown hair in a low sleek ponytail, dark brown eyes, small gold earrings, a fitted black turtleneck dress'\n\t},\n\t{\n\t\tid: 'mila',\n\t\tname: 'Mila',\n\t\tsub: 'teasing',\n\t\tvoice: 'carina',\n\t\tlook: 'mid twenties, wavy honey-blonde hair to her shoulders, green eyes, a soft cream off-the-shoulder sweater'\n\t},\n\t{\n\t\tid: 'noor',\n\t\tname: 'Noor',\n\t\tsub: 'cold',\n\t\tvoice: 'celeste',\n\t\tlook: 'late twenties, straight black hair in a sharp chin-length bob, cool grey eyes, a tailored black blazer over a black silk blouse'\n\t}\n];\n\nexport const housePortrait = (id: string) => `/studio/session/${id}.webp`;\n\n/** An xAI voice for a character whose voice is a sentence written for the\n *  video model (\"low and husky, slow\"). Pitch-matched: luna is the lowest of\n *  the seven female voices, carina the highest. */\nexport function voiceFor(description: string | undefined): string {\n\tconst d = (description ?? '').toLowerCase();\n\tif (/\\b(low|deep|husky|smoky|sultry|raspy|throaty)\\b/.test(d)) return 'luna';\n\tif (/\\b(cold|flat|clipped|icy|stern|calm)\\b/.test(d)) return 'celeste';\n\tif (/\\b(bright|high|sweet|girly|bubbly|light|giggl)/.test(d)) return 'carina';\n\treturn 'eve';\n}\n\n/** A name for a character made from the session sheet. The clip studio names\n *  one after the first words of her description, which gave a session\n *  \"A photography of full body is ready\". */\nconst NAMES = [\n\t'Kira',\n\t'Lena',\n\t'Ivy',\n\t'Nadia',\n\t'Sasha',\n\t'Elena',\n\t'Mara',\n\t'Zoe',\n\t'Talia',\n\t'Rhea',\n\t'Nina',\n\t'Alix'\n];\nexport function herName(taken: string[]): string {\n\tconst free = NAMES.filter((n) => !taken.includes(n));\n\tconst pool = free.length ? free : NAMES;\n\treturn pool[Math.floor(Math.random() * pool.length)];\n}\n\n/** Ready to play: a scenario, a character and a tone already chosen. */\nexport const READY: {\n\t/** What a link from the rest of the site names it by: `/studio?session=<id>`. */\n\tid: string;\n\ttitle: string;\n\tscenario: Scenario;\n\ther: string;\n\tminutes: 10 | 20;\n\ttones: string[];\n}[] = [\n\t{\n\t\tid: 'slow-count',\n\t\ttitle: 'The slow count',\n\t\tscenario: 'joi',\n\t\ther: 'vera',\n\t\tminutes: 10,\n\t\ttones: ['Commanding']\n\t},\n\t{\n\t\tid: 'her-tempo',\n\t\ttitle: 'Her tempo',\n\t\tscenario: 'gooning',\n\t\ther: 'mila',\n\t\tminutes: 20,\n\t\ttones: ['Playful', 'Mocking']\n\t},\n\t{\n\t\tid: 'house-rules',\n\t\ttitle: 'House rules',\n\t\tscenario: 'femdom',\n\t\ther: 'noor',\n\t\tminutes: 10,\n\t\ttones: ['Cold', 'Commanding']\n\t}\n];\n\nexport const SESSION_TOKENS = 25;\nexport const EXTEND_TOKENS = 15;\nexport const EXTEND_MINUTES = 10;\n\n/** Chapters per length: four for ten minutes, eight for twenty. */\nexport const chaptersFor = (minutes: number) => Math.max(1, Math.round((minutes / 10) * 4));\n\n/** What the setup sheet sends. */\nexport interface SessionSetup {\n\ttones: string[];\n\ttoneNote?: string;\n\trole: string;\n\troleNote?: string;\n\tpet?: string;\n\tsize?: string;\n\tminutes: 10 | 20;\n\tvideo: boolean;\n\t/** Typed into the composer instead of picking: \"what you want in your own words\". */\n\trequest?: string;\n}\n\n/** Who she is, as the page shows her. */\nexport interface HerView {\n\tid: string;\n\tname: string;\n\tsub: string;\n\tportrait: string;\n\thouse: boolean;\n\t/** For one of the user's own characters: whether her talking clip exists yet. */\n\tready?: boolean;\n}\n\nexport type Cue = [t0: number, t1: number, text: string];\n\n/** One stretch of the session the player can play: her voice, and her saying it. */\nexport interface SegView {\n\tid: string;\n\tchapter: number;\n\tdur: number;\n\tcues: Cue[];\n\taudio: string;\n\tvideo?: string;\n}\n\nexport interface ChapterView {\n\tn: number;\n\ttitle: string;\n\tbpm: number | null;\n\t/** Seconds, once every stretch of it is voiced. */\n\tdur?: number;\n\tready: boolean;\n}\n\nexport interface TellView {\n\tid: string;\n\ttext: string;\n\tstate: 'answering' | 'ready' | 'failed' | 'care';\n\treply?: string;\n\tseg?: SegView;\n\t/** Her answer's voice request for the live stream, signed: it says it itself. */\n\tlive?: string;\n}\n\nexport type Ending = 'done' | 'early' | 'finished';\n\nexport interface SessionView {\n\tid: string;\n\ttitle: string;\n\tscenario: Scenario;\n\t/** `idle`: her talking clip, looped under her voice until a stretch's own\n\t *  lip-synced video exists. */\n\ther: HerView & { idle?: string };\n\tminutes: number;\n\tvideo: boolean;\n\t/** Her mouth can be made live as it plays (a live stream, her face prepared):\n\t *  the page asks for a ticket and streams the session from the GPU. */\n\tlive: boolean;\n\tstate: 'making' | 'ready' | 'failed';\n\t/** Shown on the generating screen: which of the three steps are done. */\n\tsteps: { written: boolean; voiced: boolean; scene: boolean };\n\terror?: string;\n\tchapters: ChapterView[];\n\t/** Every playable stretch, in order. A stretch is listed only once it can\n\t *  play, and only while every one before it can too. */\n\tsegs: SegView[];\n\t/** Stretches still being made. Zero once the whole session exists. */\n\tpending: number;\n\t/** Expected length in seconds: measured where voiced, estimated elsewhere. */\n\ttotal: number;\n\treactions: Record<string, SegView>;\n\tfinish: { countdown?: SegView; came?: SegView };\n\ttells: TellView[];\n\tprogress: { seg: string; t: number } | null;\n\tended: Ending | null;\n\trating: 'up' | 'down' | null;\n\texports: { state: 'none' | 'making' | 'ready' | 'failed'; video?: string; audio?: string };\n\tcreatedAt: number;\n\tprice: number;\n}\n\n/** A line in the sidebar's Your sessions. */\nexport interface SessionSummary {\n\tid: string;\n\ttitle: string;\n\tscenario: Scenario;\n\ther: string;\n\tminutes: number;\n\tended: Ending | null;\n\tcreatedAt: number;\n}\n\n/** Crawlers and link previewers, by user agent. */\nconst BOT =\n\t/bot|crawl|spider|slurp|mediapartners|lighthouse|headless|preview|facebookexternalhit|embedly/i;\n/** Search engines, by the host a visitor came from. */\nconst SEARCH = /(^|\\.)(google|bing|yahoo|duckduckgo|yandex|baidu|ecosia|qwant|startpage|brave)\\./i;\n\nfunction hostOf(url: string): string {\n\ttry {\n\t\treturn url ? new URL(url).hostname : '';\n\t} catch {\n\t\treturn '';\n\t}\n}\n\n/** Whether this visit opens on the Clip/Session chooser.\n *\n *  Only a first visit, and never in front of a search engine's renderer nor in\n *  front of somebody who has just come from one: they clicked a result for the\n *  page underneath — its H1 and copy are what the result promised (RAT-632) —\n *  and Google demotes a mobile page that hides that behind a full-screen\n *  interstitial. Nor in front of a prompt handed over from the prompt library\n *  (/?prompt=…): that visitor has already chosen, and the prompt is waiting in\n *  the clip composer. A search visitor is not marked as having seen it, so a\n *  later direct visit still can. */\nexport function shouldOnboard(v: {\n\tseen: boolean;\n\tuserAgent: string;\n\treferrer: string;\n\tsearch: string;\n}): boolean {\n\tif (v.seen || BOT.test(v.userAgent)) return false;\n\tif (SEARCH.test(hostOf(v.referrer))) return false;\n\t// Sent with something in hand — a prompt, or a session from the rest of the\n\t// site — the visitor has already chosen.\n\tconst q = new URLSearchParams(v.search);\n\treturn !q.has('prompt') && !q.has('session');\n}\n\nexport function fmtTime(s: number): string {\n\tconst v = Math.max(0, Math.round(s));\n\treturn `${Math.floor(v / 60)}:${String(v % 60).padStart(2, '0')}`;\n}\n"],"version":3}