Commit Graph

275 Commits

Author SHA1 Message Date
yuanzonghao a18b91c48c fix(play): story-card clicks no longer trigger vision
Symptom: on a choice beat, clicking the dialogue/narration card fired
the vision ("识图") flow instead of doing nothing. Picking an option with
fast clicks that landed on the card repeatedly kicked off the expensive
/api/vision → insert-beat/scene chain — janky and confusing.

Root cause: the story-card <div> had `pointer-events-none`, so clicks
passed through to the background <img> onClick (handleImageClick), which
on choice beats calls onBackgroundClick → vision.

Fix: the card now owns its clicks (`pointer-events-auto` + handleCardClick):
  - mid-typing   → completes the text (VN skip affordance, unchanged)
  - continue beat → advances, as before
  - choice beat  → no-op (no vision)
Clicking the actual scene art still triggers vision; choice buttons
already had pointer-events-auto and are unaffected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 09:17:30 +08:00
DESKTOP-I1T6TF3\Q b805b1d9c2 fix(play): scene image renders progressively from top → CF Worker proxy
Symptom: in Chrome on certain networks the scene <img> renders row-by-row
from top to bottom — "层层加载" — instead of appearing atomically.

Root cause (confirmed via DevTools):
  - Chrome opportunistically opens HTTP/3 (QUIC) to im.runware.ai.
  - QUIC streams to Runware sometimes error mid-transfer:
      net::ERR_QUIC_PROTOCOL_ERROR
    HTTP-level status stays 200 (response headers received), but bytes are
    truncated. The browser paints whatever PNG bytes it has so far → visible
    row-by-row decode.
  - The earlier preloadImage()+decode() trick can't fix this — neither
    HTTP-cache reuse nor sync decode helps when the bytes themselves were
    never fully delivered.

Two-tier fix:

1. Client: fetch → Blob → URL.createObjectURL() (app/play/page.tsx)
     - <img src> only ever points to a blob: URL whose bytes are 100%
       resident in the JS heap. No network-backed src = no possibility of
       progressive paint.
     - Module-level blobUrlCache keys by original URL so speculative
       prefetch + the eventual commit share one fetch.
     - Old blobs are URL.revokeObjectURL()'d on scene swap + unmount to
       release memory.

