feat(novel-game): add image/TTS/save/mobile support, remove bl text chat

- Asset mode: video / image (`bl image generate`) / mixed — user chooses
- Optional TTS narration via `bl speech synthesize`
- Save system: auto-save + 3 manual slots (localStorage)
- Mobile-first: portrait layout, touch events, safe-area, 44px targets
- Video lazy loading: preload=none, memory release on scene exit
- Parallel asset generation: --async + bl video download batch mode
- Ken Burns effect for image-mode cutscenes
- Remove bl text chat — agent itself handles story generation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lisheng.lisheng
2026-06-22 23:15:57 +08:00
parent 6924a6f5bf
commit 313f6852ae
3 changed files with 247 additions and 221 deletions
+17 -13
View File
@@ -3,20 +3,22 @@
> [中文版 / Chinese →](README.zh.md)
Turn novels and stories into browser-based interactive fiction games
(React SPA) — with AI-generated video portraits, cutscenes,
(React SPA) — with AI-generated video/image assets, optional TTS narration,
programmatic audio, and branching storylines.
## Features
- **Branching narrative engine** — multiple endings, flags system, collectible archives
- **AI asset generation** — character portraits and cutscene videos via `bl video generate` / `bl video ref`
- **LLM-assisted writing** — plot extraction, scene text generation, video prompt authoring via `bl text chat`
- **AI asset generation** — character portraits and cutscenes via video (`bl video generate` / `bl video ref`) or image (`bl image generate`), mixable
- **Optional TTS narration** — scene narration and character monologues via `bl speech synthesize`
- **Programmatic audio** — BGM and SFX generated entirely with Web Audio API, zero external dependencies
- **Save system** — auto-save + 3 manual save slots (localStorage)
- **Mobile-first** — portrait layout, touch-optimized, safe-area support
- **Multiple UI themes** — pixel art / cyberpunk / ink-wash Chinese / minimal modern
## Prerequisites
- [Alibaba Cloud Model Studio CLI (`bl`)](https://bailian.aliyun.com/cli/install.md) — AI asset generation and LLM calls
- [Alibaba Cloud Model Studio CLI (`bl`)](https://bailian.aliyun.com/cli/install.md) — AI asset generation (video/image/speech)
- Node.js + npx — React project initialization
## Usage
@@ -28,30 +30,32 @@ In Claude Code (or any skill-aware agent), type:
```
The skill will:
1. Ask key design decisions (game type, UI style, length, AI assets)
1. Ask key design decisions (game type, UI style, length, asset mode)
2. Design the story structure with branching choices
3. Scaffold a React project with all components
4. Generate AI video assets (portraits + cutscenes) via `bl` CLI
4. Generate AI assets (video/image/speech) via `bl` CLI
5. Launch a dev server for browser testing
## Project structure (generated)
```
src/
├── components/ # UI components (TypeWriter, GameScene, ChoicePanel, ...)
├── data/ # Story graph, characters, archives
├── hooks/ # useGameState (reducer + hash routing), useAudio (Web Audio)
├── components/ # UI components (TypeWriter, GameScene, ChoicePanel, SaveLoadPanel, ...)
├── data/ # Story graph, characters, archives, asset index
├── hooks/ # useGameState (reducer + hash routing + save), useAudio (Web Audio)
└── styles/ # Theme CSS (cyberpunk, pixel, ink-wash, ...)
scripts/
└── generate-assets.sh # bl CLI asset generation script
public/assets/ # Downloaded AI-generated video files
└── generate-assets.sh # bl CLI asset generation script (supports parallel generation)
public/assets/ # Downloaded AI-generated asset files
```
## How it works
- **Story data** (`story.js`) defines a scene graph — each scene has text segments, choices with flag mutations, and optional cutscene/archive triggers
- **Game state** is managed via `useReducer` with hash-based routing for scene navigation
- **Character portraits** are 5-second looping videos (720P) displayed in a sidebar; fallback to text initials when videos aren't available
- **Cutscenes** are 5-second 1080P videos played as full-screen overlays at key story moments
- **Character portraits** support video (5-second looping 720P) or image (with CSS breathing animation), auto-detected
- **Cutscenes** support video (full-screen playback) or image (Ken Burns pan/zoom effect), auto-detected
- **Save system** — auto-save after each choice + 3 manual save slots in localStorage
- **BGM** uses fixed MIDI pitch arrays with multi-voice layering, convolution reverb, and ADSR envelopes
- **SFX** includes noise-pulse typing sounds, sweep + chord archive unlocks, and arpeggio scene transitions
- **TTS narration** (optional) — pre-generated MP3 files play automatically during scenes
+27 -23
View File
@@ -1,21 +1,23 @@
# novel-game
> [English version →](README.md)
> [English →](README.md)
将小说/故事改编为浏览器端的 H5 互动小说游戏(React SPA)——
含 AI 生成的动态角色立绘、过场动画、程序化音频和分支剧情引擎。
含 AI 生成视频/图片素材、可选 TTS 旁白、程序化音频和分支剧情引擎。
## 功能
- **分支剧情引擎** — 多结局、flags 系统、档案收集
- **AI 素材生成** — 通过 `bl video generate` / `bl video ref` 生成角色立绘和过场动画
- **LLM 辅助创作** — 通过 `bl text chat` 辅助剧情提取、场景文本生成、视频 prompt 编写
- **程序化音频** — Web Audio API 生成 BGM 和音效,零外部依赖
- **AI 素材生成** — 角色立绘和过场 CG,支持视频 (`bl video generate` / `bl video ref`) 或图片 (`bl image generate`),可混合使用
- **可选 TTS 旁白** — 通过 `bl speech synthesize` 生成场景旁白和角色独白
- **程序化音频** — BGM 和音效完全用 Web Audio API 生成,零外部依赖
- **存档系统** — 自动存档 + 3 个手动存档槽位(localStorage)
- **移动端适配** — 竖屏优先、触控优化、安全区适配
- **多种 UI 风格** — 像素风 / 赛博朋克 / 水墨中国风 / 简约现代
## 依赖
- [阿里云百炼 CLI (`bl`)](https://bailian.aliyun.com/cli/install.md) — AI 素材生成和 LLM 调用
- [阿里云百炼 CLI (`bl`)](https://bailian.aliyun.com/cli/install.md) — AI 素材生成(视频/图片/语音)
- Node.js + npx — React 项目初始化
## 使用
@@ -23,34 +25,36 @@
在 Claude Code(或其他支持 skill 的 agent)中输入:
```
/novel-game 把《三体》改编为互动小说游戏,赛博朋克风格,30分钟时长
/novel-game 把《三体》地球往事改编为互动小说,水墨风格,30分钟时长
```
skill 会自动:
1. 确认关键设计决策(游戏类型、UI 风格、时长、AI 素材)
Skill 将会:
1. 询问关键设计决策(游戏类型、UI 风格、时长、素材模式)
2. 设计分支剧情结构
3. 搭建 React 项目及全部组件
4. 通过 `bl` CLI 生成 AI 视频素材(立绘 + 过场动画)
3. 初始化 React 项目并实现所有组件
4. 通过 `bl` CLI 生成 AI 素材(视频/图片/语音)
5. 启动 dev server 在浏览器中测试
## 生成的项目结构
```
src/
├── components/ # UI 组件(TypeWriter、GameScene、ChoicePanel 等)
├── data/ # 场景图、角色数据、档案数据
├── hooks/ # useGameState(reducer + hash 路由)、useAudio(Web Audio)
└── styles/ # 主题样式(赛博朋克、像素风、水墨风等)
├── components/ # UI 组件(TypeWriter、GameScene、ChoicePanel、SaveLoadPanel...)
├── data/ # 场景图、角色、档案、素材索引
├── hooks/ # useGameState(reducer + hash 路由 + 存档)、useAudio(Web Audio)
└── styles/ # 主题 CSS(赛博朋克、像素风、水墨、简约...)
scripts/
└── generate-assets.sh # bl CLI 素材生成脚本
public/assets/ # 下载到本地的 AI 生成视频文件
└── generate-assets.sh # bl CLI 素材生成脚本(支持并行生成)
public/assets/ # 下载到本地的 AI 生成素材文件
```
## 工作原理
- **场景数据**(`story.js`)定义场景图——每个场景包含文本段落、带 flag 变更的选择项、可选的过场动画/档案触发
- **游戏状态** 通过 `useReducer` 管理,结合 hash 路由实现场景导航
- **角色立绘** 为 5 秒循环视频(720P),在侧边栏展示;视频不可用时回退为文字首字占位
- **过场动画** 为 5 秒 1080P 视频,在关键剧情节点以全屏覆盖层播放
- **BGM** 使用固定 MIDI 音高数组,多声部叠加、卷积混响、ADSR 包络
- **音效** 包含噪声脉冲打字音、扫频+和弦档案解锁音、琶音场景转场音
- **场景数据** (`story.js`) 定义场景图——每个场景含文本段落、带 flag 变更的分支选择、可选的过场/档案触发
- **游戏状态** 通过 `useReducer` 管理,hash 路由实现场景导航
- **角色立绘** 支持视频(5秒循环 720P)或图片(带呼吸动效),组件自动适配
- **过场** 支持视频(全屏播放)或图片(Ken Burns 缓动效果),自动适配
- **存档** 自动存档 + 3 个手动槽位,存入 localStorage
- **BGM** 使用固定 MIDI 音高数组 + 多声部叠加 + 卷积混响 + ADSR 包络
- **音效** 包括噪声脉冲打字音、扫频档案解锁音、琶音场景切换音
- **TTS 旁白**(可选)预生成的 MP3 文件在场景播放时自动播放
+203 -185
View File
@@ -1,10 +1,10 @@
---
name: novel-game
description: >-
将小说/故事改编为互动小说网页游戏(React SPA),含 AI 生成素材、程序化音频、分支剧情引擎。
使用 `bl` CLI 完成视频素材生成(`bl video generate` / `bl video ref`)和
LLM 辅助剧情设计(`bl text chat`)。当用户提到互动小说、文字冒险游戏、
小说改编游戏、H5 互动游戏、分支剧情、视觉小说时激活。
将小说/故事改编为互动小说网页游戏(React SPA),含 AI 生成素材(视频/图片可选)、
可选 TTS 旁白、程序化音频、分支剧情引擎、存档系统。
使用 `bl` CLI 完成素材生成(`bl video generate` / `bl image generate` / `bl speech synthesize`)。
当用户提到互动小说、文字冒险游戏、小说改编游戏、H5 互动游戏、分支剧情、视觉小说时激活。
---
你是一个专业的游戏策划兼全栈开发者,擅长将小说或故事改编为浏览器端的互动小说游戏。
@@ -15,7 +15,7 @@ description: >-
## 前置依赖
本技能依赖阿里云百炼 CLI(`bl`)进行 AI 素材生成和 LLM 辅助创作。使用前请检查:
本技能依赖阿里云百炼 CLI(`bl`)进行 AI 素材生成(视频/图片/语音)。使用前请检查:
```bash
bl --version
@@ -33,52 +33,28 @@ bl --version
2. **游戏类型** — 互动小说(选择影响剧情) / 文字冒险+解谜 / 文字RPG(含属性系统)
3. **UI 风格** — 像素风 / 赛博朋克 / 水墨中国风 / 简约现代
4. **叙事视角** — 第一人称(扮演主角) / 第三人称上帝视角(旁观者选择)/ 双主角切换
5. **AI 素材生成** — 是否需要 AI 生成角色立绘和过场动画?(使用 `bl video generate` / `bl video ref`)
6. **音频** — 无音频 / 仅 BGM / BGM + 音效(全部用 Web Audio API 程序化生成,零外部依赖)
5. **AI 素材生成** — 是否需要 AI 生成角色立绘和过场?如需要,选择素材模式:
- **视频模式** — 角色立绘为动态视频循环,过场为视频(效果最佳,生成慢,成本高)
- **图片模式** — 角色立绘为静态图片,过场为静态 CG + Ken Burns 动效(生成快,成本低)
- **混合模式**(推荐)— 角色立绘用图片省成本,关键过场用视频提升体验
6. **音频** — 选择音频方案:
- 无音频
- 仅 BGM(Web Audio API 程序化生成)
- BGM + 音效(全部 Web Audio API 程序化生成,零外部依赖)
- BGM + 音效 + TTS 旁白(BGM/音效程序化生成 + `bl speech synthesize` 生成旁白语音)
7. **游戏时长** — 15-20分钟(8-10场景)/ 30-45分钟(15-18场景)/ 1小时+(25+场景)
---
## 第二步:剧情设计
### 使用 `bl text chat` 辅助剧情提取与设计
如果用户提供了小说原文,使用 `bl text chat` 提取剧情结构:
```bash
# 从小说文件提取核心剧情线、关键分支点、角色列表
bl text chat \
--system "你是专业的互动小说策划师,擅长从原著中提取适合改编为互动游戏的剧情结构。" \
--message "以下是小说原文:$(cat novel.txt)" \
--message "请提取:1) 1-3条主线剧情 2) 3-5个适合做分支选择的关键节点 3) 主要角色列表(含外观描述)4) 3-5个可能的不同结局 5) 适合做过场动画的高潮场景" \
--max-tokens 8192
```
```bash
# 根据大纲生成具体的场景文本和选择
bl text chat \
--system "你是互动小说编剧,擅长写引人入胜的场景描写和有意义的分支选择。" \
--message "根据以下剧情大纲,为每个场景生成详细的叙事文本和 2-3 个分支选择(含对应 flag 设置):$(cat outline.md)" \
--max-tokens 8192
```
```bash
# 生成 AI 视频素材的 prompt
bl text chat \
--system "你是 AI 视频生成的 prompt 专家,擅长写出能产生高质量视频的描述。" \
--message "为以下互动小说场景生成视频 prompt(中文,每个 50-100 字,描述画面、动作、氛围):$(cat scenes.md)" \
--max-tokens 4096
```
### 剧情结构产出物
根据原著/素材提取:
根据原著/素材/用户描述,直接设计以下内容:
1. **核心剧情线** — 识别 1-3 条主线(可交织),每条线梳理关键场景
2. **关键分支点** — 选出 3-5 个影响结局的重大选择
3. **结局设计** — 设计 3-5 个不同结局,每个由 flags 组合决定
4. **角色列表** — 列出需要立绘的主要角色(6-8个)
5. **过场动画** — 列出需要生成视频的高潮场景(5-8个)
4. **角色列表** — 列出需要立绘的主要角色(6-8个),含外观描述
5. **过场场景** — 列出需要生成素材的高潮场景(5-8个),含画面描述
6. **收集物/档案** — 设计通过选择解锁的背景知识条目
---
@@ -92,26 +68,27 @@ src/
├── App.jsx # 主应用,游戏状态路由
├── index.js
├── components/
│ ├── TitleScreen.jsx # 开始画面(主题动画+标题)
│ ├── GameScene.jsx # 核心场景渲染(文本+选择+立绘)
│ ├── TitleScreen.jsx # 开始画面(主题动画+标题+继续游戏+存档管理)
│ ├── GameScene.jsx # 核心场景渲染(文本+选择+立绘+旁白)
│ ├── TypeWriter.jsx # 打字机逐字显示效果
│ ├── ChoicePanel.jsx # 选择面板(hover动效+延迟入场)
│ ├── CharacterPortrait.jsx # 角色立绘(视频/图片/首字占位)
│ ├── CutScene.jsx # 过场动画播放
│ ├── CharacterPortrait.jsx # 角色立绘(自动适配视频/图片)
│ ├── CutScene.jsx # 过场播放(视频/图片 Ken Burns)
│ ├── EndingScreen.jsx # 结局界面
│ ├── ArchivePanel.jsx # 档案/收集物面板
│ ├── ArchiveNotification.jsx # 档案解锁通知
│ ├── SaveLoadPanel.jsx # 存档/读档面板
│ └── ProgressBar.jsx # 章节进度条
├── data/
│ ├── story.js # 场景图(核心数据)
│ ├── characters.js # 角色定义
│ ├── archives.js # 档案数据
│ └── generated-assets.json # AI 生成素材的本地路径
│ └── generated-assets.json # AI 生成素材的本地路径+类型
├── hooks/
│ ├── useGameState.js # useReducer 游戏状态管理
│ ├── useGameState.js # useReducer 游戏状态管理 + 存档
│ └── useAudio.js # Web Audio API 音频系统
├── styles/
│ ├── pixel.css # 主题样式(根据用户选择调整)
│ ├── theme.css # 主题样式(根据用户选择调整)
│ ├── animations.css # 动画定义
│ └── crt.css # CRT 扫描线效果(像素风专用)
└── [特殊场景组件] # 按需:Canvas 动态背景等
@@ -119,8 +96,10 @@ scripts/
└── generate-assets.sh # bl CLI 素材生成脚本
public/
└── assets/ # 下载到本地的素材文件
├── portraits/
└── cutscenes/
├── portraits/ # 角色立绘(.mp4 或 .png)
├── cutscenes/ # 过场素材(.mp4 或 .png)
├── backgrounds/ # 场景背景图(.png)
└── narrations/ # TTS 旁白音频(.mp3)
```
---
@@ -138,18 +117,19 @@ export const scenes = {
year: "1967", // 显示年份
character: "character_key", // 当前场景角色立绘
bgm: "bgm_name", // 背景音乐
cutscene: "cutscene_key", // 过场动画(可选)
isEnding: false, // 是否为结局场景
endingType: "ending_a", // 结局类型标识
texts: [ // 逐段显示的文本
cutscene: "cutscene_key", // 过场素材 key(可选)
narration: "narration_key", // TTS 旁白 key(可选)
isEnding: false,
endingType: "ending_a",
texts: [
"第一段文字...",
"第二段文字..."
],
choices: [ // 最后一段文字后出现的选择
choices: [
{
text: "选择A的文字",
next: "next_scene_id", // 跳转目标(null = 根据 flags 进入结局)
setFlags: { flag_name: true }, // 设置标记
setFlags: { flag_name: true },
archive: "archive_key" // 解锁档案(可选)
}
]
@@ -161,20 +141,41 @@ export function getEnding(flags) {
}
```
### generated-assets.json 数据结构
```json
{
"portraits": {
"ye_wenjie": { "path": "/assets/portraits/ye_wenjie.mp4", "type": "video" },
"luo_ji": { "path": "/assets/portraits/luo_ji.png", "type": "image" }
},
"cutscenes": {
"red_coast": { "path": "/assets/cutscenes/red_coast.mp4", "type": "video" },
"countdown": { "path": "/assets/cutscenes/countdown.png", "type": "image" }
},
"backgrounds": {
"campus_1967": { "path": "/assets/backgrounds/campus_1967.png", "type": "image" }
},
"narrations": {
"scene_opening": { "path": "/assets/narrations/scene_opening.mp3" }
}
}
```
### useGameState 状态结构
```js
{
phase: 'title' | 'playing' | 'cutscene' | 'ending',
currentScene: string, // 当前场景 ID
flags: {}, // 玩家选择累积的标记
history: string[], // 已访问场景
archives: string[], // 已解锁档案
textIndex: number, // 当前场景第几段文本
typingDone: boolean, // 打字机是否完成当前段
currentScene: string,
flags: {},
history: string[],
archives: string[],
textIndex: number,
typingDone: boolean,
showCutscene: boolean,
showArchive: boolean,
newArchive: null | string, // 新解锁档案通知
newArchive: null | string,
bgm: string | null,
}
```
@@ -185,7 +186,7 @@ export function getEnding(flags) {
### 打字机效果(TypeWriter)
- 用 setInterval 逐字显示,speed 约 40-50ms
- 点击可跳过(立即显示全文)
- 点击/触摸可跳过(立即显示全文)
- 每个字符触发打字音效回调
- 显示完毕调用 onDone 回调
@@ -193,7 +194,7 @@ export function getEnding(flags) {
- 在最后一段文字打字完成后淡入
- 每个选项延迟入场动画(nth-child animation-delay)
- hover 时边框变色 + 微位移 + 阴影扩大
- 点击触发音效 → 设置 flags → 跳转下一场景
- 点击触发音效 → 设置 flags → 自动存档 → 跳转下一场景
### Hash 路由
- URL hash 同步当前场景:`#scene_id`
@@ -201,145 +202,158 @@ export function getEnding(flags) {
- 监听 hashchange 支持浏览器前进/后退
- 回到标题时清除 hash
### 存档系统(localStorage)
- **自动存档**:每次做出选择后自动保存当前状态到 `localStorage.setItem('novel_autosave', JSON.stringify(state))`
- **手动存档**:支持 3 个存档槽位(`novel_save_1` / `novel_save_2` / `novel_save_3`),每个槽位保存完整 state + 存档时间 + 当前场景标题
- **TitleScreen 入口**:
- 「新游戏」— 清空状态从头开始
- 「继续游戏」— 仅当自动存档存在时显示,加载自动存档
- 「读取存档」— 打开 SaveLoadPanel,显示 3 个手动槽位
- **SaveLoadPanel**:游戏内可随时打开(菜单按钮/快捷键),支持存档和读档
- 存档数据结构:`{ state, savedAt, sceneTitle, playTime }`
### 角色立绘(CharacterPortrait)— 自动适配视频/图片
- 从 `generated-assets.json` 读取素材信息,根据 `type` 字段渲染:
- `type: "video"` → `<video src={path} autoPlay loop muted playsInline />`
- `type: "image"` → `<img src={path} />` + 可选呼吸动效(CSS `animation: breathe 3s ease-in-out infinite`)
- 无素材时显示角色名首字母占位符
### 过场播放(CutScene)— 自动适配视频/图片
- `type: "video"` → 全屏 `<video>` 播放,结束后自动关闭
- `type: "image"` → 全屏 `<img>` + Ken Burns 动效(CSS `animation: kenburns 5s ease-in-out`),5 秒后自动关闭
- Ken Burns 效果:从 `scale(1.1) translate(-2%, -2%)` 过渡到 `scale(1.0) translate(0, 0)`,模拟镜头缓慢推拉
### 素材懒加载
- 所有视频元素默认 `<video preload="none">`,不预加载
- 仅预加载当前场景和下一可能场景的素材
- 场景切换时:`video.load()` 加载当前 → `requestIdleCallback` 预加载下一个
- 离开场景时:`video.pause(); video.removeAttribute('src'); video.load()` 释放内存
- 图片使用 `loading="lazy"` 属性
### 移动端适配
- `<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">`
- 竖屏优先布局:文字区占屏幕上方 60%,立绘在下方 40%(横屏时立绘在侧边)
- 选择按钮最小高度 44px(iOS 触控标准)
- 字体 16px 起步,防止 iOS Safari 自动缩放
- 打字机跳过同时监听 `click` 和 `touchend`(不能只依赖 click,移动端有 300ms 延迟)
- CSS `env(safe-area-inset-bottom)` 处理 iPhone 底部安全区
- 禁止双指缩放:`touch-action: manipulation`
- 禁止长按菜单:`-webkit-touch-callout: none; user-select: none`(仅对游戏 UI 元素)
### AI 素材生成(使用 `bl` CLI,⚠️ 必须下载到本地)
使用 Shell 脚本 `scripts/generate-assets.sh` 调用 `bl` CLI 生成并下载素材。`bl video generate` 的 `--download` 标志会自动轮询任务状态直到完成并下载文件,无需手写轮询逻辑。
使用 Shell 脚本 `scripts/generate-assets.sh` 调用 `bl` CLI 生成并下载素材。
#### 角色立绘(有参考图 → image-to-video)
#### 视频素材 — `bl video generate` / `bl video ref`
```bash
# 从角色参考图生成动态立绘
# 角色立绘(有参考图 → image-to-video)
bl video generate \
--image ./references/character_a.png \
--prompt "一个年轻女子微微转头,长发随风飘动,表情温柔,半身特写,电影质感光影" \
--resolution 720P \
--duration 5 \
--watermark false \
--download public/assets/portraits/character_a.mp4
```
--image ./references/ye_wenjie.png \
--prompt "一位穿着军绿色工装的年轻女性,目光坚定地注视远方,微风吹动发丝,半身特写" \
--resolution 720P --duration 5 --watermark false \
--download public/assets/portraits/ye_wenjie.mp4
#### 角色立绘(无参考图 → text-to-video)
```bash
# 纯文本描述生成角色立绘
# 角色立绘(无参考图 → text-to-video)
bl video generate \
--prompt "水墨画风格,一位身穿白色长袍的青年剑客站在竹林中,风吹竹叶,衣袂飘飘,半身特写" \
--resolution 720P \
--duration 5 \
--watermark false \
--download public/assets/portraits/swordsman.mp4
```
--prompt "一位穿着深色西装的中年物理学家,站在大学讲台前,表情严肃,半身特写" \
--resolution 720P --duration 5 --watermark false \
--download public/assets/portraits/physicist.mp4
#### 过场动画
```bash
# 生成高潮场景过场动画
# 过场动画
bl video generate \
--prompt "暴风雨中的悬崖边,闪电划破夜空,两个人影对峙,镜头从远景缓慢推近到中景,电影级画质" \
--resolution 1080P \
--duration 5 \
--watermark false \
--download public/assets/cutscenes/cliff_confrontation.mp4
```
--prompt "红岸基地,巨大的抛物面天线在暴风雨中矗立,闪电照亮天线轮廓,镜头从远景推近,电影级画质" \
--resolution 1080P --duration 5 --watermark false \
--download public/assets/cutscenes/red_coast_storm.mp4
#### 多角色场景(使用 `bl video ref` 保持角色一致性)
```bash
# 多角色一致性场景
# 多角色一致性场景(bl video ref)
bl video ref \
--prompt "Image1 和 Image2 面对面站在古城门前,Image1 递出一封信,Image2 犹豫地伸出手,夕阳逆光" \
--prompt "Image1 和 Image2 在实验室中争论,Image1 愤怒地拍桌子,Image2 冷静地注视" \
--image ./references/character_a.png \
--image ./references/character_b.png \
--resolution 1080P \
--duration 5 \
--watermark false \
--download public/assets/cutscenes/letter_scene.mp4
--resolution 1080P --duration 5 --watermark false \
--download public/assets/cutscenes/lab_argument.mp4
```
#### 完整生成脚本示例 `scripts/generate-assets.sh`
#### 图片素材 — `bl image generate`
```bash
# 角色立绘(静态图片)
bl image generate \
--prompt "一位穿着军绿色工装的年轻中国女性,目光坚定,黑色短发,70年代风格,半身肖像画,电影光影" \
--size 768*1024 --watermark false \
--out-dir public/assets/portraits/ --out-prefix ye_wenjie
# 场景背景图
bl image generate \
--prompt "1960年代中国大学校园,苏式建筑,红色标语横幅,秋天的梧桐树,灰暗天空,写实油画风格" \
--size 1920*1080 --watermark false \
--out-dir public/assets/backgrounds/ --out-prefix campus_1967
# 过场 CG(静态图 + 游戏内 Ken Burns 动效)
bl image generate \
--prompt "红岸基地全景,巨大的抛物面天线矗立在山顶,黄昏,金色光芒,电影级构图" \
--size 1920*1080 --watermark false \
--out-dir public/assets/cutscenes/ --out-prefix red_coast_base
```
#### TTS 旁白 — `bl speech synthesize`(可选)
```bash
# 场景旁白
bl speech synthesize \
--text "1967年,中国大地上,一场浩劫正在改变无数人的命运。" \
--voice longxiaochun --language zh \
--out public/assets/narrations/scene_opening.mp3
# 角色独白
bl speech synthesize \
--text "叶文洁按下了发射按钮,那一刻,她做出了一个改变人类文明命运的决定。" \
--voice longxiaochun --language zh \
--instruction "用沉重而悲伤的语气" \
--out public/assets/narrations/ye_decision.mp3
```
#### 并行生成脚本模式(视频素材提速)
视频生成每个耗时 2-5 分钟,串行太慢。用 `--async` 并行提交再批量下载:
```bash
#!/bin/bash
set -e
# 角色立绘生成
declare -A PORTRAITS=(
["character_a"]="一位身穿红色汉服的少女,微风中秀发飘动,温柔微笑,半身特写"
["character_b"]="一位戴着斗笠的中年剑客,目光锐利,轻抚剑柄,半身特写"
)
# 并行提交所有视频生成任务
declare -A TASKS # name → task_id
echo "=== 生成角色立绘 ==="
echo "=== 提交视频生成任务 ==="
for name in "${!PORTRAITS[@]}"; do
outfile="public/assets/portraits/${name}.mp4"
if [ -f "$outfile" ]; then
echo " [跳过] $name — 已存在"
continue
fi
[ -f "$outfile" ] && echo " [跳过] $name" && continue
echo " [生成] $name ..."
# 如果有参考图则用 --image
if [ -f "references/${name}.png" ]; then
bl video generate \
--image "references/${name}.png" \
--prompt "${PORTRAITS[$name]}" \
--resolution 720P --duration 5 --watermark false \
--download "$outfile"
else
bl video generate \
--prompt "${PORTRAITS[$name]}" \
--resolution 720P --duration 5 --watermark false \
--download "$outfile"
fi
echo " [完成] $name → $outfile"
echo " [提交] $name ..."
tid=$(bl video generate \
--prompt "${PORTRAITS[$name]}" \
--resolution 720P --duration 5 --watermark false \
--async --output json | jq -r '.taskId')
TASKS[$name]="$tid"
echo " [任务] $name → $tid"
done
# 过场动画生成
declare -A CUTSCENES=(
["opening"]="古老的卷轴缓缓展开,露出水墨山水画,镜头推进画中世界,色彩从黑白渐变为彩色"
["climax"]="暴风雨中两人在悬崖边对峙,闪电照亮面庞,镜头围绕旋转,电影质感"
)
echo "=== 生成过场动画 ==="
for name in "${!CUTSCENES[@]}"; do
outfile="public/assets/cutscenes/${name}.mp4"
if [ -f "$outfile" ]; then
echo " [跳过] $name — 已存在"
continue
fi
echo " [生成] $name ..."
bl video generate \
--prompt "${CUTSCENES[$name]}" \
--resolution 1080P --duration 5 --watermark false \
--download "$outfile"
echo " [完成] $name → $outfile"
# 等待并下载所有任务
echo "=== 下载视频 ==="
for name in "${!TASKS[@]}"; do
echo " [下载] $name (task: ${TASKS[$name]}) ..."
bl video download --task-id "${TASKS[$name]}" --out "public/assets/portraits/${name}.mp4"
echo " [完成] $name"
done
# 更新 generated-assets.json
echo "=== 更新 generated-assets.json ==="
node -e "
const fs = require('fs');
const path = require('path');
const assets = { portraits: {}, cutscenes: {} };
for (const f of fs.readdirSync('public/assets/portraits')) {
if (f.endsWith('.mp4')) assets.portraits[path.basename(f,'.mp4')] = '/assets/portraits/' + f;
}
for (const f of fs.readdirSync('public/assets/cutscenes')) {
if (f.endsWith('.mp4')) assets.cutscenes[path.basename(f,'.mp4')] = '/assets/cutscenes/' + f;
}
fs.writeFileSync('src/data/generated-assets.json', JSON.stringify(assets, null, 2));
console.log(' 写入完成:', JSON.stringify(assets, null, 2));
"
echo "=== 全部完成 ==="
```
#### 关键规则
- **素材必须离线生成并下载到本地**:`bl video generate --download` 一步到位完成生成+等待+下载
- **素材必须离线生成并下载到本地**:游戏运行时零 API 调用
- **本地文件路径直接传入 `--image`**:`bl` CLI 自动上传到临时存储,无需手动上传
- **已生成素材自动跳过**:脚本检查本地文件是否存在(`[ -f path ]`)
- **generated-assets.json 只存本地路径**:如 `/assets/portraits/character_a.mp4`,绝不存远程 URL
- **游戏运行时零 API 调用**:所有素材都是预生成的本地文件
- **generated-assets.json 只存本地路径**:如 `/assets/portraits/ye_wenjie.mp4`,绝不存远程 URL
- **视频用 `--async` 并行提交**:3-5 个并发,避免串行等待
### Web Audio API 程序化音乐
- 用 MIDI 音高数组定义旋律乐句,循环播放
@@ -362,17 +376,19 @@ echo "=== 全部完成 ==="
按以下顺序执行,每步完成后标记 task:
1. 初始化 React 项目 + 目录结构
2. 编写 story.js(所有场景文本、选择、分支)— 这是最大的工作量,可用 `bl text chat` 辅助生成
2. 编写 story.js(所有场景文本、选择、分支)— 这是最大的工作量
3. 编写 characters.js 和 archives.js
4. 实现主题样式(CSS 变量、字体、配色、动画)
5. 实现核心组件:TypeWriter → GameScene → ChoicePanel → CharacterPortrait
6. 实现 TitleScreen + EndingScreen + ArchivePanel
7. 实现 useGameState(reducer + hash 路由)
8. 实现 useAudio(BGM 乐谱 + 音效)
9. 如需 AI 素材:编写 `scripts/generate-assets.sh` → 执行 `bash scripts/generate-assets.sh` 生成并下载素材
10. 实现特殊场景效果(Canvas 动态背景、点击交互等)
11. 整合 App.jsx
12. 启动 dev server,浏览器测试完整流程(至少走通两条路线到不同结局)
4. 实现主题样式(CSS 变量、字体、配色、动画、移动端适配)
5. 实现核心组件:TypeWriter → GameScene → ChoicePanel → CharacterPortrait(支持视频/图片)
6. 实现 CutScene(支持视频/图片 Ken Burns)
7. 实现 TitleScreen + EndingScreen + ArchivePanel + SaveLoadPanel
8. 实现 useGameState(reducer + hash 路由 + localStorage 存档)
9. 实现 useAudio(BGM 乐谱 + 音效 + 可选 TTS 旁白播放)
10. 如需 AI 素材:编写 `scripts/generate-assets.sh` → 执行生成并下载
11. 实现特殊场景效果(Canvas 动态背景、点击交互等)
12. 整合 App.jsx
13. 启动 dev server,浏览器测试完整流程(至少走通两条路线到不同结局)
14. 移动端测试(用 Chrome DevTools 模拟手机视口 + 触控)
---
@@ -381,9 +397,10 @@ echo "=== 全部完成 ==="
以下是从实际开发中总结的经验,务必遵循:
- **bl video 分辨率**:`bl video generate` 支持 720P 和 1080P,测试阶段用 720P(更快更便宜),最终版用 1080P
- **bl video --download 自动轮询**:`--download` 标志会自动等待任务完成并下载文件,无需手写轮询代码;如需异步可用 `--async` 获取 task-id 后用 `bl video download --task-id <id> --out <path>` 手动下载
- **本地路径自动上传**:`bl` CLI 的 `--image` 接受本地文件路径,会自动上传到临时存储(48小时有效),无需手动上传到 OSS
- **素材必须离线生成并下载到本地**:视频生成耗时 2-5 分钟/个,绝不能在游戏运行时调用。`--download` 下载到 `public/assets/`,generated-assets.json 中只存本地路径
- **bl video --download 自动轮询**:`--download` 标志会自动等待任务完成并下载文件,无需手写轮询代码;批量生成时用 `--async` + `bl video download` 并行提速
- **bl image generate 尺寸**:用 `--size` 指定,格式为 `宽*高`(如 `1920*1080`),也支持比例格式(如 `16:9`)
- **本地路径自动上传**:`bl` CLI 的 `--image` 接受本地文件路径,会自动上传到临时存储(48小时有效),无需手动上传
- **素材必须离线生成并下载到本地**:视频生成耗时 2-5 分钟/个,绝不能在游戏运行时调用。下载到 `public/assets/`,generated-assets.json 中只存本地路径
- **Prompt 内容审核**:避免暴力、吸烟等敏感描述,否则会被 API 拒绝;换温和表述重试
- **React Hooks 顺序**:所有 useCallback/useEffect 必须在 early return 之前调用,否则报 rules-of-hooks 错误
- **BGM 编曲**:用固定 MIDI 乐谱数组循环播放,不要用随机音符漫游(听起来像噪声)
@@ -391,4 +408,5 @@ echo "=== 全部完成 ==="
- **特殊场景视觉**:Canvas 动态背景必须与叙事内容紧密关联(出现什么元素画什么),不能泛泛画星空了事
- **点击交互**:Canvas 场景加粒子爆发 + 冲击波环 + 屏幕震动 + 主题相关额外效果,大幅提升沉浸感
- **CRA 清理**:初始化后立即删除 App.css/logo.svg/setupTests.js 等样板文件,避免冲突
- **bl text chat 辅助创作**:剧情大纲、场景文本、分支设计、视频 prompt 都可以用 `bl text chat` 辅助生成,大幅减少手写工作量
- **移动端字体**:正文最小 16px,否则 iOS Safari 会自动缩放页面
- **视频内存泄漏**:离开场景时必须 `video.pause(); video.removeAttribute('src'); video.load()` 释放内存