Commit Graph

260 Commits

Author SHA1 Message Date
yuanzonghao 941b54c3f8 fix(share): require at least 1 byte of content in .infiplot files
Reject header-only files (37 bytes, empty plaintext) at the unpack
boundary instead of letting them through as empty strings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-18 21:56:38 +08:00
yuanzonghao 64cf9c330d refactor(share): remove GALLERY_SECRET, use plaintext + SHA-256 integrity for .infiplot files
The encrypted .infiplot format (AES-256-GCM via GALLERY_SECRET) provided no
meaningful security — the payload is AI-generated story content with no
credentials or PII, and the project is open source. Replace with plaintext +
SHA-256 integrity check (format v2). Story share is now always enabled without
requiring a server secret.

- galleryCrypto.ts: AES-256-GCM → plaintext + SHA-256 hash; remove secret param
- 4 API routes: remove GALLERY_SECRET guard and 503 fallback
- story-unpack: forward specific error messages (v1 compat, hash mismatch)
- gallery/page.tsx: remove stale AES-GCM comment
- AGENTS.md: document gallery-pack/gallery-unpack routes
- .env.example, wrangler.jsonc: remove GALLERY_SECRET references

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-18 21:41:56 +08:00
Zonghao Yuan 03dccd7c74 chore(deploy): add keep_vars and remove placeholder vars from wrangler.jsonc (#97)
Add "keep_vars": true so Dashboard-set variables survive redeployments
(Vercel-like behavior). Remove empty-string placeholder vars — provider
config (TEXT_BASE_URL, IMAGE_*, VISION_*, TTS_*) is now managed entirely
via Dashboard. Update comments to document three variable categories
(secrets, runtime, build-time) and their setup locations.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-18 20:22:54 +08:00
Zonghao Yuan 1a7d8a21a9 chore(deploy): sanitize wrangler.jsonc for open source, hide story persistence UI (#96)
- Replace hardcoded API URLs and model names with empty-string placeholders
  so self-deployers set their own values via Dashboard
- Comment out D1/R2/KV bindings and placement region to prevent deploy
  failure for users who don't need these optional services
- Add comprehensive comments for secrets, optional tuning vars, and
  build-time NEXT_PUBLIC_* variables
- Hide /stories navigation link until auth integration is ready
  (all story persistence code is retained)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-18 18:57:17 +08:00
Zonghao Yuan 0e4c2ebef4 feat(engine): merge cloudflare-migration — paradigm D engine, BYOK proxy, story persistence (#95)
Squash-merge the cloudflare-migration branch (7 commits by Kai ki) into
staging with conflict resolution, feature integration, and bug fixes.

Engine:
- Paradigm D: single-stream Writer replacing dual-phase Plan/Beats
- Delete Architect agent; story bible generated via Writer <plan> tag
- Modular prompt architecture (segments/registry/builder)
- StreamRouter for tagged stream splitting (<plan>/<story>/<choices>)

Infrastructure:
- Cloudflare Workers deployment (wrangler.jsonc, OpenNext adapter)
- D1 database schema + Drizzle ORM (scaffolded, not yet active)
- R2 storage helpers (scaffolded, not yet active)
- Story persistence API routes + client-side persistence

BYOK (Bring Your Own Key):
- /api/llm/user-proxy with SSRF-protected LLM proxy (+ requireUser auth)
- CORS-aware fetch in ai-client: auto-detect CORS failure, fallback to
  server proxy transparently via OpenAI SDK custom fetch
- BYO config support added to classify-freeform and vision routes
- SettingsModal CORS privacy notice (keys never logged/stored)

SSE streaming:
- engineClient.ts: fetchSSE helper for progressive scene events
- startSession/requestScene accept optional emit callback
- Fix SSE error event field name (error → message) in scene/start routes

i18n integration:
- Wire buildLanguageDirective into paradigm D's prompt builder
- Update corsNotice i18n keys (zh-CN/en/ja) with CORS proxy privacy text
- Preserve Session.language + LanguageSwitcher from i18n commit

Co-authored-by: Kai ki <155355644+zbf1009@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-18 18:05:38 +08:00
Qi Chen 05bd7e229c Merge pull request #94 from zonghaoyuan/feat/gender-x
feat(i18n): language switcher with en/ja translations
2026-06-18 17:01:20 +08:00
DESKTOP-I1T6TF3\Q 2d35c1d9de feat(i18n): add language switcher with en/ja translations
- New client-side i18n via React Context (useI18n, tArray, I18nProvider)
- Catalog ships 21 locale stubs; only zh-CN/en/ja have reviewed translations
- Header language switcher (globe icon + short label) before settings gear
- All hardcoded Chinese UI text migrated to keys: typewriter, options,
  hints (with embedded gear icon via dangerouslySetInnerHTML), settings
  panel, footer/about, play page hints
- AI output language follows user-selected locale via trailing one-liner
  directive appended to Architect/Writer/CharacterDesigner/InsertBeat
  user messages (preserves system-prompt cacheability)
- Per-locale separator rule: zh uses middot between every glyph; en/ja
  use plain spaces
- Option value → i18n key suffix maps preserve Chinese as the underlying
  identifier so analytics unions and STYLE_MAP keys stay byte-stable

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 16:54:35 +08:00
Zonghao Yuan 4abb57ec1f Merge pull request #89 from zonghaoyuan/feat/cla-mechanism
feat(repo): add CLA mechanism for external contributors
2026-06-18 11:05:26 +08:00
Zonghao Yuan 09844bd5db fix(web): migrate proxy.ts to edge middleware.ts for Cloudflare compat (#88)
Next.js 16 locks proxy.ts to the Node.js runtime, but OpenNext for
Cloudflare rejects Node.js middleware at build time ("Node.js middleware
is not currently supported", build.js exit 1). Rename to middleware.ts
with an explicit experimental-edge runtime so the Supabase SSR cookie
refresh runs on edge and stays deployable to both Vercel and Workers.

Supabase SSR only uses Web APIs (fetch, cookies), so it is
edge-compatible; the core getUser() refresh logic is unchanged. The
matcher excludes static assets by file extension (not by "contains a
dot") so future dotted dynamic routes (e.g. /u/john.doe) still get the
cookie refresh. getUser() is wrapped in try/catch so a transient
network error (rethrown by @supabase/auth-js) doesn't 500 or crash the
page request — the cookie simply isn't refreshed that round.

Note: runtime must be "experimental-edge", not "edge". Next.js 16 routes
the root middleware file through the pages-router static-info path, where
runtime "edge" throws E1015 at build ("Use runtime 'experimental-edge'
instead"). "experimental-edge" only warns; both are treated as edge by
isEdgeRuntime().

Verified: pnpm typecheck, pnpm build (Vercel), pnpm build:cf
(Cloudflare — Bundling middleware function -> OpenNext build complete,
node-middleware guard no longer fires).
2026-06-18 11:00:49 +08:00
Qi Chen 9ec21c46e7 Merge pull request #90 from zonghaoyuan/feat/gender-x
feat(options): add third gender option "X" for universal gender
2026-06-18 09:26:33 +08:00
DESKTOP-I1T6TF3\Q f1fe7964a2 feat(options): add third gender option "X" for universal gender
- Add "X" to GENDERS array in lib/options.ts
- Add example phrases for "X" gender (sci-fi themed)
- Make "X" use same preset cards as male gender
- Map "X" to "通用性别" when transmitting to AI
- Add "X" to DISPLAY_ORDER (same as male)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 09:18:50 +08:00
Zonghao Yuan 4c7bdcffa5 Merge pull request #87 from zonghaoyuan/worktree/update-readme-2
docs: sync README with recent changes (TTS, model support, Cloudflare)
2026-06-17 19:50:36 +08:00
yuanzonghao 6ee74a0680 chore(repo): address pr-agent review on CLA workflow
Address the valid points from pr-agent on PR #89, skip the inaccurate
ones (e.g. the $contributorName placeholder it suggested does not exist
in cla-assistant-action; create-file-commit-message fires before any
signer exists).

- cla.yml: move 'token configured?' check into job env and put the whole
  step-level if inside a single ${{ }} so && / || are evaluated as a
  boolean (step-level if cannot safely reference secrets.* directly)
- cla.yml: declare minimal explicit permissions (contents/pull-requests/
  issues/statuses) — this workflow runs on pull_request_target with a token
- cla.yml: drop the overly broad '*bot' allowlist wildcard; keep explicit
  bot + maintainer accounts only
- cla.yml: clean up the stray trailing '@' in create-file-commit-message
  (used once, at signature-store creation, before any signer exists)
- README{,.en,.ja}: clarify that the CLA is signed via a PR comment, not
  before opening the PR — matches the actual CONTRIBUTING flow
2026-06-17 19:49:48 +08:00
yuanzonghao 51afeb09ac docs: sync README with recent changes (TTS, model support, Claude caching)
- Model support: text/vision now OpenAI-compatible only (Claude/Gemini
  via their OpenAI-compatible endpoints); drop stale native-protocol copy
  and the anthropic/google rows from the *_PROVIDER table
- TTS: document StepFun (step-tts-2, paid, better quality) alongside
  Xiaomi MiMo (free) across zh/en/ja; update Vercel deploy envDescription
- Claude: note that direct Anthropic endpoints lack caching, recommend
  gateway for full experience
- Cloudflare: preserve existing one-click deploy (compat work in progress)
2026-06-17 18:26:36 +08:00
yuanzonghao 2b0b9c6f8d feat(repo): add CLA mechanism for external contributors
Introduce a Contributor License Agreement (CLA) so external contributions
can be licensed under AGPL-3.0 and any other terms (incl. closed-source),
keeping the AGPL-3.0 codebase usable in closed-source projects.

- CLA.md: authoritative English CLA (ICLA + employer authorization, v1.0)
- CLA.zh.md: non-binding Chinese reference translation
- CONTRIBUTING.md: bilingual contributing guide, points to CLA
- .github/workflows/cla.yml: self-hosted cla-assistant-action that records
  signatures into cla-signatures/version-1.json; exempts maintainers & bots
  via allowlist; skips when CLA_BOT_TOKEN is unset
- .github/PULL_REQUEST_TEMPLATE.md: guides contributors to sign
- README.{md,en.md,ja.md}: add License & contributing footer
- app/terms: note CLA requirement in the IP section

Enforcement requires repo-level setup (PAT secret + branch protection)
documented in cla.yml; not covered by this commit.
2026-06-17 12:23:46 +08:00
Zonghao Yuan 98d8e83c92 Merge pull request #85 from zonghaoyuan/test/image-compression
perf(image): switch Runware output format from PNG to WEBP
2026-06-15 18:07:39 +08:00
yuanzonghao d0faa06cc1 perf(image): switch Runware output format from PNG to WEBP
WEBP produces ~90% smaller files than PNG at visually identical quality
(tested: 5.4MB → ~550KB per 1792×1024 image), significantly reducing
client download time for users on slower connections.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-15 18:04:31 +08:00
Zonghao Yuan c452dcfeb0 Merge pull request #84 from zonghaoyuan/fix/header-icon-alignment
fix(web): hide login button when logged out to fix header icon alignment
2026-06-15 17:51:08 +08:00
yuanzonghao 60224ea33b fix(web): hide login button when logged out to fix header icon alignment
未登录时 UserChip 的登录按钮使用了深色背景配色(cream-50 色系),
在首页浅色背景上不可见但仍占据约 60px 宽度,导致齿轮/GitHub/X
图标被推离右边缘。改为未登录时不渲染 UserChip,登录入口保留在
开始游戏等需要认证的操作流程中。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-15 17:47:01 +08:00
yuanzonghao d29087a875 chore: add piolium/ to .gitignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-15 17:14:58 +08:00
Zonghao Yuan dc08f64ec1 fix(web): 恢复移动端 Header 的 GitHub/X 链接 (#81)
* fix(web): restore GitHub/X links in mobile header

上一个 PR (#80) 为缓解 Header 移动端拥挤,给 GitHub/X 图标加了
hidden sm:inline-flex,导致手机端(<640px)看不到这两个入口。
横向溢出的真正元凶(导入按钮负偏移)已在 #80 修复,Header 全显示
不会重新引入左右滑动,故恢复图标在所有视口常驻显示。

* fix(web): 移动端 UserChip 只显示头像,宽度与其他图标按钮对齐

登录态 UserChip 之前是「头像 + 名字」的宽按钮(≈90px),
是齿轮/GitHub/X 图标按钮(≈16px)的 5-6 倍,导致 Header
右侧集群宽度在登录/未登录切换时剧烈变化,所有按钮位置大幅偏移。

改动:
- 去掉名字显示,只保留头像圆圈
- 头像尺寸 h-5→h-4(16px,与图标按钮一致)
- 按钮 padding 收窄至 p-0.5(总宽≈22px)
- 加 title 属性保留完整用户名(hover 可见)
- 移除开发过程中使用的 mock 登录态临时代码
2026-06-15 15:28:24 +08:00
yuanzonghao ba9f9c1342 Merge PR #79: feat(tts): StepFun voice selection via CharacterDesigner + provider-aware beat-audio
- StepFun voice selection: CharacterDesigner picks a preset voiceId from the
  32-entry catalog (zero extra LLM call); pickStepfunVoiceId remains as fallback.
- Prebaked homepage cards enriched with stepfunVoiceId (147 characters, gemini model).
- /api/tts-provider endpoint + client probe: skip the ~220KB Xiaomi reference
  audio when the server runs StepFun (saves Fast Origin Transfer bandwidth).
- Server-side resolveVoice normalization: re-provisions on provider mismatch.
- Removed hardcoded 1.2x speech playback speed (was for slow MiMo voice).
- Hardened voice-provider validation per PR-agent review.

Xiaomi path prompt is byte-identical to history (prompt-cache-preserving).
2026-06-15 15:08:21 +08:00
Zonghao Yuan 6e1ad55f1a Merge pull request #82 from zonghaoyuan/fix/play-oauth-resume
fix(play): resume in-progress game after OAuth full-page redirect
2026-06-15 14:59:46 +08:00
yuanzonghao 65b7daff0b fix(beat-audio): harden voice-provider validation and resolveVoice fast path
Address PR-agent review findings:

- resolveVoice fast path: replace ambiguous boolean comparison
  (voiceProvider === "stepfun") === serverStepfun with explicit
  per-provider equality checks. Prevents an undefined or unknown
  provider from matching the non-stepfun (xiaomi) branch by accident.

- /api/beat-audio route: reject requests whose voice.provider is present
  but not in the VALID_TTS_PROVIDERS whitelist (e.g. "azure"). Previously
  such a request would pass validation when fallback fields were also
  present, and resolveVoice might use the invalid voice directly instead
  of falling back to reprovision — producing a silent beat instead of a
  voiced one.
2026-06-15 14:33:46 +08:00
yuanzonghao 3a012d46bf fix(auth): harden snapshot paths per PR agent review
Address two suggestions from the PR agent review:

1. lib/authResume.ts — catch isAuthed() exceptions in
   consumeResumeSnapshot. The network/timeout path now returns
   null (snapshot already removed earlier to prevent the play-page
   bootstrap's retryBootstrap loop from re-entering this path).
   Document the intentional removeItem-before-isAuthed ordering.

2. components/AuthModal.tsx — wrap onBeforeOAuth in try-catch so
   a snapshot failure (e.g. sessionStorage blocked in privacy mode)
   does not abort the OAuth flow and leave the UI stuck in loading.
2026-06-15 14:32:04 +08:00
yuanzonghao 0166c5e0a9 chore(home): re-enrich firstact voiceIds with gemini model
Re-ran scripts/enrich-firstacts-stepfun.mjs with gemini-3.1-flash-lite-preview
as the TEXT model (was deepseek-v4-flash). The new picks better match the
mysterious / cool / melancholic archetypes common in the curated cards
(e.g. 夜煌 清冷空灵+悲怆决绝 → lengyanyujie 冷艳御姐, was youyanvsheng).

Only stepfunVoiceId values changed across 52 cards; voice (Xiaomi) /
imageUrl / scene untouched. 0 failures across the 147-character run.

run: pnpm enrich:firstacts [--force] [--portrait]
2026-06-15 14:03:34 +08:00
yuanzonghao 17341cbd4a feat(play): remove hardcoded 1.2x speech playback speed
The SPEECH_RATE=1.2 constant was added to speed up the somewhat slow MiMo
voicedesign voice. With StepFun preset voices (whose tempo is already
appropriate) and no per-provider logic, a global 1.2x is no longer the
right default. Remove the constant and all 4 of its uses:

- the constant declaration + comment
- two `el.playbackRate = SPEECH_RATE` assignments (audio now plays at 1.0)
- the typewriter pacing divisor (`/ SPEECH_RATE`) — audio and text both
  return to original duration, staying in lockstep

A future user-facing speech-speed setting (UI control + persisted pref)
would be a separate feature with a different shape; no placeholder kept.
2026-06-15 14:03:20 +08:00
yuanzonghao 6060d76b44 fix(auth): close two regressions from the resume refactor
Critical: play-page bootstrap infinite loop when AUTH_ENABLED and no
resume snapshot. The refactor changed the gate from
`if (AUTH_ENABLED && hasSnapshot)` to `if (AUTH_ENABLED)`, so any
snapshot-less /play entry (the common case — normal card/preset/custom
start) entered the async branch, got null from consumeResumeSnapshot,
bumped retryBootstrap, and re-ran the effect forever. Restored the
peek-before-await: only enter the async resume branch when a snapshot
actually exists; otherwise fall straight through to normal bootstrap.
Verified via control-flow simulation across all three paths (no
snapshot / snapshot + signed in / snapshot + not signed in).

Major: homepage auto-started a game after a bare OAuth login. Routing
persistPendingStart through AuthModal.onBeforeOAuth fired it for every
OAuth redirect, including bare logins via UserChip / StyleModal
onRequireAuth (where pendingAction is null and the user only wanted to
sign in). Guarded the snapshot on `pendingAction === "start"` so only
the mid-start flow persists; bare logins no longer resurrect the form
and auto-start on return.
2026-06-15 14:03:14 +08:00
yuanzonghao 8cdeb1592f refactor(auth): share OAuth-resume plumbing between home and play pages
Extract the page-agnostic resume primitives into lib/authResume.ts:
- isAuthed() — single login check (was duplicated in app/page.tsx)
- writeResumeSnapshot(key, primary, fallbacks) — quota-safe sessionStorage
  write with ordered lighter-payload fallbacks (was hand-rolledTry/catch
  in both pages)
- consumeResumeSnapshot<T>(key) — consume-once resume gate that verifies
  the user is signed in before returning the snapshot, else clears it

Both pages now share this plumbing while keeping their own snapshot shapes
and restore side effects (home: form fields + start(); play: Session +
restorePlayResume + deferred action replay).

Unify the persist trigger: home previously snapshotted eagerly inside
start() before opening the modal, while play snapshotted in
AuthModal.onBeforeOAuth at redirect time. Move home to the same
onBeforeOAuth trigger so both pages persist at the single OAuth-redirect
instant — the eager-snapshot special case is gone, and OTP (no redirect)
keeps its in-place onSuccess resume on both pages.

Net: -21 lines. Behavior preserved for OTP; OAuth resume now consistent.
2026-06-15 14:03:14 +08:00
yuanzonghao 99ad8d111e fix(play): resume in-progress game after OAuth full-page redirect
Google/GitHub OAuth is a full-page round-trip that unmounts the app and
destroys the in-memory Session (the server is stateless). Returning to
/play?card=m0 re-bootstrapped from the first-act JSON, restarting the
story from scene 1 — the user lost all progress. OTP login kept state
in-memory (no redirect) and was unaffected.

Mirror the homepage 89a5c54 OAuth state-loss fix: snapshot the exact
scene/beat/visited-beats/orientation/image into sessionStorage just
before the redirect, then restore it on mount after the round-trip
(verified signed in). Re-resolve the remote image URL to a fresh blob
(blob: URLs are revoked on unmount). The pending action that hit the
401 (choice / freeform / background-click) is replayed once the restored
state commits, so the player lands exactly where they were headed.

Quota fallback drops the user-uploaded style-reference image (~100KB)
and retries; voices are kept (continuity over rare quota miss). Failure
to restore (corrupt snapshot / not signed in) relinquishes the bootstrap
slot and falls back to normal card/preset/custom start instead of a
blank loading screen.

AuthModal gains an optional onBeforeOAuth callback fired synchronously
before signInWithOAuth navigates away (sessionStorage.setItem is sync).
2026-06-15 14:03:14 +08:00
Zonghao Yuan 7f263b2b14 fix(web): improve home page mobile compatibility (#80)
- 消除移动端横向滑动:重构「载入剧情」按钮的负偏移定位为 right-0 锚定的操作集群
- 加固 overflow-x 兜底(wrapper 层 overflow-x-hidden)
- 收住类别下拉面板宽度,避免靠右选择器展开时溢出
- 移动端质感优化:Header 图标适配、Hero 留白、提示框内边距、风格弹窗小屏布局
- 「载入剧情」按钮改毛玻璃半透明,避免打字机占位文字穿透
2026-06-15 13:16:01 +08:00
yuanzonghao 375f401c8f fix(tts): persist stepfunVoiceId on Character + harden probe race
Two follow-ups from pr-agent review of #79:

1. director.ts voicePromises built a Character WITHOUT stepfunVoiceId, so
   on a StepFun server the client (which omits the voice payload to save
   FOT) echoed back only voiceDescription — and the server re-scored via
   pickStepfunVoiceId every beat instead of honoring the LLM pick. The
   whole "CharacterDesigner picks a preset id" mechanism was effectively
   bypassed on live StepFun sessions (it only worked for prebaked cards,
   which carry stepfunVoiceId in their JSON). Persist stepfunVoiceId onto
   the Character so the client→server round-trip keeps the LLM selection.

2. fetchBeatAudio's null-provider branch (probe pending) required
   speaker.voice and silently dropped a stepfun-only speaker. Accept any
   synthesizable source (voice | stepfunVoiceId | voiceDescription) so a
   slow getTtsProvider probe can't drop audio during the first scene's
   fetch window. The server resolveVoice normalizes regardless of which
   fields arrive.
2026-06-15 13:05:36 +08:00
yuanzonghao ff03f3c085 chore(home): enrich firstact JSONs with StepFun voiceId
Add characters[i].stepfunVoiceId to all 106 prebaked homepage first-act
JSONs (firstact/ + firstact-portrait/) so cards produce sound when the
server runs StepFun. Generated by scripts/enrich-firstacts-stepfun.mjs —
one TEXT-provider LLM call per character picking from the 32-preset
catalog. voice (Xiaomi reference audio), imageUrl, and scene are untouched;
only the new stepfunVoiceId field is appended.

All 147 characters across both orientation sets are enriched (0 failures).
2026-06-15 12:50:09 +08:00
yuanzonghao ca73a41a0b feat(tts): StepFun voice selection via CharacterDesigner + provider-aware beat-audio
Make homepage cards and live sessions produce sound when the server is
configured for StepFun TTS, instead of silently failing (the prebaked
Xiaomi voice was useless on a StepFun server, and wasted ~220KB/beat in
Fast Origin Transfer).

Three coordinated changes:

1. CharacterDesigner now picks a StepFun preset voice id directly from the
   32-entry catalog in the SAME LLM call that designs the character — zero
   extra latency, LLM-grade match quality. The Xiaomi prompt path is
   byte-identical to history (verified programmatically) so cache hit rate
   and voice quality are preserved. pickStepfunVoiceId (keyword scorer)
   remains the fallback for orphan speakers / invalid LLM picks.

2. The 32-preset catalog moves to lib/tts-client/stepfun-voices.json as the
   single source of truth, shared by the scorer, the CharacterDesigner
   prompt, /api/tts-provider, and the offline enrich script.

3. A new GET /api/tts-provider endpoint lets the client probe the server's
   TTS provider at /play mount. fetchBeatAudio then shapes its request body:
   on a StepFun server it sends the lightweight stepfunVoiceId /
   voiceDescription and omits the ~220KB Xiaomi reference audio (FOT saving
   ~13MB per protagonist per session on prebaked cards). requestBeatAudio
   re-provisions on a provider mismatch before synth, so audio never goes
   silent on a cross-provider replay or mid-session provider flip.

New type fields are all optional and backward-compatible: Character.stepfunVoiceId,
BeatAudioRequest.voiceDescription/characterName/stepfunVoiceId, voice made
optional. AGENTS.md updated for the new route, type fields, dependency map,
and StepFun voice-selection flow.
2026-06-15 12:49:25 +08:00
Zonghao Yuan da191dd7a2 fix(play): render AuthModal in immersive branch (#78)
手机竖屏 (orientation === 'portrait') 和桌面按 F 全屏
(presentation) 都会走 PlayInner 的 immersive 渲染分支,但该分支
加入时只带了 SettingsModal、漏掉了 AuthModal。导致这两条路径下
若 API 返回 401 触发 setAuthModalOpen(true),登录框不会被挂载,
用户无法登录继续游戏。

预设故事卡片入口 (onCardClick) 不做跳转前登录校验,未登录用户进
/play 后点选项即触发 401,在手机上复现该 bug。

补上与非 immersive 分支完全一致的 AuthModal 块,复用现有
authResolveRef 重试机制,登录成功后自动重放被拦截的请求。
2026-06-14 23:26:52 +08:00
Zonghao Yuan 74e87673d1 Merge pull request #77 from zonghaoyuan/feat/legal-pages
feat(web): add privacy & terms pages for Google OAuth verification
2026-06-14 23:06:54 +08:00
yuanzonghao d813d3dccf fix(web): clarify data transmission vs storage in legal pages
Distinguish between temporary server-side processing and persistent
storage to accurately reflect the actual data flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-14 23:04:22 +08:00
yuanzonghao b7ff39d467 feat(web): add privacy policy & terms pages, update homepage copy
Add /privacy and /terms pages for Google OAuth brand verification.
Update homepage: 内测→公测, remove sponsor text, refresh save tip,
simplify load button label, add footer legal links.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-14 22:49:02 +08:00
Zonghao Yuan 4812d5b0b7 Merge pull request #75 from zonghaoyuan/worktree-update-roadmap
docs: update Roadmap with completed milestones and new directions
2026-06-14 22:47:24 +08:00
yuanzonghao f8c1d4a8f5 docs: use inline code formatting for .infiplot extension
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-14 17:55:49 +08:00
yuanzonghao 989f2a7872 docs: update Roadmap with completed milestones and new directions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-14 17:45:27 +08:00
Zonghao Yuan 0dea2f8e36 fix(ai-client): clean up regressions from OpenAI SDK migration and canvas frame fix (#74)
Three follow-ups to ef3b579 (OpenAI SDK migration) and ebe39ef (canvas frame):

- .env.example / config.ts / AGENTS.md: anthropic & google native protocols
  were removed with the Vercel AI SDK, but .env.example and AGENTS.md still
  advertised them. Rewrite the docs to point Claude/Gemini at their
  OpenAI-compatible endpoints (api.anthropic.com/v1,
  generativelanguage.googleapis.com/v1beta/openai), drop the dead Gemini
  "Nano Banana" image example, sync AGENTS.md (text/vision protocol list,
  image protocol list, the "OpenAI/Gemini via AI SDK" reference note), and
  append a short hint in readProvider() error message guiding
  anthropic/google users to openai_compatible instead of a bare rejection.

- chat.ts: drop the unsafe `as { prompt_tokens_details?: ... }` cast; read
  cached_tokens straight off the SDK's CompletionUsage type. Add a comment
  noting the OpenAI usage object reports cache reads only (no cache-write
  count), so the create cost the old AI SDK path logged is unrecoverable.

- PlayCanvas.tsx: revert <img key={imageUrl}> to key={imageUrl.slice(-48)}.
  The gpt-image/mock paths emit multi-MB data URIs; using the full string as
  React's reconciliation key adds avoidable diff overhead during the frequent
  re-renders. Matches the existing <audio> element's key convention.

Validation: pnpm typecheck passes. (pnpm lint fails on a pre-existing Next 16
`next lint` CLI issue, identical on staging — unrelated to this change.)
2026-06-14 13:36:19 +08:00
Zonghao Yuan 9157454b46 Merge pull request #73 from zonghaoyuan/fix/restore-server-tts-and-fot
fix(play): restore server TTS, FOT strip/merge, nudge, and blob cleanup
2026-06-14 13:11:58 +08:00
yuanzonghao 2f6e67bd80 fix(play): restore server TTS, FOT strip/merge, nudge, and blob cleanup
Reverts the regressions from b63b694 on the server-fallback path:

P0 — fetchBeatAudio non-BYO branch was a bare return; every non-BYO
user got silent playback regardless of server TTS config. Re-connect
to /api/beat-audio with the beatAudioAbortRef signal, count 204/!ok
as silence strikes, create a blob URL on success.

P1 — stripVoicesForTransport + mergeCharactersPreserveVoice were
deleted, so the server-fallback path re-sent ~160KB
referenceAudioBase64 per character on every request AND lost voices
for already-known characters after scene 1. Re-add both, applied
ONLY on the server-fallback branches in engineClient.ts (BYO
client-direct path untouched).

P3 — the aborted-before-store blob URL race had no revoke, leaking
one blob URL per cancelled synth. Re-add the else-if revoke.

P2 — handleSettingsSaved ignored ttsConfigured, so a BYO key entered
mid-session only took effect after a page reload. Re-add the ref/state
refresh + audio re-prefetch. Also restore the silence-nudge UI
(silenceStrikes counter, SILENCE_NUDGE_THRESHOLD, dismissible pill
beside the mute toggle) that surfaces BYO-key guidance when the
shared server key is being rate-limited.

Verified live: /api/beat-audio now returns 200 (was 0 calls under
the bug); audio plays after synth completes.
2026-06-14 13:09:09 +08:00
Zonghao Yuan 5a966627a6 Merge pull request #72 from zonghaoyuan/fix/settings-ui-polish
fix(web): unify settings model sections and refine home hint
2026-06-14 12:44:51 +08:00
yuanzonghao 54a0083e23 fix(web): unify settings model sections and refine home hint
- Rename "自带配音 Key" → "配音模型", drop the section-level "可选" badge,
  and switch its icon to fa-volume-high to match the other model sections
- Drop redundant manual letter-spacing and "·" separators from settings
  field labels (let .smallcaps tracking handle spacing)
- Move the CORS endpoint note to the top of the Models tab
- Home hint: reword to "输入想法", mention text/image/vision models + voice
  key, and add an AUTH_ENABLED-gated "测试期间,登录即可免费畅玩" line

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:24:22 +08:00
Zonghao Yuan c8ffd6443b feat(home): localize character portrait URLs in prebaked first-act JSONs (#71)
* feat(home): localize character portrait URLs in prebaked first-act JSONs

Runware CDN URLs expire, breaking character portraits in prebaked story
cards. Download all 144 portraits as static WebP assets and rewrite
first-act JSONs to reference local paths instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(scripts): add fetch timeout and simplify resize logic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-14 11:23:17 +08:00
yuanzonghao 0c83f5f2a8 chore: gitignore local-only OpenDeploy and pitch files
- Dockerfile.opendeploy: local OpenDeploy build that hardcodes the public
  image-proxy URL; kept out of the repo so a public fork doesn't route image
  traffic through our Cloudflare Worker.
- .opendeploy: OpenDeploy CLI local context/credentials dir.
- pitch/: local pitch materials.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 00:56:54 +08:00
Zonghao Yuan d5ae45b943 Merge pull request #68 from zonghaoyuan/feat/supabase-auth
feat(auth): add Supabase auth with Google, GitHub, and email OTP login
2026-06-13 23:49:15 +08:00
yuanzonghao cb830f023d Merge origin/staging into feat/supabase-auth
Resolve conflicts: keep login_success alongside the new play_error /
play_visibility_lost analytics events; fold auth retry into the play-page
catch blocks so 401s open the login modal and are NOT tracked as play_error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 23:44:23 +08:00