Commit Graph

99 Commits

Author SHA1 Message Date
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
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
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
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
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
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 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 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
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
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
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
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
yuanzonghao 11f5ca83ec fix(auth): reject control chars in OAuth callback next param
Defense-in-depth against header injection if the post-login redirect
target ever reaches a context that doesn't re-encode it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 23:19:44 +08:00
yuanzonghao 89a5c54065 fix(auth): address PR review and OAuth state-loss bugs
- proxy: await getUser() so refreshed session cookies land on the response
- callback: gate on AUTH_ENABLED, reject non-relative next (open redirect)
- page: snapshot + resume form and style image across the OAuth redirect;
  require login before the style-image vision parse
- play: wire authResolveRef so login retries the action that hit 401;
  dismissing the modal no longer re-fires it
- server: wrap cookie setAll in try/catch for read-only contexts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 19:27:51 +08:00
yuanzonghao ccdb4780d6 fix(play): throw AbortError on cancelled prefetch to avoid false analytics
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 19:09:04 +08:00
yuanzonghao 0998f7c46a feat(play): add error observability analytics for mobile diagnostics
Track play_error and play_visibility_lost events via Umami to
distinguish mobile vs desktop failure modes. Each error event
captures orientation, connection type, visibility state, elapsed
time bucket, and error classification — all categorical, no free
text. Includes postJson "HTTP \d+" status parsing for the new
engineClient dual-path architecture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 18:57:38 +08:00
yuanzonghao 87a2f93edb feat(auth): add Supabase auth with Google, GitHub, and email OTP login
Introduce user registration/login gated behind optional NEXT_PUBLIC_SUPABASE_*
env vars (leave blank to disable — app behaves exactly as before). Adds
proxy.ts for automatic cookie session refresh, requireUser() API route
guards on all 7 compute-consuming routes, AuthModal (Google/GitHub OAuth +
6-digit email OTP), UserChip header component, and login_success analytics
event. Identity is fully decoupled from Session/engine — no type changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-13 17:33:55 +08:00
yuanzonghao e3ee3547e5 fix(play): gate scene transition on image decode
Keep the "transitioning" overlay visible until the <img> element's
bitmap is fully decoded, so the user never sees progressive paint
or a blank flash between scenes.

- Add onImageReady callback to PlayCanvas (<img onLoad> + decode())
- Delay setPhase("ready") until decode resolves (3s timeout fallback)
- Applied to all 4 scene entry paths: prebaked card, live /api/start,
  performSceneTransition, and recorded replay transition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-13 11:43:35 +08:00