2. Network: optional Cloudflare Worker proxy (worker/)
     - Browser ↔ Worker is HTTP/2 over CF edge (extremely stable).
     - Worker ↔ Runware is a server-to-server fetch (no QUIC fragility,
       Cloudflare's backbone handles transit).
     - Worker buffers the full upstream response → client never sees a
       half-stream.
     - Bonus: CF edge cache (cacheEverything, 1y TTL) on Runware UUIDs;
       Access-Control-Allow-Origin: * so client fetch() can't hit CORS.
     - Hardened: only proxies im.runware.ai, only GET/HEAD/OPTIONS, all
       other hosts/methods → 403/405.

Wired via NEXT_PUBLIC_IMAGE_PROXY_URL (inlined at build). Empty → no proxy
→ direct fetch (which still uses the blob path, just exposed to QUIC).

──────────────────────────────────────────────────────────────────────
Deploy steps (one-time, do this AFTER pulling this commit):

  1. Install wrangler globally:
       npm i -g wrangler

  2. Log in to Cloudflare (opens browser for OAuth):
       wrangler login

  3. From the worker/ directory, deploy:
       cd worker
       wrangler deploy

     wrangler will print the deployed URL, e.g.
       https://infiplot-image-proxy.<your-cf-username>.workers.dev

  4. Paste that URL into .env.local for local dev:
       NEXT_PUBLIC_IMAGE_PROXY_URL=https://infiplot-image-proxy.<...>.workers.dev
     …and into Vercel project settings (Environment Variables) for prod.
     NEXT_PUBLIC_ vars are inlined at build time, so the URL bakes into
     the bundle on the next deploy/dev-server restart.

  5. Restart dev server (pnpm dev) so the new env baked in. Generate a
     scene; Network tab should show requests going to *.workers.dev
     instead of im.runware.ai, no ERR_QUIC_PROTOCOL_ERROR, image renders
     atomically.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 22:50:48 +08:00
DESKTOP-I1T6TF3\Q 347ab297d5 feat(web,engine): custom style — image upload, AI-extract prompt, painter ref
自定义画风入口里加上传按钮:客户端把图缩到 512px webp(base64),传到新
路由 /api/parse-style-image,vision LLM 解析成英文 style prompt 回填 textarea;
图本身随 sessionStorage → /api/start → Session.styleReferenceImage 透传,
painter.collectReferenceImages 把它置于 slot 0,整局每一幕都作为 reference
图锚定画风(brush / color / mood),比 priorScene 优先级更高。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 19:15:19 +08:00
Zonghao Yuan 1298e99339 Merge pull request #21 from zonghaoyuan/staging
docs: sync README 14-image 2-column gallery to main (#20)
2026-06-03 13:42:18 +08:00
Zonghao Yuan a6825f0292 Merge pull request #20 from zonghaoyuan/docs/readme-2col-screenshots
docs: replace README screenshots with 14-image 2-column gallery
2026-06-03 13:37:24 +08:00
Zonghao Yuan 95cfe24065 Merge pull request #19 from zonghaoyuan/staging
chore: promote staging to main (favicon + home revamp)
2026-06-03 11:27:16 +08:00
Zonghao Yuan a333b21759 feat(web): add favicon (IP monogram SVG) (#18)
站点此前没有 favicon,浏览器显示默认地球图标。新增 app/icon.svg:
站点同款 Cormorant Garamond 衬线的「IP」字母组合 —— I 直立 (clay #2D1810)、
P 斜体 (ember #D97A2E),米白圆角底,呼应 README 的 wordmark。字形以矢量轮廓
内嵌(从 Cormorant wght 600 / italic 400 实例化抽取),不依赖 webfont,各处渲染一致。
Next.js 16 见到 app/icon.svg 会自动注入 <link rel="icon">,无需改动 layout.tsx。

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 11:23:34 +08:00
DESKTOP-I1T6TF3\Q 298ecd4ec0 perf(engine): reorder Writer/Cinematographer prompts for prefix caching
Goal: lift prompt-cache hit rate from the ~75% baseline toward 95%+
on DeepSeek/MiMo-style 64-token chunked prefix caches. Both providers
match a stable byte-identical prefix from message[0]; once a single
byte changes everything after it misses, so the trick is to push every
session-stable bit to the front and concentrate per-call churn in a
short suffix.

Three coordinated changes:

1. Split storyState rendering into spine + dynamic.

   renderStoryStateSpine: logline / genreTags / protagonist / castNotes
   — Architect-set fields that StoryStatePatch literally cannot touch
     (the type only declares the 4 volatile ones; coerce and apply both
     cherry-pick), so spine bytes are guaranteed stable for the entire
     session. Goes in the STABLE PREFIX.

   renderStoryStateDynamic: synopsis / openThreads / relationships /
     nextHook — the Writer rewrites these every scene via storyStatePatch.
     Goes in the DYNAMIC SUFFIX.

   renderStoryState kept as a convenience wrapper that joins both, for
   anything that still wants the merged bible.

2. Rewrite buildWriterUserMessage with a stable/dynamic split.

   STABLE PREFIX (byte-identical or pure append across consecutive calls):
     - 世界观 / 画风 (session-immutable scalars)
     - story bible spine
     - 已登记角色  [sentinel: "(以下每行一个已登记角色,开场前为空。)"] + entries
     - 已使用的 sceneKey  [sentinel] + entries
     - 场景历史,已完结 [sentinel] + archivedHistory entries
        ↑ archivedHistory = history.slice(0, -1), NOT the full history
        — the live entry (history[-1]) keeps mutating mid-scene as the
          player walks new beats and speculative prefetches snapshot it
          at different moments, so it MUST stay out of the stable prefix
          or the byte-monotonic invariant breaks.

   DYNAMIC SUFFIX:
     - storyState dynamic patch
     - last-beat snippet (the exact emotional cliffhanger to continue from)
     - lastExit hint
     - format reminder tail

   The previous structure put the full storyState (including patched
   fields) at the very top of the user message, so the very first byte
   of the user message changed every scene — user-side cache hit was
   effectively 0% across the board.

3. Sentinel pattern for variable-length sections.

   Every list (characters / sceneKeys / archivedHistory) now emits a
   constant placeholder line after its header REGARDLESS of whether
   it has entries. With the old "if empty print '(暂无)' else print
   entries" pattern, adding the first item silently rewrites those
   placeholder bytes — the byte at offset N moves from a Chinese
   parenthesis to a dash, prefix cache torched. The sentinel line is
   the same bytes whether the list has 0 or N items; new items are
   pure appends after it.

4. Rewrite buildCinematographerUserMessage.

   New CINE_STABLE_HINT constant (~80 tokens of fixed guidance) glued
   right after the session-stable styleGuide line, so the stable prefix
   is long enough to cross at least one full 64-token chunk boundary
   beyond the system prompt. The per-scene inputs (sceneSummary,
   entryBeatActive, entryBeatSpeaker policy, prior-sceneKey continuity
   hint) all moved into the dynamic suffix below.

Verified (see [cache] / [debug-writer] logs from staging): hash of
500-byte slices of the user message is byte-identical across two
same-historyLen Writer calls through the entire stable prefix; only
the dynamic suffix slice differs. The remaining cache-hit gap under
MiMo is a server-side quirk (hit plateaus near 3072 tokens, occasionally
jumps to 4096); on DeepSeek the same prefix should hit fully.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 10:42:33 +08:00
DESKTOP-I1T6TF3\Q 37c911f510 chore(engine): log prompt-cache hit/miss per chat call
Add a `tag` option to chat() and have it print one `[cache] <tag>
hit=X miss=Y rate=Z%` line per call. Three Usage-shape variants are
probed in order so the same logger works across providers:

  - DeepSeek (v3+):  usage.prompt_cache_hit_tokens / *_miss_tokens
  - OpenAI / o-series: usage.prompt_tokens_details.cached_tokens
  - Anthropic:        usage.cache_read_input_tokens / *_creation_*

When none of them are present (MiMo / local Ollama / others) we still
print prompt + completion totals so the cost baseline is visible.

Tag every callsite so the log is greppable:
  architect / writer / character-designer / cinematographer / insert-beat

This is the prerequisite for the prefix-cache reordering work that
follows — without per-agent visibility there's no way to tell if a
prompt rearrangement actually moved the needle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 10:42:33 +08:00
Zonghao Yuan 334c9808c6 Merge pull request #22 from zonghaoyuan/chore/clean-play-ui
chore(play): remove session-id readout and decorative footer mark
2026-06-03 16:35:23 +08:00
yuanzonghao 3fa3da5378 chore(play): remove session-id readout and decorative footer mark
The session-id slice shown in the play header was an opaque timestamp
that reads as noise to players. The footer's "Ⅰ · Ⅰ" was a leftover
decorative mark after its sibling controls were moved above the canvas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 16:00:16 +08:00
DESKTOP-I1T6TF3\Q b5f73d8082 fix(play): scene image renders as 1px sliver while CDN bytes still arrive
When the Runware CDN download was slow (~10-20s over VPN / strict
networks, vs. the optimistic <2s the existing comment assumed), the
preload's 8s timeout fired and setImageUrl committed before the bytes
were actually decoded. The rendered <img> has w-auto h-auto and no
intrinsic aspect-ratio source — until the image loads the layout
collapses to roughly 1px tall, giving the "等了很久 → 一根线 → 突然
出图" jank.

Two compounding fixes:

  app/play/page.tsx       IMAGE_PRELOAD_TIMEOUT_MS  8000 → 20000.
                          Real CDN+decode usually finishes well before
                          this; pushing the ceiling out just stops the
                          window where we commit a half-loaded URL.

  components/PlayCanvas.tsx  Add width={1792} height={1024} HTML attrs
                          to the scene <img>. Doesn't affect rendered
                          size (still driven by w-auto h-auto and the
                          maxWidth/maxHeight in sizeStyle); the
                          browser uses them purely as an intrinsic
                          aspect-ratio source, so the placeholder box
                          reserves a 16:9-ish frame even mid-download.

Together: slow networks now mostly wait through preload; on the rare
genuine timeout the layout still holds shape instead of collapsing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 07:24:42 +08:00
DESKTOP-I1T6TF3\Q ea861b7c99 feat(web): style modal — custom prompt + per-preset override, inline edit
Two coordinated additions to the 绘画风格 modal so the user can shape
the styleGuide that ultimately feeds every painter/director agent,
without ever mutating the source-of-truth STYLE_MAP:

1. New "自定义" entry sits right under "自动" — opens an inline
   textarea where the user can write a free-form styleGuide (mix of
   Chinese / English, sent verbatim to the image model). Stored as
   in-memory state on HomePage (customStyleGuide), so refresh clears
   it — fits the "one-shot session" semantics of this UI.

2. Every preset card now exposes a small pencil on the right of its
   prompt area. Clicking it inlines a textarea pre-filled with the
   current effective prompt (override if any, else STYLE_MAP value).
   Saving writes to styleOverrides[name] — a separate in-memory
   record keyed by preset name. STYLE_MAP is never written to.

start() selects the styleGuide with this priority:
  customStyleGuide (when 自动→自定义)
  > styleOverrides[artStyle]
  > STYLE_MAP[artStyle]
  > STYLE_MAP[DEFAULT_STYLE]

UX polish in the same change:
- 标题永远只读 (only the prompt is editable)
- 只读 prompt 行去掉边框/底色,回归纯文字 + 右上铅笔
- 「自动」项无 prompt 可编辑,标题下直接放一行说明
- 编辑态 textarea 用 ember 边框作为"正在编辑"视觉反馈
- 「保存并选用」一并 onPick + close;「还原默认」清除该预设的 override
- 搜索框同时匹配标题/原名/prompt 内容
- 移除「自由输入」标签 (now visually redundant with the pencil affordance)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 07:24:42 +08:00
DESKTOP-I1T6TF3\Q 4d46bd0664 docs: replace README screenshots with 14-image 2-column gallery
Drop the old 9-image 3x3 grid (4/5/a4/c3/c5/c7/d2/f2/f5.webp) and bring
in 14 new stills as 1.webp..14.webp, laid out as 7 rows of 2 columns at
width=420. Source PNGs (1920x1080 for 1-8, 1200x680 for 9-14) are
resized to fit inside 1200x680 and saved as q=85 WebP — 70-150KB each.

All three README locales (zh/en/ja) share the same paths so a single
asset swap refreshes every edition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 06:53:46 +08:00
DESKTOP-I1T6TF3\Q 1bcea098d5 chore(web): swap 数据幽灵 and 极简杀机 positions in male DISPLAY_ORDER
m14 (极简杀机) is currently a 14.7KB placeholder while m18 (数据幽灵)
got a real curated cover this round — promote 数据幽灵 into the front
row and demote 极简杀机 back to its original neighborhood so the visible
首屏 only shows finished art.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 04:13:51 +08:00
DESKTOP-I1T6TF3\Q c90929ce4b feat(web): pin curated male cards to the front + row-first grid layout
Two coupled changes so the user's preferred male cards (复古未来梦,
社团存亡日, 黄昏归途, 极简杀机, 辐射新娘, 霓虹义体, 月光下的约定,
花魁的刀) actually appear in the visual front row:

1. Add a DISPLAY_ORDER indirection. STORIES, covers (m{i}.webp),
   prebaked first-acts (firstact/m{i}.json) and prompts.json are all
   keyed on the original array index — renaming them would touch
   dozens of static assets. DISPLAY_ORDER instead lets the homepage
   iterate cards in a curated order while still resolving each card's
   assets via its original index. Editing one line re-shuffles the
   gallery.

2. Switch the gallery wrapper from CSS multi-column (columns-N) to
   grid (grid-cols-N). columns fills column-first (top-of-col-1, then
   bottom-of-col-1, then top-of-col-2...) so the first eight entries
   of DISPLAY_ORDER ended up stacked down the leftmost column instead
   of across the top row. Grid fills row-first, which is what "visual
   front row" actually means. Cards are already fixed at aspect-ratio
   4/5 so row heights stay uniform — no masonry effect lost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 04:11:26 +08:00
DESKTOP-I1T6TF3\Q cd7619265c chore(web): swap in 6 curated male covers
Replace m7 / m8 / m9 / m11 / m13 / m18 with hand-picked images from
the offline reroll set. Source files at 686×1200 (m7/m8/m9/m11) were
attention-cropped to the existing 960×1200 4:5 target via sharp's
fit:'cover' + position:'attention' — same pipeline as the bed4dc5
cover batch, so the new images blend into the masonry without visible
aspect-ratio drift. m13 (复古未来梦) and m18 (数据幽灵) were already
at 960×1200 and copied straight through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 04:11:26 +08:00
DESKTOP-I1T6TF3\Q cbabc54273 chore(engine): log worldSetting and storyBible at session start
Two lines in startSession: the full worldSetting being fed to the
Architect, and the resulting logline/genreTags/synopsis it produced.
Cheap to keep — fires once per session — and makes it possible to tell
at a glance whether a "story unrelated to my input" report is a frontend
transport bug, a worldSetting layout problem, or the LLM ignoring the
seed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 03:51:58 +08:00
DESKTOP-I1T6TF3\Q d241300ed6 fix(web): fall back to current Typewriter phrase + frontload it in worldSetting
Two related fixes so the home start button actually reflects what the
user sees:

1. Lift the Typewriter's current phrase index up to HomePage so start()
   can read which example is on screen right now. When the textarea is
   empty, start() now substitutes that phrase as the user's story seed —
   "what you see is what you play", instead of the previous behavior
   where an empty input produced a generic worldSetting with no plot
   direction and the model invented something unrelated.

2. Restructure the worldSetting string so the user prompt (or the
   chosen Typewriter phrase) sits at the top, alone, wrapped in a
   strong directive ("必须以此为剧情主线,不要偏离"). Before, the seed
   was a single line sandwiched between the gender/style/pace boilerplate
   and the generic "edit with dramatic tension" tail, which the Architect
   tended to skim past when expanding the bible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 03:51:51 +08:00
DESKTOP-I1T6TF3\Q eb9b875454 fix(web): use existing STYLE_MAP key for home 「自动」 fallback
After bed4dc5 renamed style keys to include the (Image N参考) suffix,
the home start() still resolved 「自动」 against the legacy bare name
「京阿尼细腻日常」, leaving styleGuide undefined and tripping the
/api/start required-field check on the default click.

Fall back to "Galgame CG 梦幻光影" — a key that actually exists in
STYLE_MAP — so the default path resolves cleanly without changing the
behavior of explicitly selected styles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 03:37:49 +08:00
DESKTOP-I1T6TF3\Q bed4dc5a8f feat(web): gender-differentiated 4:5 covers + per-card styleGuide prebake
- Regenerate 60 covers (30 male + 30 female) via FLUX with story-specific
  prompts, replacing the prior gender-shared set
- Crop covers to 4:5 (960×1200) via sharp attention cover; matches new
  homepage card aspectRatio
- Persist all 60 prompts to public/home/prompts.json so the prebake step
  can reuse the cover's exact visual anchor (per-card styleGuide) and the
  first-act scene visually carries over from the poster the player clicked
- Restore /play?card= prebaked instant-play path on homepage card click
- Add OpenAI-compatible image route in ai-client for non-Runware endpoints
- Hide Next.js dev indicators globally; tweak F-key fullscreen label

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 02:26:35 +08:00
DESKTOP-I1T6TF3\Q 820a5f7e87 feat(web): refactor home preset cards to 16:9 poster style with titles and tags below cover 2026-06-03 02:25:02 +08:00
Zonghao Yuan 8ca818e4d7 Merge pull request #16 from zonghaoyuan/staging
Merge staging into main
2026-06-03 01:19:14 +08:00
Zonghao Yuan 6ddbe7d377 feat: add privacy-friendly Umami page-view analytics (#15)
Cookieless, env-gated page-view tracking via Umami. The <Analytics />
component injects the script only when NEXT_PUBLIC_UMAMI_SRC and
NEXT_PUBLIC_UMAMI_WEBSITE_ID are both set, so local dev and forks send
nothing to our instance. Adds .env.example docs (section 6) and a
homepage footer privacy disclosure. No Cookie consent banner needed.
2026-06-03 01:14:55 +08:00
Zonghao Yuan 639201cd38 docs: replace How it works mermaid with localized SVG diagrams (#14)
The auto-laid-out Mermaid flowchart looked rough. Replace it with a
hand-built Anthropic-style diagram: color-coded role cards, a dashed
scene-generation group, and the speculative pre-generation loop.

Each SVG bakes in a dark background so it renders consistently whether
the viewer's GitHub theme is light or dark (theme is per-viewer, not
per-repo). Ship one localized SVG per README (zh/en/ja) to preserve the
existing per-language diagrams, and strip claude.ai artifact residue
(onclick / var() / context-stroke).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 01:08:08 +08:00
Zonghao Yuan dc5ecd60f6 refactor: flatten monorepo to single web package (#12)
Flatten the pnpm monorepo (apps/web + packages/*) into a single web package at the repo root.

- Move app/lib/components/scripts/public to root; drop apps/web and packages/* wrappers
- Rewrite tsconfig paths (@infiplot/*) to ./lib/*; turbopack.root = __dirname
- Update Vercel (no root-directory) and Cloudflare (pnpm build:cf at root) deploy paths
- Regenerate pnpm-lock.yaml to drop stale workspace importers
- Bump engines.node to >=22 to match wrangler

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 00:55:45 +08:00
yuanzonghao 9543c3dba1 docs: note Cloudflare deploy needs Workers Paid Plan
The scene pipeline needs more CPU time than the Workers Free 10ms cap
allows, so Cloudflare deploys require Workers Paid. The old "pick
whichever you prefer" implied false cost parity with Vercel (free
Hobby works), so recommend the one-click Vercel deploy for personal
use. Applied to all three READMEs (zh/en/ja).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 00:39:17 +08:00
Zonghao Yuan ee2c29ae63 Merge pull request #11 from zonghaoyuan/merge-staging-to-main
Merge staging into main (#10 的冲突已解决)
2026-06-03 00:07:43 +08:00
yuanzonghao 4725b2cf72 Merge staging into main (#10)
将 staging 全部进展合入 main:Cloudflare 部署、README 重构(中文默认 +
hero header + 版块重排 + 日文截图 + 切换器修复)、LICENSE 修复(恢复 AGPL-3.0
识别)、engine 点击标注迁移至浏览器 Canvas。冲突全部取 staging 版,移除已
废弃的 README.zh-CN.md 与 annotate.ts;合并后 main 树与 staging 完全一致。
2026-06-03 00:00:34 +08:00
yuanzonghao cd9e9012bf docs: point 简体中文 switcher to repo homepage
The relative `README.md` link resolved to GitHub's blob file view instead
of the repo landing page. Use the absolute repo URL so the switcher returns
to the homepage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 22:29:35 +08:00
yuanzonghao 6038216751 docs: reorder README sections, simplify Live Demo badge, add JP screenshots
- Move "How it works" above "Team & Vision" so the technical deep-dive
  follows the screenshots while reader interest is highest
- Simplify the Live Demo badge to a single segment (drop the domain; the
  badge still links to infiplot.com)
- Add the missing Screenshots section to the Japanese README (i18n gap:
  it was added to zh/en but never backported to ja)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 22:29:35 +08:00
Zonghao Yuan d263437756 Merge pull request #9 from zonghaoyuan/feat/cloudflare-migration
feat: add Cloudflare Workers deployment alongside Vercel
2026-06-02 22:14:52 +08:00
yuanzonghao 203e63edc2 fix: server-side payload cap + cleaner image abort
Addresses Copilot review on PR #9:

- /api/vision: add MAX_ANNOTATED_BYTES (3 MB) cap on annotatedImageBase64,
  plus an explicit type/non-empty check. Browser annotator resizes to 768
  wide (typically 200-800 KB base64), so 3 MB rejects abusive direct-API
  payloads that would otherwise inflate upstream vision LLM costs.
- annotateClient: replace `img.src = ""` on timeout with removeAttribute
  to avoid the legacy browser behavior of treating empty src as a
  navigation to the current document URL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 22:13:40 +08:00
yuanzonghao 72331bb865 feat: add Cloudflare Workers deployment alongside Vercel
InfiPlot now deploys to either Vercel or Cloudflare Workers — both
targets are first-class. The project is fully stateless (sessions live
on the client), so the Cloudflare side needs only Workers + Workers
Assets and zero D1/KV/R2.

- apps/web/wrangler.jsonc — nodejs_compat, Assets binding, 60s CPU
  limit (Workers Paid required; matches vercel.json maxDuration). I/O
  wait does not count against this budget — fits the LLM-bound
  workload that's most of the runtime.
- apps/web/open-next.config.ts — minimal defineCloudflareConfig (no
  cache needed since the engine is stateless).
- apps/web/package.json — added build:cf / preview:cf / deploy:cf via
  @opennextjs/cloudflare + wrangler (both devDeps); sharp moved from
  dependencies to devDependencies (only used by the manual
  optimize-home-images.mjs / localize-firstact-images.mjs scripts now).
- .gitignore — .open-next, .wrangler, .dev.vars.
- READMEs (3 langs) — Deploy to Cloudflare button next to Vercel,
  plus a Cloudflare section in the env-var setup (wrangler secret put
  + Cloudflare Access for staging access control).

Verified: pnpm typecheck + pnpm build (Vercel path) + pnpm build:cf
(OpenNext bundle: worker 4 KB, server 24 MB, assets 32 MB / 186
files — all within Workers limits) + pnpm preview:cf with the full
play loop (start → scene → background click → CORS-clean Canvas
annotation via Runware CDN → vision LLM → insert-beat) all green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 21:47:03 +08:00
yuanzonghao 346d5359d4 refactor(engine): move click annotation from sharp to browser Canvas
The vision pipeline used sharp to draw a click marker on the scene image
server-side (engine/src/annotate.ts) and to render the MOCK_IMAGE
placeholder PNG (engine/src/mockImage.ts). Both moved off the runtime:

- annotateClick → apps/web/lib/annotateClient.ts (Canvas 2D in the
  browser; toDataURL → raw PNG base64 forwarded to /api/vision). Saves
  a server-side image re-fetch per click and frees the engine from
  sharp's native binding (which doesn't run on Cloudflare Workers).
- mockImageDataUri → self-describing SVG data URI (no rendering needed).

VisionRequest contract changes: prevImageUrl + click → annotatedImageBase64.
Server forwards the bytes straight to the vision LLM as image_url.

sharp is removed from packages/engine entirely and from next.config.ts's
serverExternalPackages. apps/web/package.json + lockfile cleanup ships
in the follow-up Cloudflare deployment commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 21:46:45 +08:00
yuanzonghao dd8b60c06b docs: move one-click deploy section directly below Live Demo
Surfaces the deploy entry point higher on the page so it isn't buried
near the bottom. The section keeps its inline link to the Configuration
guide, so the deploy flow is unaffected by the reorder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 20:55:23 +08:00
yuanzonghao 236f5494c1 docs: add hero header to READMEs and make Chinese the default
- Add centered hero block: SVG wordmark banner, short tagline, and
  project-stat badges (stars/watchers/forks/issues + Live Demo, License,
  LINUX DO forum backlink)
- Swap default README to Chinese (targeting CN developers); English
  moves to README.en.md, Japanese stays README.ja.md
- Add SVG wordmark banner at docs/banner.svg
- Cross-link language switchers and fix per-language deploy envLink anchors

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 20:55:23 +08:00
yuanzonghao ee3a614d26 style(web): tidy the homepage QQ group caption
Drops the fa-qq penguin icon and the "扫码加入,或搜索群号" call-to-action
in favor of a plain "QQ群号:575404333" label — the QR right above already
implies scanning, and the column header names the group.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 19:32:37 +08:00
yuanzonghao abb6be5cdd feat(web): add QQ beta-community group to homepage + READMEs
Fills the long-empty "内 测 用 户 群" placeholder (was "群二维码 /
邀请链接(待补充)") on the homepage contact grid with the real QQ
group QR (group ID 575404333) plus a scan-or-search line.

Mirrors it across all three READMEs as a scan-to-join block right
after the contact line, rendered from apps/web/public/qq-group.webp
(760×760 QR-only crop with a white quiet zone, ~45KB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 19:26:54 +08:00
DESKTOP-I1T6TF3\Q 3e132ce28b docs: drop the click-to-enlarge caption from README screenshots
GitHub markdown can't host an in-page lightbox or prev/next carousel —
all <script> is stripped server-side, so a clickable thumbnail can only
ever open the raw image in a new page. The hint line was misleading
about that interaction, so just remove it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 17:31:32 +08:00
DESKTOP-I1T6TF3\Q bce686a7eb docs: make README screenshots clickable to view full size
Wraps each <img> in an <a href="..."> linking to the same path so GitHub
opens the full-resolution image on click instead of just showing the
inline thumbnail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 17:24:44 +08:00
DESKTOP-I1T6TF3\Q d93c16d836 feat(web): 红果-style homepage + instant-play prebaked first acts
Rewrites all 64 homepage cards (32 男性向 + 32 女性向) as short-drama hook
stories (战神归来 / 重生分手前夜 / 系统选妃 / 穿成乙游男配 / 末世异能 / 民国
谍战 / 修真渡劫 …) and regenerates each cover via FLUX in its assigned art
style (12 styles spread across 64 cards) at 832×1024 ≈4:5.

Click-to-play path: cards now jump straight to /play?card=<name> and hydrate
Session from /home/firstact/<name>.json — the engine pipeline (Architect +
Writer + CharacterDesigner + Painter) has been pre-run for 44/64 cards. The
remaining 20 (m14/m29/f14..f31) are pending an LLM credit top-up; their
clicks fall through to live /api/start for now.

Runware-hosted first-scene images are downloaded into /home/firstscene/
and the JSONs are rewritten to point at the local webp, so click → first
image is bounded by local-disk decode (~100ms) instead of CDN round-trip.

Scripts:
- scripts/generate-home-images.mjs  — rewrites all 64 cover prompts, per-card
  styles baked into prompts, 832×1024 dims to match StoryCard aspect
- scripts/prebake-firstacts.mjs     — POST /api/start × 64 with concurrency
  4, saves StartResponse to public/home/firstact/<name>.json
- scripts/localize-firstact-images.mjs — downloads each prebaked imageUrl
  to public/home/firstscene/<name>.webp (q80, ≤1600px) and rewrites JSON

README: adds Screenshots section (3×3 gallery) to README.md / README.zh-CN.md,
9 in-game shots compressed to docs/screenshots/*.webp (7.5MB → 680KB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 17:20:34 +08:00
DESKTOP-I1T6TF3\Q 9ae91dd3ed feat(play): hug-canvas action buttons, unified mute, enlarged back-link
UI layout (PlayCanvas + play/page.tsx):
- "F · 全 · 屏" button (renamed from 演 · 示 to match what users
  actually mean by F) floats above the canvas, right-aligned, via a
  new `aboveCanvas` ReactNode slot that lives on the relative
  inline-block image wrapper at `bottom-full right-0`. It hugs the
  actual image right edge regardless of aspect ratio.
- "有 · 声 / 静 · 音" button mirrors that on the left via a new
  `aboveCanvasLeft` slot.
- Both slots also render inside the loading placeholder so the two
  controls appear from frame one, before the scene image arrives.
- InfiPlot back-link grows from 15px to 22/26px (mobile/desktop) with
  a slightly larger arrow, matching the brand'\''s presence on the
  homepage hero.
- Canvas-bottom metadata row (image dims on left, tutorial hint on
  right) dropped. The "—" placeholder and "···" loading state looked
  like stray punctuation; users found them noisy.
- Footer collapses to a single centered "Ⅰ · Ⅰ" mark.

Audio gating logic (play/page.tsx):
- Collapse the two-flag audio gate into one source of truth. The
  homepage "语音配音" choice no longer lives in a separate
  `audioEnabledRef` flag that gates `fetchBeatAudio` independently
  of the in-page mute state. Instead the `muted` useState lazy
  initializer reads `sessionStorage["infiplot:custom"].audioEnabled`
  and projects it inversely (audioEnabled=false → muted=true) so
  the 静音/有声 button correctly reflects the homepage selection
  from the first frame. The in-page toggle remains the source of
  truth from then on (persisted to localStorage:infiplot:muted).
- This fixes a visible disconnect where picking "关闭" on the
  homepage left the play page showing 有声 because the in-page
  state had no link to the homepage choice.
- The sessionStorage read uses the renamed key "infiplot:custom"
  (the infiplot rename PR changed it from yume:custom on the home
  side but the play side hadn'\''t been updated to match).

No new TTS quota is ever burned while muted: fetchBeatAudio'\''s
mutedRef.current early-return is the only path to /api/beat-audio
and is checked before the fetch fires; mute transitions also abort
in-flight requests.
2026-06-02 15:42:49 +08:00
Zonghao Yuan cffe4da4ca docs: streamline 3 READMEs and fix EN language switcher (#6)
Slim overview across EN/zh/JA, drop badges/blockquote/contributing, trim LICENSE header; fix the English switcher to point at the repo homepage instead of the GitHub site root.
2026-06-02 15:33:08 +08:00
DESKTOP-I1T6TF3\Q 6da87df73a feat(web): home story-card polish + play page back-link rebrand
Home (apps/web/app/page.tsx):
- StoryCard locked to uniform aspectRatio "4 / 5". The previous
  "placeholder 4/5 → naturalRatio after onLoad" flow coupled card
  height to lazy-load order: cards still below the fold sat at the
  placeholder ratio while above-the-fold cards snapped to their
  image's actual ratio (1.6 landscape vs 0.75 portrait vs 1.23
  squarish), so the gallery looked inconsistent until a hard refresh
  re-decoded everything from cache synchronously. Fixed ratio +
  object-cover removes the coupling.
- StoryCard hover overlay collapsed from two sibling layers
  (backdrop-blur + mask-image + dark gradient sibling) into one
  element with a pure rgba(0,0,0,…) linear-gradient and an opacity
  transition. Chromium does not animate backdrop-filter cleanly when
  combined with mask-image on an empty element — the first hover
  frame shows a full rectangular blur before the mask kicks in, then
  snaps to the feathered shape ("矩形磨砂 → 渐变磨砂"). One layer,
  one transitioning property, no compositing race.

Play (apps/web/app/play/page.tsx):
- Header back-link "云梦" → "InfiPlot" using the same serif + italic
  ember "Plot" treatment as the homepage wordmark. Resolved against
  the parallel plain-text rebrand already on infiplot/staging by
  keeping the styled version for brand consistency.
2026-06-02 14:42:26 +08:00
Zonghao Yuan 588b668d14 Merge staging into main (#3)
* feat(engine): Architect agent + cross-scene StoryState coherence

Add a dedicated Architect LLM call at session start that expands the terse
world/style prompt into a persistent story bible (logline, genre, second-
person protagonist, cast, engineered opening hook). The bible seeds a
StoryState the Writer reads and patches every scene, carried + merged
across cuts (applyStoryStatePatch) so the story keeps a spine from beat
one instead of jumping between scenes.

- prompts: inject web-novel / short-drama / galgame craft into Writer +
  Architect; Writer emits storyStatePatch to update the running bible
- director: parallelize voice + non-entry portraits with the Painter
  (only entry-beat portraits block paint) to offset Architect latency
- architect: chat/parse guarded so a malformed response never aborts start
- types: StoryState / StoryStatePatch; required on Start/SceneResponse

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

* docs: add AGPL-3.0 license, README i18n, and TTS accuracy fix (#2)

* docs: add AGPL-3.0 license, README i18n, and TTS accuracy fix

- LICENSE: add GNU AGPL v3 with InfiPlot copyright notice
- README.md: rewrite for open-source project, fix TTS description
  (TTS uses MiMo's own protocol, not OpenAI-compatible)
- README.zh-CN.md: add Simplified Chinese translation
- README.ja.md: add Japanese translation
- package.json: change license from UNLICENSED to AGPL-3.0-only

* fix: address Copilot review — .env.example TTS comment, zh-CN formatting

- .env.example: clarify TTS uses MiMo's own protocol, not OpenAI-compatible
- README.md: 'land paper after paper' → 'publish paper after paper'
- README.zh-CN.md: add spaces around '5 月', fix code formatting
  for model names (deepseek-v4-flash)

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 13:41:37 +08:00
Zonghao Yuan 9a3511f220 docs: add AGPL-3.0 license, README i18n, and TTS accuracy fix (#2)
* docs: add AGPL-3.0 license, README i18n, and TTS accuracy fix

- LICENSE: add GNU AGPL v3 with InfiPlot copyright notice
- README.md: rewrite for open-source project, fix TTS description
  (TTS uses MiMo's own protocol, not OpenAI-compatible)
- README.zh-CN.md: add Simplified Chinese translation
- README.ja.md: add Japanese translation
- package.json: change license from UNLICENSED to AGPL-3.0-only

* fix: address Copilot review — .env.example TTS comment, zh-CN formatting

- .env.example: clarify TTS uses MiMo's own protocol, not OpenAI-compatible
- README.md: 'land paper after paper' → 'publish paper after paper'
- README.zh-CN.md: add spaces around '5 月', fix code formatting
  for model names (deepseek-v4-flash)
2026-06-02 13:39:54 +08:00
Zonghao Yuan 70d0927a3e Merge pull request #1 from zonghaoyuan/feature/story-harness-opt
feat(engine): Architect agent + cross-scene StoryState coherence
2026-06-02 13:24:46 +08:00
yuanzonghao 15ce03a912 feat(engine): Architect agent + cross-scene StoryState coherence
Add a dedicated Architect LLM call at session start that expands the terse
world/style prompt into a persistent story bible (logline, genre, second-
person protagonist, cast, engineered opening hook). The bible seeds a
StoryState the Writer reads and patches every scene, carried + merged
across cuts (applyStoryStatePatch) so the story keeps a spine from beat
one instead of jumping between scenes.

- prompts: inject web-novel / short-drama / galgame craft into Writer +
  Architect; Writer emits storyStatePatch to update the running bible
- director: parallelize voice + non-entry portraits with the Painter
  (only entry-beat portraits block paint) to offset Architect latency
- architect: chat/parse guarded so a malformed response never aborts start
- types: StoryState / StoryStatePatch; required on Start/SceneResponse

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 11:44:55 +08:00
Zonghao Yuan 16707cc255 Merge pull request #10 from zonghaoyuan/rename/infiplot
chore: complete @yume → @infiplot rename (post-PR#9)
2026-06-02 09:30:23 +08:00