"It becomes you." Two skins, a living voice, a status bar you can feel, and a clone that already knows you.
Build target: Claude Opus 5 · Creative/plan: Claude Fable 5 (this document) · Architecture critique: folded in (system-architect pass) · Date: 2026-08-29
App: https://clone.arnao.ai · source /Users/byronarnao/clone-mvp · worker /Users/byronarnao/clone-absorb
Predecessor: Absorption v2 proposal (the UX overhaul this builds on)
You have three separate wallets, and topping up one never funds the others:
| Wallet | Powers | State today |
|---|---|---|
| Anthropic subscription plan | Claude Code / me (this session) | Fine — this is what you pay monthly |
Anthropic API key (ANTHROPIC_API_KEY) | clone's server chat, the fable-critique skill, Gia Signal-on-Fable | Empty — "credit balance too low" |
OpenRouter (OPENROUTER_API_KEY) | clone chat (I wired it in), can route Fable/Opus too | ~$12.50 left of $250 ($237 used) |
Recommendation: top up OpenRouter — it consolidates everything (clone chat + I can re-point the critique skill and worker LLM calls through one balance, at OpenRouter's no-markup token rates), and separately drop ~$20 on the Anthropic API credits only if you want the direct-API skills back verbatim. Neither is your subscription. One-tap: Anthropic API billing · OpenRouter credits.
Why it matters for this PRD: the Mac worker (watch.py) runs Gemini Flash end-to-end — persona, first-line, and any new research summarization. It has no premium path. So "use the default Opus 5 model" (v2 §5) describes the chat path only; the worker needs its own funded LLM route (OpenRouter is the clean answer). This is load-bearing for the research agent below.
Three things Byron asked for, layered onto Absorption v2:
Plus the v2 backlog and three real bugs the architecture pass surfaced (§6).
Verdict: sound. One hard constraint: the lines are static, so never run TTS at runtime.
/voice/<theme>/<beat>.mp3). Zero per-user cost, zero worker load, no Chatterbox on the request path.playHello), and the milestone callouts that name the person are rendered by the worker. Everything else is static.SpeechSynthesis is acceptable only for the Absorption theme (robotic is on-brand). The warm theme must use pre-rendered clips — browser voices are uncanny-bad for "human."Proven already: both hero lines rendered locally today —
Absorption hero (cold) · Cloning hero (warm).
| Beat | ABSORPTION (cold · 2001 · the machine takes you in) | CLONING (warm · human · a reflection) |
|---|---|---|
| Land | "Step closer. I want to see what you're made of." | "Let's make your reflection." |
| Disclose (unlock gesture) | "This takes more than your picture — your voice, your patterns, the way you decide. Hold the light when you're ready." | "This isn't just a photo. I'll learn your voice, and the small ways you're you. Touch the light when you're ready." |
| Arm | "Steady." | "There. Hold still." |
| Face lock (HERO) | "Absorption initiated. I have your face." | "There you are. I see you." |
| Capture | "Held. That version of you is mine now." | "That's the you the world meets. Got it." |
| Interview | "A face is only the surface. Now I take how you think." | "A picture isn't a person. Tell me who you are underneath." |
| 40% | "I'm beginning to sound like you." | "You're starting to feel familiar." |
| 70% | "I know what you'll say before you say it." | "You feel like someone I already know." |
| 90% | "Almost all of you." | "Nearly you." |
| Synthesize | "Assembling." | "Bringing you together." |
| Endpoint (clone speaks, your voice) | "Absorption complete." → twin: "So — ask me something only I would know." | "Come meet yourself." → twin: "Hi. It's strange, isn't it, hearing me be you." |
| Dossier reveal | "I went looking. This is what the world already knows about you." | "I looked you up, the way anyone might. Tell me what's really true." |
Host narrator voice is a design choice per theme (cold synthetic vs warm human) — distinct from the user's cloned voice, which only speaks the endpoint. Production picks two host reference voices; the demo above reuses Byron's sample at two exaggeration settings to prove the mood range.
**Verdict: needs rework. The existing HOST_SKINS cannot carry this — it swaps one CSS var (--accent) and six copy strings. You're renaming a product, not recoloring an accent.**
THEMES = {
absorption: {
term: { verb:"absorb", gerund:"absorbing", noun:"absorption", done:"Absorbed." },
palette: { bg:"#05070a", accent:"#4fd6e0", type:"grotesk, wide-tracking" }, // cold monolith
voiceSet: "absorption", // → /voice/absorption/*.mp3
soundscape: "low-sine-drone + single tone on lock",
copy: { ...full per-beat strings... }
},
cloning: {
term: { verb:"clone", gerund:"cloning", noun:"clone", done:"Here you are." },
palette: { bg:"#0f0b08", accent:"#e0a24f", type:"humanist, soft" }, // warm (≈ current default)
voiceSet: "cloning",
soundscape: "warm pad + gentle chime",
copy: { ... }
}
}
term across all UI copy AND anywhere the literal word "absorption" is currently baked into state/status strings. (Worker stage names — developing/voice/avatar/persona — stay internal; they're never shown, so they don't need renaming. Only user-facing copy reads from term.)resolveSkin() pattern (URL param → env default → neutral). New order: ?theme= URL param (shareable) → localStorage (sticky between visits) → env default → cloning.resolveSkin() runs once in boot() today — the toggle needs a applyTheme(key) that's callable at runtime, not a one-shot setProperty.Verdict: risky. Build P2, behind an owner/admin flag, identity-asserted, reverse-image OFF. Three of the ask's load-bearing claims are false or unenforceable against the current code — corrected below.
While your face and voice absorb, a second track — the Dossier — quietly searches the public web for you and returns confirm cards:
> "I found this. Is it you?"
> AI/ML engineer at AWS, Responsible AI — source: linkedin.com/in/… — [Yes, that's me] · [No] · [Skip]
Only cards you tap Yes on join your clone's persona. Each card cites a source URL. The Dossier sub-track fills the status bar alongside Face/Voice/Mind.
watch.py) is strictly sequential — one job at a time inside process_job. A research stage would add 10–60s to the critical path and, if a search hangs, strand the user behind the 20-minute stale timeout. → Build it as a separate research-jobs/ record claimed by a second worker loop, genuinely concurrent with absorption.claims.email + claims.name from the signed session + what the user explicitly types** ("I'm an AI engineer at AWS"). This eliminates reverse-image search as an input entirely. Reverse-image, if ever, is isAdmin-only and disabled, not merely defaulted off.COST.research, its own BURST_PER_MIN entry, charged at create-time, summarization routed through the funded OpenRouter path.BRAVE_API_KEY, already used by generate-rai-content.py) or the harness search agent. Never scrape LinkedIn/Facebook/Reddit directly — their ToS ban it and the IPs get blocked.gen_persona today commands "do not invent biography" (watch.py:208). Appending research contradicts it → confidently-wrong facts the user rubber-stamps. Confirmed facts go in a separate "verified facts" block with its own grounding rule.research-jobs/ and any research derived/ path must be added to delete-me.js's prefix list in the same PR that creates them (see Bug 3).One hero number, decomposed into what's being taken. This is the "I want to see how much has happened and how much is left" ask.
ABSORBING ▓▓▓▓▓▓▓░░░ 68%
Face ✓ Voice ▓▓▓░ Mind ▓▓░░ Dossier ▓░░░
absorb-status; the bar only moves on actual server progress, never a fake timer. The v2 "holding beat" covers slow renders.absorb-create.js:32 charges a hardcoded ABSORB_CENTS = 8; _meter.js COST.absorb is never read on this path. My v2 "raise COST.absorb to 15" edit would do nothing. → Fix the literal in absorb-create.js (or make it read COST.absorb). Real absorption cost ≈ 13¢; chat turn under the new tokenizer ≈ 1.7¢ not 1¢. Re-price both so the $1 cap means what it says before adding the metered research op.jobView() is an allowlist — new fields are invisible. absorb-status.js:25-43 projects a fixed field set; every new field (v2 heartbeat, voice-early stage, research summary, theme) is silently dropped until jobView is extended. → Make "extend jobView" an explicit line item for every new job field, v2 and v3 alike. This is the #1 "we built it and the UI shows nothing" trap.delete-me.js:46-50 deletes a hardcoded prefix list. Any new prefix (research-jobs/, research derived/) survives "delete my clone" — and research data is the one category that names non-consenting third parties. → Register new prefixes in delete-me.js in the same PR; make it a checklist gate. Sharpest legal exposure in the plan.absorb-jobs/<sub>/<id>.json, bump schema:1→2): add theme (active manifest key), heartbeat (worker liveness echo, v2), research_job_id (pointer, nullable).research-jobs/<sub>/<id>.json): { schema, sub, jobId, status, claims:[{ text, sourceUrl, sourceType, confidence, decision:"pending"|"confirm"|"reject", namesThirdParty:bool }], summary, error, updated }.decision into the research record; the second loop's persona-merge reads only confirm claims.derived/_worker/heartbeat.json or similar): written every cycle; absorb-status exposes it so the UI tells "still cooking" from "the forge is cold."| Phase | Scope | Gate |
|---|---|---|
| P0 — de-risk what's already claimed | Bug fixes 1–3 (metering literal, jobView extension discipline, delete-me prefixes). Ship Absorption v2 (v2 §2) with jobView/delete-me as explicit line items. Worker gets a funded LLM route (OpenRouter). | metering re-priced & verified before any new metered op |
| P1 — voice + themes | Feature 1 (pre-rendered voice sets, DISCLOSE audio-unlock, captions) + Feature 2 (theme manifest, live toggle, re-prime on toggle) + the Absorption Meter (§5). Low risk, high payoff, no new backend surface. Build the manifest first — the voice sets are per-theme. | works sound-off (captions) and on mobile (unlock) |
| P2 — Dossier, flagged & owner-only | Feature 3 behind isAdmin, identity-asserted, reverse-image disabled, separate research-jobs + second worker loop, Brave Search, per-claim provenance, exclusion-default, delete-me prefix registered same PR. | self-only owner path proven before it's even a question whether non-owners get it — answer stays "no" until the face-belongs-to-user problem has a real gate (liveness/selfie-match, its own project) |
Do not let Feature 3 ride P0's coattails. It's the one feature that can turn a consented self-portrait into an abuse tool.
@vercel/blob; worker = Python watch.py polled by loop.sh (relaunch after every reboot — TCC blocks launchd; a real restart-survival mechanism is worth adding).bash ~/.claude/skills/vercel-deploy/deploy.sh ~/clone-mvp clone --project clone-app.api/absorb-create.js · api/absorb-status.js · api/_meter.js · api/delete-me.js · clone-absorb/watch.py · index.html (HOST_SKINS + resolveSkin, ~279–374).Talking head: FasterLivePortrait-MLX (proven 47.9s local). Voice: Chatterbox (local, $0). Interview science: Stanford genagents (finite interview ≈ 85% fidelity). Search backbone: Brave Search API (in-fleet, generate-rai-content.py). Meet stand-in (v2): joinly. Models: refreshed benchmarks.
| What | Link |
|---|---|
| This PRD (published) | https://clone-v3.arnao.ai |
| Absorption v2 proposal (predecessor) | https://clone-v2.arnao.ai |
| Live app | https://clone.arnao.ai |
| Hero voice — Absorption (cold) | https://clone-v2.arnao.ai/assets/theme-absorption-hal.wav |
| Hero voice — Cloning (warm) | https://clone-v2.arnao.ai/assets/theme-cloning-warm.wav |
| Talking-head smoke test | https://clone-v2.arnao.ai/assets/byron-talkinghead-quality.mp4 |
| Anthropic API billing | https://console.anthropic.com/settings/billing |
| OpenRouter credits | https://openrouter.ai/settings/credits |