{"release_id":"01a1998d-9802-0000-b0f7-33a23307f4ed","chunk_id":"01a1998d-9931-7222-b6f6-302ae433c1e8","file":"DWPQz4cz.mjs","mappings":"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","names":["NOT_ENOUGH_TOKENS","TOKENS_BY_SECONDS","LENGTHS","a","b","tokensForClip","seconds","s","exact","up","n","tokensForSend","clips","freeFirst","priceLabel","tokens","price","STUDIO_OG_IMAGE","STUDIO_TITLE","studioSeoByPath","getStudioSeo","pathname","override","front","clean","studioOrganizationSchema","origin","STUDIO_BRAND","studioWebsiteSchema","CLIP_SECONDS","spoken","items","last","words","studioAppSchema","STUDIO_FAQ","studioFaqSchema","f"],"sources":["../../../../../../src/lib/studio/pricing.ts","../../../../../../src/lib/studio-seo.ts"],"sourcesContent":["/** What a render costs, in tokens.\n *\n *  One table, read by both sides. The button has to name the price before the\n *  press and the server has to charge exactly that after it, and those two\n *  numbers agreeing is not something to leave to two implementations: a button\n *  that says four and a charge of six is worse than no price at all.\n *\n *  No imports on purpose. This sits under `$lib/studio` and is reached from the\n *  browser, so it must not pull in the database, the token ledger or anything\n *  else that belongs on the server — the seam in `host.ts` is the only way\n *  across, and a price is arithmetic rather than state.\n */\n\n/** What the operator is told when the purse is short.\n *\n *  Matched, not read. The page compares against this exact string to know that\n *  the answer is the shop rather than one more render that went wrong, so it\n *  lives beside the prices where both sides of the wire can see it. */\nexport const NOT_ENOUGH_TOKENS = 'not enough tokens';\n\n/** The price of one clip, by its length in seconds.\n *\n *  Not linear, and deliberately so: the fixed cost of a render — the model\n *  loading, the sandbox coming up — is most of a short clip and a fraction of a\n *  long one, so per second a fifteen lands cheaper than three fives. The three\n *  lengths here are the three the composer offers.\n */\nexport const TOKENS_BY_SECONDS: Readonly<Record<number, number>> = {\n\t5: 4,\n\t10: 6,\n\t15: 9\n};\n\n/** The lengths above, ascending — the fallback below walks them. */\nconst LENGTHS = Object.keys(TOKENS_BY_SECONDS)\n\t.map(Number)\n\t.sort((a, b) => a - b);\n\n/** What one clip of this length costs.\n *\n *  A length nobody offers still has to have an answer, because a stale draft or\n *  a continuation that inherits its length from the clip before it can arrive\n *  with something not in the table. It is charged at the next length up, and at\n *  the longest one beyond that: rounding toward the operator would let an\n *  unlisted length be the cheap way to buy a long clip.\n */\nexport function tokensForClip(seconds: number): number {\n\tconst s = Number(seconds);\n\tif (!Number.isFinite(s) || s <= 0) return TOKENS_BY_SECONDS[LENGTHS[0]];\n\tconst exact = TOKENS_BY_SECONDS[s];\n\tif (exact !== undefined) return exact;\n\tconst up = LENGTHS.find((n) => n > s);\n\treturn TOKENS_BY_SECONDS[up ?? LENGTHS[LENGTHS.length - 1]];\n}\n\n/** What a send costs: one clip's price for each clip it makes.\n *\n *  Versions and camera angles are two axes of the same thing — every message\n *  makes their product, and each one of those is its own render on its own GPU.\n *  So the price multiplies with them, which is also the only honest way to show\n *  it: \"two versions\" costs twice, and the button says so before the press.\n */\nexport function tokensForSend(seconds: number, clips = 1, freeFirst = false): number {\n\tconst n = Math.max(1, Math.round(Number(clips) || 1));\n\t// An account's first clip is free (freeclip.server.ts), so a send that makes\n\t// it owes one clip fewer.\n\treturn tokensForClip(seconds) * (freeFirst ? n - 1 : n);\n}\n\n/** The price as it appears on a button. Just the number and the word, because\n *  the button already says what it does. */\nexport function priceLabel(tokens: number, freeFirst = false): string {\n\tif (freeFirst && tokens === 0) return 'your first clip is free';\n\tconst price = `${tokens} ${tokens === 1 ? 'token' : 'tokens'}`;\n\treturn freeFirst ? `${price} · the first clip is free` : price;\n}\n","/** What PornPilot's pages say about themselves to search engines and link\n *  previews.\n *\n *  Everything served on the studio's own domain reads its meta from here rather\n *  than from ratemyd's seoByPath. A page there with no entry of its own falls\n *  back to PornPilot's front page, never to ratemyd's: before this existed the\n *  studio shipped ratemyd's rating-tool description and share card.\n */\nimport type { SeoMeta } from '$lib/seo';\nimport { STUDIO_BRAND } from '$lib/studio-host';\nimport { TOKENS_BY_SECONDS } from '$lib/studio/pricing';\n\nexport const STUDIO_OG_IMAGE = '/og/pornpilot.png';\n\n/** The studio's title, which is the front page's.\n *\n *  RAT-632: it names the search it answers, word for word. \"AI porn video\n *  generator\" alone matched none of the NSFW queries — \"nsfw ai video\n *  generator\", \"nsfw ai video\" — whose first page is small tools rather than\n *  tube sites, so the phrase went in beside it. The brand stays at the end,\n *  where every PornPilot title carries it. */\nexport const STUDIO_TITLE = 'AI Porn & NSFW AI Video Generator — PornPilot';\n\n/** Keyed by the canonical path (studioCanonicalPath): the studio is `/`. */\nexport const studioSeoByPath: Record<string, SeoMeta> = {\n\t'/': {\n\t\ttitle: STUDIO_TITLE,\n\t\t// RAT-632: the phrase people search for goes first, where the snippet is\n\t\t// read and bolded; the sentence that was here before follows it unchanged.\n\t\tdescription:\n\t\t\t'Uncensored NSFW AI video generator. Direct your own AI porn film. Describe the scene, keep your characters and get every clip back with sound, then carry it on into the next. Writing scenes and building characters is free.',\n\t\togImage: STUDIO_OG_IMAGE\n\t},\n\t// The number is the library's own count (PROMPT_COUNT); a test holds the two\n\t// together. A count in the title is what the libraries ranking for these\n\t// searches lead with (\"Best 50+ Hot Porn AI Prompts\").\n\t'/prompts': {\n\t\ttitle: '50 NSFW AI Prompts & AI Porn Prompts for Video — PornPilot',\n\t\tdescription:\n\t\t\t'50 NSFW AI prompts for video, sorted by scene: POV, sensual, oral, lesbian, gay and solo. Copy one, or open it in the studio and make the clip, with sound.',\n\t\togImage: STUDIO_OG_IMAGE\n\t},\n\t'/prompts/generator': {\n\t\ttitle: 'NSFW AI Prompt Generator — Free Video Prompts | PornPilot',\n\t\tdescription:\n\t\t\t'Free NSFW AI prompt generator for video: pick who, where, what happens, the camera and the sound, and get a prompt the studio reads well. Copy it or open it.',\n\t\togImage: STUDIO_OG_IMAGE\n\t},\n\t'/prompts/grok': {\n\t\ttitle: 'Grok NSFW Prompts & Spicy Mode: What It Allows — PornPilot',\n\t\tdescription:\n\t\t\t'Grok NSFW prompts written for spicy mode, what Grok Imagine allows and blocks under its 2026 rules, and where to make the explicit scenes it filters.',\n\t\togImage: STUDIO_OG_IMAGE\n\t}\n};\n\nexport function getStudioSeo(pathname: string, override?: Partial<SeoMeta> | null): SeoMeta {\n\tconst front = studioSeoByPath['/'];\n\tif (override && override.title && override.description) {\n\t\treturn { ...front, ...(override as SeoMeta) };\n\t}\n\tconst clean = pathname.replace(/\\/$/, '') || '/';\n\treturn studioSeoByPath[clean] ?? front;\n}\n\n/** What the studio domain's sitemap lists: the front page, which is the studio,\n *  and PornPilot's own pages beside it. /studio is the same page as `/`,\n *  canonical there, so it is not listed.\n *\n *  Each carries the day its words last changed, written by hand when they do —\n *  the same rule as ratemyd's sitemap (RAT-587): a real date or none, never a\n *  made-up \"today\" that crawlers learn to ignore. */\nexport const STUDIO_SITEMAP: readonly { path: string; lastmod: string }[] = [\n\t{ path: '/', lastmod: '2026-10-31' },\n\t{ path: '/prompts', lastmod: '2026-10-31' },\n\t{ path: '/prompts/generator', lastmod: '2026-10-31' },\n\t{ path: '/prompts/grok', lastmod: '2026-10-31' }\n];\n\nexport function studioOrganizationSchema(origin: string) {\n\treturn {\n\t\t'@context': 'https://schema.org',\n\t\t'@type': 'Organization',\n\t\tname: STUDIO_BRAND,\n\t\turl: origin,\n\t\tdescription: 'PornPilot is an AI porn video generator: adult film, directed by you.'\n\t};\n}\n\nexport function studioWebsiteSchema(origin: string) {\n\treturn {\n\t\t'@context': 'https://schema.org',\n\t\t'@type': 'WebSite',\n\t\tname: STUDIO_BRAND,\n\t\turl: origin\n\t};\n}\n\n/** The lengths a clip comes in, from the price table the composer and the\n *  server both read — so the copy below cannot promise a length nobody sells,\n *  or a price nobody charges. */\nconst CLIP_SECONDS = Object.keys(TOKENS_BY_SECONDS)\n\t.map(Number)\n\t.sort((a, b) => a - b);\n\n/** \"5, 10 or 15\": a list the way a sentence says it. */\nfunction spoken(items: (string | number)[], last: 'and' | 'or'): string {\n\tconst words = items.map(String);\n\treturn words.length < 2 ? (words[0] ?? '') : `${words.slice(0, -1).join(', ')} ${last} ${words.at(-1)}`;\n}\n\n/** The studio as an application, on the front page (which is the studio). The\n *  free part is writing scenes and building characters; clips are paid in\n *  tokens. It said \"your first clip is free\" while that was true — keep this in\n *  step with StudioSettings.freeFirstClip. */\nexport function studioAppSchema(origin: string) {\n\treturn {\n\t\t'@context': 'https://schema.org',\n\t\t'@type': 'WebApplication',\n\t\tname: STUDIO_BRAND,\n\t\t// RAT-632: what the application is called by the people searching for one,\n\t\t// the way /rate names itself the AI dick rater (RAT-623).\n\t\talternateName: [\n\t\t\t'NSFW AI video generator',\n\t\t\t'Uncensored AI video generator',\n\t\t\t'AI porn video generator'\n\t\t],\n\t\turl: origin,\n\t\tdescription: studioSeoByPath['/'].description,\n\t\tapplicationCategory: 'MultimediaApplication',\n\t\toperatingSystem: 'Any',\n\t\tbrowserRequirements: 'Requires JavaScript',\n\t\tisFamilyFriendly: false,\n\t\tfeatureList: [\n\t\t\t'Video from a written description',\n\t\t\t'Sound and spoken lines in every clip',\n\t\t\t'The same characters from clip to clip',\n\t\t\t'Clips that continue into the next one',\n\t\t\t`${spoken(CLIP_SECONDS, 'or')} second clips, portrait or landscape`\n\t\t],\n\t\toffers: {\n\t\t\t'@type': 'Offer',\n\t\t\tprice: '0',\n\t\t\tpriceCurrency: 'USD',\n\t\t\tdescription: 'Writing scenes and building characters is free; clips are paid in tokens'\n\t\t}\n\t};\n}\n\n/** The front page's questions, answered on the page (below the studio, in the\n *  empty stage) and nowhere else — the FAQPage markup is built from this same\n *  list, so the two cannot drift apart.\n *\n *  Everything here is a fact about the product as it ships, and some of it will\n *  change: prices and lengths come from the price table; the resolutions are\n *  the studio's RESOLUTIONS; \"nothing is shown to anyone else\" holds while\n *  there is no public share page (D8). Change the words when the product\n *  changes, and move the sitemap's lastmod with them. */\nexport const STUDIO_FAQ: readonly { q: string; a: string }[] = [\n\t{\n\t\tq: 'What is an NSFW AI video generator?',\n\t\ta: 'Software that turns a written description into adult video. PornPilot is one built for film rather than single pictures: it writes the direction from your words, keeps your characters the same from clip to clip, adds sound, and lets one clip carry on into the next.'\n\t},\n\t{\n\t\tq: 'Is PornPilot an uncensored AI video generator?',\n\t\ta: 'For adults, yes. Explicit scenes between consenting adults are what it is built for. Anyone under 18 is refused outright, and real people who have not agreed to it, or deepfakes of anyone, are against the rules.'\n\t},\n\t{\n\t\tq: 'Is it free?',\n\t\ta: `Writing scenes and building characters is free, and you can start writing without an account. Rendering a clip costs tokens: ${spoken(\n\t\t\tCLIP_SECONDS.map((s) => `${TOKENS_BY_SECONDS[s]} for ${s} seconds`),\n\t\t\t'and'\n\t\t)}.`\n\t},\n\t{\n\t\tq: 'How long are the clips, and do they have sound?',\n\t\ta: `Each clip is ${spoken(CLIP_SECONDS, 'or')} seconds long, in 288p, 576p or 864p, portrait or landscape, and comes back with sound. Carry clips on into one another and join them into a longer film.`\n\t},\n\t{\n\t\tq: 'How long does a clip take to make?',\n\t\ta: 'A few minutes, and longer clips and 864p take longer. You can close the tab while it renders: the clip is collected when it finishes and waits for you.'\n\t},\n\t{\n\t\tq: 'Can I make longer videos?',\n\t\ta: `Yes. A clip runs up to ${CLIP_SECONDS[CLIP_SECONDS.length - 1]} seconds, and you carry it on into the next one — the same people, the same voice — then join the clips into a single film.`\n\t},\n\t{\n\t\tq: 'Can I make gay or lesbian AI porn?',\n\t\ta: 'Yes. Say who is in the scene — two men, two women, a man and a woman — and the studio casts them. The prompt library has gay and lesbian prompts to start from.'\n\t},\n\t{\n\t\tq: 'Can I turn a photo into an NSFW video?',\n\t\ta: 'A photo of yourself, yes: attach it and the studio builds a character from it, then films that character in whatever scene you describe. Photos of anyone else are not allowed without their explicit consent.'\n\t},\n\t{\n\t\tq: 'Do I need to install anything?',\n\t\ta: 'No. It runs in the browser, on a phone or a computer, and the rendering happens on our servers. A clip that finishes while the tab is closed is waiting when you come back.'\n\t},\n\t{\n\t\tq: 'Is what I make private?',\n\t\ta: 'Yes. Your clips and characters belong to your account, and the studio does not show them to anyone else.'\n\t},\n\t{\n\t\tq: 'Does it make NSFW images too?',\n\t\ta: 'It draws the characters you build as stills, four views of each, so it can keep them the same from shot to shot. What it makes is video: an NSFW AI generator for clips, with motion and sound.'\n\t}\n];\n\n/** FAQPage markup for the questions above. Google stopped showing FAQ results\n *  for most sites in 2023, so this is not for a rich result: it states the\n *  answers in a form a search engine or an answer engine reads without having\n *  to find them in the layout. */\nexport function studioFaqSchema() {\n\treturn {\n\t\t'@context': 'https://schema.org',\n\t\t'@type': 'FAQPage',\n\t\tmainEntity: STUDIO_FAQ.map((f) => ({\n\t\t\t'@type': 'Question',\n\t\t\tname: f.q,\n\t\t\tacceptedAnswer: { '@type': 'Answer', text: f.a }\n\t\t}))\n\t};\n}\n"],"version":3}