baizhi958216 c4ffc16498 Merge pull request #64 from zonghaoyuan/refactor/settings-modal
feat: add client-side model configuration and server fallback
2026-06-12 22:09:43 +08:00
baizhi958216 6cd7d88326 feat(web): fallback to server API routes when no client-side model config is set
When a user has not configured their own model keys in localStorage,
engine calls now automatically route through /api/* server routes
instead of throwing "模型配置未设置". This lets Vercel deploys with
server-side environment variables work out of the box.

- Add lib/engineClient.ts as a unified client-side routing layer:
  checks localStorage for BYO config, falls back to POST /api/start,
  /api/scene, /api/vision, /api/classify-freeform, /api/insert-beat
- Update app/play/page.tsx to use engineClient instead of direct
  engine imports; remove buildEngineConfig()
- Update app/page.tsx style-image parsing to also fall back to
  /api/parse-style-image when no local model config exists

Signed-off-by: zhi <zhi@peropero.net>
2026-06-11 12:15:14 +08:00
baizhi958216 0f8e641c4c feat(web): merge SettingsModal and ModelSettingsModal with tab navigation
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-11 12:15:14 +08:00
baizhi958216 b63b694940 refactor(play): use client-side engine API instead of direct fetch
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-11 12:15:14 +08:00
baizhi958216 ab2f42bc42 feat(web): merge TTS settings into ModelSettingsModal, remove from SettingsModal
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-11 12:15:14 +08:00
baizhi958216 6b11a225cd feat(web): add model settings button, modal, and client-side style image parsing
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-11 12:15:14 +08:00
DESKTOP-I1T6TF3\Q 621f83c47b feat(web): embed beat audio into gallery and infiplot exports
Walk every speaking beat at export time, reuse current scene's beatAudioMap,
and synth the rest via BYO TTS or /api/beat-audio with concurrency 4. Show a
progress toast on the play page while collecting.

Gallery export keeps audio in a sidecar localStorage key so the first paint
is not blocked by JSON.parse-ing several MB of base64; the gallery lazy-loads
it after the first scene image, then plays per-beat audio with a mute toggle
persisted to localStorage. .infiplot share files embed audioByBeatId in the
doc itself (v2); on import the data URIs survive scene swaps and feed back
into the per-beat audio map so replayers hear the original voices for free.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 09:29:16 +08:00
Zonghao Yuan a61a91060d Merge pull request #62 from zonghaoyuan/feat/home-import-tooltip-infiplot
feat(web): clarify home import button tooltip as "载入infiplot剧情"
2026-06-10 00:18:06 +08:00
DESKTOP-I1T6TF3\Q 1a50ed9fc4 chore(home): swap Kyoto Animation and Shinkai style thumbnails
Replace the auto-generated kyoani / shinkai style thumbnails with hand-picked
reference frames. Source PNGs were center-cropped to square and re-encoded as
512x512 WEBP (~41KB each) to match the existing thumbnail format. Bumps the
shared cache-buster from v5 to v6 so existing browsers fetch the new files.
2026-06-09 16:38:55 +08:00
DESKTOP-I1T6TF3\Q b72bbd5501 feat(web): clarify home import button tooltip as "载入infiplot剧情"
The home-page file-import button accepts .infiplot story files. The
tooltip now spells out the file type so users distinguish it from
"开始剧情"/"载入预设" affordances on the same screen.
2026-06-09 16:31:34 +08:00
Zonghao Yuan d15d53ba65 Merge pull request #57 from zonghaoyuan/feat/tts-stepfun-provider
feat(tts): add StepFun preset-voice provider, route by URL + voice tag
2026-06-09 14:28:36 +08:00
yuanzonghao 1a6238f8b8 fix(tts): harden StepFun provider integration
- Validate voice.provider against known whitelist (xiaomi|stepfun) in
  beat-audio route to return a clear 400 instead of falling through
- Move single-char pronouns (他/她) to weak-signal fallback in
  detectGender to avoid false positives on compounds like 其他
- Update .env.example with StepFun configuration examples

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 14:24:27 +08:00
DESKTOP-I1T6TF3\Q 04f22249c9 fix(tts): make stepfun preset pick case-stable and per-character
- Hash the lowercased description (matching the case-insensitive scoring)
  so the same archetype text picks the same preset regardless of case.
- Thread the character name through provisionVoice -> stepfunProvision as
  the hash salt, so two characters that share archetype keywords spread
  across the top-N candidate presets instead of collapsing on one voice.

Xiaomi path is unaffected (voicedesign mints a unique clip per call).
2026-06-09 09:14:44 +08:00
baizhi958216 24b97fa3fb chore(share): remove stale gallery pack code
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-08 19:30:04 +08:00
baizhi958216 1d12417cb0 fix(share): remove infiplot file download event before enter gallery page
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-08 19:24:40 +08:00
DESKTOP-I1T6TF3\Q 19bbee16fe feat(tts): add StepFun preset-voice provider, route by URL + voice tag
Add StepFun step-tts-mini / step-tts-2 / stepaudio-2.5-tts as an alternate
TTS provider alongside Xiaomi MiMo. Auto-detected from TTS_BASE_URL host
(contains `stepfun.com` → StepFun; otherwise → MiMo), mirroring how the
image client infers Runware from `*.runware.ai`.

CharacterVoice becomes a discriminated union on `provider`:
- xiaomi: { referenceAudioBase64, mimeType } — unchanged
- stepfun: { voiceId, model, mimeType } — preset voice ID + chosen model

Provision dispatches on the current cfg's base URL; synthesis dispatches
on the voice's own `provider` tag so a session with mixed voices (e.g. a
provider switch mid-development) routes each beat through the correct
protocol. xiaomiSynthesize now guards against being called with a non-
xiaomi voice, surfacing the bug as a clear runtime error instead of a
TypeScript narrow violation at the access site.

StepFun has no voicedesign equivalent — only preset voices + voice
cloning from a reference audio upload. Cloning would require an extra
asset per character, so v1 maps the LLM's Chinese voiceDescription to one
of the 32 published preset IDs via gender + age + tone keyword scoring,
with a deterministic hash spread across the top-3 candidates so multiple
characters with similar descriptions don't collapse onto the identical
preset. lineDelivery is accepted but not yet propagated to StepFun's
voice_label.emotion / .style fields — left as a follow-up.

beat-audio route validation relaxed from `voice.referenceAudioBase64`
(xiaomi-shaped) to `voice.provider` (shape-agnostic), so stepfun voices
pass the gate; provider-specific shape errors still surface from the
synth function.

Observed latency on InfiPlot's dev loop: StepFun step-tts-mini median
~2.3s per beat with 0% timeouts across the test session, vs MiMo's
median ~8s with the long tail tripping the existing 15s synth budget
on roughly 2 of 3 beats. Pricing: step-tts-mini ¥0.9/万字符 (~¥0.14
per typical 50-beat session) vs MiMo TTS currently free under the
Token Plan creator incentive.

AGENTS.md provider matrix updated to describe both providers and the
discriminated-union dispatch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 17:15:02 +08:00
yuanzonghao 7c676fc43b fix(play): guard handleExportStory against duplicate clicks
Adds a ref-based mutex so concurrent /api/story-pack requests and
duplicate file downloads cannot be triggered by rapid clicking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 15:45:36 +08:00
yuanzonghao 75548ce005 Merge pull request #52 from zonghaoyuan/feat/story-share
feat(play): add encrypted story sharing with replay
2026-06-08 09:57:16 +08:00
yuanzonghao 39a7269494 fix(share): harden story share and relocate import button
- Add Content-Length pre-check to story-pack and story-unpack routes
  to reject oversized payloads before buffering the body
- Suppress internal error details in story-unpack catch (was leaking
  e.message to the client)
- Strengthen sceneIndex validation: require non-negative integer
- Guard against undefined storyState when replaying shared stories
- Fix prefetch regression: remove currentBeat?.id from useEffect deps
  that was re-triggering all change-scene prefetches on every beat
- Fix double detach: use else-if so the second replay detach guard
  doesn't fire redundantly after the first already detached
- Align client file-size limit by format (.json 12MB, .infiplot 13MB)
- Move "载入剧情" import button next to "开始" with hover tooltip

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 08:46:05 +08:00
yuanzonghao 867c52c24f fix(gallery): address review findings in zip download module
- Handle downloadImagesAsZip return value and surface errors to user
- Fix inferImageExtension garbage output for data URIs without semicolons
- Scale blob URL revocation delay for large zip files (>5MB → 60s)
- Cap uniqueZipPath dedup loop at 10k iterations with timestamp fallback
- Support relative URLs in inferImageExtension via base URL
- Handle svg+xml MIME subtype correctly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-07 22:32:23 +08:00
baizhi958216 0abd5f1525 feat(play): add encrypted story sharing 2026-06-07 17:13:27 +08:00
baizhi958216 7925e9c459 feat(gallery): download scene gallery as zip
Signed-off-by: baizhi958216 <1475289190@qq.com>
2026-06-07 15:45:46 +08:00
yuanzonghao df48e73d62 fix(play): sync playerName to active session on settings save
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-07 15:02:57 +08:00
yuanzonghao 4972243a93 fix: address PR Agent review findings across 6 files
Restrict PR Agent workflow to trusted collaborators on PR comments only,
fix UTF-8 byte counting in gallery-pack, correct portrait-to-landscape
fallback orientation, track inserted freeform beats in visitedBeatIds,
allow clearing stored TTS key, and guard empty-string fuzzy match in
style selector.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-07 14:40:37 +08:00
yuanzonghao 69ae1380cb fix(play): resolve hydration mismatch and fragile pace index
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-07 14:23:44 +08:00