vr-courseware-builder · VR 交互课件生成器
把「教材/课文文本 → 可离线运行的 VR 交互课件」的全流程固化为可反复调用的技能。
> 本技能是从一个已完整落地的交互课件项目(4797 行引擎 + 13 个教学页 + 8 类题型 + 12 项系统能力)
> 反向抽象而成,保留了原课件全部交互能力,并剥离了具体课文业务逻辑,使任意教材都能套用。
1. 触发条件
应当触发:
- 用户给出教材/课文原文(如「把这篇课文做成交互课件」「按这个文本生成 VR 课件」)
- 用户要求改造现有交互课件(增加题型、换风格、适配其他学科)
- 用户问「这个课件是怎么做的 / 能不能复用到别的课文」
不应触发:
- 只是要一份纯文本教案 / 纯 HTML 静态页面(无需交互)
- 用户未提供任何文本素材且期望 AI 自行编写教材内容
2. 输入参数
2.1 必填
| 参数 | 类型 | 说明 | 示例 |
|---|---|---|---|
text |
string | 教材/课文正文,支持中英文混排 | "After a busy day at work, I got on the train..." |
2.2 选填
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
title |
string | "未命名课件" |
课件标题 |
subject |
enum | 自动判定 | english / chinese / 其他(走通用模板) |
stage |
enum | junior |
primary / junior / senior |
chapters |
array | [] |
章节切分:[{title, en, zh}],给了就按章节分场景 |
sceneCount |
number | 按学段 | 场景数(不给则自动) |
spotsPerScene |
number | 按学段 | 每场景热点数 |
taskMix |
array | 按学段 | 题型配比,如 ["choice","fill","tf","order"] |
chain |
array | 按学段 | 教学环节名,如 ["初读感知","精读品味","迁移表达"] |
style |
string | classic |
classic / modern / ink |
seed |
string | title |
随机种子,同 seed 产出完全一致 |
vocabLimit |
number | 12 |
抽取词条上限 |
2.3 参数示例
{
"title": "规则·八上 Unit 5 Play by the rules",
"subject": "english",
"stage": "junior",
"seed": "unit5",
"style": "classic",
"chapters": [
{ "title": "站台上车", "en": "I got on the train to go home." },
{ "title": "对面的女孩", "en": "She played loud music." }
],
"text": "After a busy day at work, I got on the train to go home. A girl opposite him started playing loud music on her phone. A man next to him took out smelly food. A boy behind him kicked his seat; his mother excused him. At last the writer stood up and cleared his throat."
}
3. 执行流程
步骤 0 · 环境准备
确认 Node.js 可用(≥16),确认技能目录下 assets/engine/ 四个文件完整。
步骤 1 · 解析教材文本 → 知识点
node scripts/parse_textbook.js --in lesson.json --pretty > skeleton.json
解析动作(详见 §5):
1. 分句(中英文标点通吃)
2. 抽词表:英文按「频次 + 长度」排序、中文去虚词;同时抽「英文—中文」对照
3. 抽句型:按学科正则命中结构词(如英语的 to/in order to/so as to、if/when/because/although)
4. 抽情感曲线:逐句命中情绪词打分
步骤 2 · 规划课件结构
- 场景切分:给了
chapters按章切,否则按句数均衡切sceneCount段 - 教学环节:按学段取默认
chain,可被chain参数覆盖 - 导航表:首页 + VR 漫游 + N 个环节 + 总结
步骤 3 · 生成交互节点
每个场景按 spotsPerScene 生成热点,每个热点绑定一题:
- 均匀分布 yaw(-150°~+150°,避开正后方死角)
- pit 高低交替(±8°)避免视觉拥挤
- 题型按 taskMix 轮转,保证题型多样性
步骤 4 · 生成全景图
node scripts/gen_pano.py --plan skeleton.json --out ./course/assets/pano
若 AI 生图可用,按 assets.pano[].prompt 逐张生成并替换;否则保留占位 SVG,课件仍可运行。
步骤 5 · 组装课件
node scripts/assemble.js --skeleton skeleton.json --out ./course
产出 index.html(内联全部 CSS/JS,可双击离线运行)+ assets/pano/ + data/course.json。
步骤 6 · 质量校验
node verify/check_courseware.js --dir ./course --port 8733
详见 §6 校验规则。
4. 输出结构
course/
├── index.html ← 课件本体(单文件,约 180KB,可双击离线运行)
├── data/course.json ← 骨架原件(便于二次编辑后重新组装)
└── assets/pano/ ← 全景图 sceneN.svg(占位)/ sceneN.jpg(真实)
骨架 JSON 契约:
{
"meta": { "title", "subject", "stage", "lang", "seed", "generator", "style" },
"stats": { "scenes", "spots", "tasks", "vocab", "sentences", "patterns" },
"knowledge": {
"words": [{ "w", "pos", "zh", "sent", "conf" }], // 词表(含原句与置信度)
"patterns": [{ "kind", "sentence", "conf" }], // 句型
"curve": [{ "t", "v", "when" }] // 情感曲线
},
"chain": ["初读感知", "精读品味", "迁移表达"], // 教学环节
"scenes": [{
"id": 1, "name": "站台上车", "ico": "🚉",
"pano": "assets/pano/scene1.jpg",
"intro": "…", "signs": [{ "yaw", "pit", "bg", "en", "zh" }],
"sents": ["…"], // 本场景原句
"spots": [{
"id": "S1-1", "t": "热点标题", "yaw": -150, "pit": -8,
"en": "原句(英文课)", "zh": "原句(中文课)", "note": "讲解",
"pts": 5,
"task": { "id": "T1", "k": "choice", "stem": "…", "opts": [], "ans": [], "fb": "解析" }
}]
}],
"assets": {
"pano": [{ "scene": 1, "file": "…", "prompt": "生图提示词" }],
"cardart": { "back": "…", "front": "…" }
}
}
5. 教材解析与结构规划方法
5.1 三层知识点抽取
| 层 | 方法 | 产出 |
|---|---|---|
| 词汇层 | 英文:分词→去停用词→按(频次,长度)排序;中文:2–4 字词→去首尾虚词 | knowledge.words,每项带 conf(有对照释义 0.95,否则 0.6) |
| 句型层 | 按学科正则表扫描全句,每种结构首次命中即记录 | knowledge.patterns(英语:目的状语/状语从句/时态;语文:比喻/拟人/情绪/修辞) |
| 语篇层 | 逐句命中情绪词,按密度打分 0.25~1.0 | knowledge.curve,用于情感曲线图 |
5.2 学科差异(SUBJECT_PROFILES)
| 英语 | 语文 | 通用 | |
|---|---|---|---|
| 场景数(初中) | 6 | 6 | 5 |
| 每景热点 | 4 | 5 | 4 |
| 题型配比 | choice/fill/tf/order/match | choice/fill/tf/order/match/cat | choice/fill/tf/order |
| 默认环节 | 读前预测→初读发现→精读理解→迁移运用 | 初读感知→精读品味→语言积累→迁移表达 | 认识→理解→分析→练习→总结 |
| 关键词表 | to/in order to、if/when/because、时态 | 像/仿佛、情绪词、比喻拟人 | — |
5.3 结构→场景映射
教材文本
├─ chapters 存在 → 按章节边界切场景(不足时按句子配额补足)
└─ chapters 缺失 → 按句数均衡切 sceneCount 段
↓
每段 → 场景 { pano, intro, signs }
↓
每场景 → spotsPerScene 个热点
↓
热点 yaw 均匀铺开(-150°~150°) / pit 高低交替 → 视觉不拥挤
↓
每热点 → 按 taskMix 轮转绑定题型
5.4 映射到 VR 交互场景
- 场景 = VRView():全景图横向平移模拟环视(拖一屏 ≈ 130°)
- 热点 = .hotspot:按
layoutHot(yaw, pit)投影到全景上,点击弹出任务卡 - 任务卡 = 题型工厂:
mkChoice / mkTF / mkFill / mkOrder / mkMatch / mkCat / mkSay / mkSort - 环节页 = 知识点卡:可朗读、列出本场景考点(骨架未给
COURSE_PAGES时的只读回退)
6. 质量校验规则
verify/check_courseware.js 自动执行以下检查,任一 FAIL 即视为产出不合格:
6.1 静态检查
| # | 规则 |
|---|---|
| S1 | index.html 存在且 > 80KB(说明引擎与样式已内联) |
| S2 | 无外部资源依赖(grep 无 http:// / https:// 的 css/js 引用) |
| S3 | CSS 花括号与注释均配平({/} 与 /*/*/ 数量相等) |
| S4 | 无 font-size 小于 20px(设计系统硬下限) |
6.2 结构检查
| # | 规则 |
|---|---|
| J1 | scenes.length ≥ 3 |
| J2 | 每个场景 spots.length ≥ 3 |
| J3 | 每个热点都有 task,且 task.k ∈ 8 类合法值 |
| J4 | 所有 yaw ∈ [-180, 180]、pit ∈ [-90, 90] |
| J5 | points(分值)总和 > 0 |
6.3 运行时检查(本地 HTTP + 无头浏览器)
| # | 规则 |
|---|---|
| R1 | 页面加载 零 JS 错误 |
| R2 | 每个页面可切换,active 类正确落在目标页 |
| R3 | 侧栏导航高亮与当前页一致 |
| R4 | VR 页存在 .vrstage、.hotspot 数量 = 该场景热点数 |
| R5 | 点击热点能弹出任务卡(modal 显示) |
| R6 | 环节页有内容(知识点卡 / 题目卡) |
| R7 | 390×844 移动端无横向滚动 |
| R8 | 顶栏进度/得分元素存在且随作答更新 |
> ⚠️ 运行时检查必须用本地 HTTP 服务,不能用 file://
> (Edge 在 file:// 下会强缓存 CSS/JS,导致读到旧版本、产生假阴性)。
> 校验脚本已内置 Cache-Control: no-store 的静态服务。
7. 内置能力清单(已实现并验证)
7.1 场景与 3D 展示
| 能力 | 说明 |
|---|---|
| 全景环视 | VRView():横向平移模拟 yaw,拖一屏 ≈ 130°,transform 平滑插值 |
| 场景切换 | 顶部分段按钮切换,记忆 S.sceneOpen |
| 视角钳制 | yaw 限 [-180, 180],不会无限旋转 |
| 标志牌 | signs 按 yaw/pit 投影,支持中英双语标识 |
| 响应式舞台 | 桌面/移动端自适应缩放,<img> 防拖拽 |
7.2 交互组件(8 类题型工厂)
| 题型 | 函数 | 形态 |
|---|---|---|
| 选择题 | mkChoice() |
N 选 1,多选 ans 数组 |
| 判断题 | mkTF() |
True/False 双选 |
| 填空题 | mkFill() |
输入框,支持多答案 |
| 连线题 | mkOrder() |
点选排序(语序/时间轴) |
| 配对题 | mkMatch() |
左右列配对 |
| 归类题 | mkCat() |
拖拽归类 |
| 口语题 | mkSay() |
录音 + Rubric 自评 |
| 排序题 | mkSort() |
拖拽排序 |
统一能力:即时判定对错 / .right .no 高亮 / 禁用 / 解析反馈 / 计分 / 错因打点。
7.3 语音与字幕
| 能力 | 说明 |
|---|---|
| TTS 朗读 | speak(text, el),Web Speech API |
| 语音库选择 | loadVoices() + pickVoice() 优选 en-US / zh-CN |
| 倍速控制 | setRate():0.75× / 1.0× / 1.25× |
| 句子框 | sentenceBox(en, zh, extra, mask):英文+译文+倍速;mask=true 支持「先听后揭晓」 |
| 录音回放 | recStart() / recStop() / scoreSpoken(),MediaRecorder |
7.4 进度与得分
| 能力 | 说明 |
|---|---|
| 任务状态 | S.done Set,按 taskId 去重 |
| 热点状态 | S.hotDone 对象,记录已点热点 |
| 计分 | addScore(n, src),累加并写入日志 |
| 正确率 | accuracy() = 答对/总作答 |
| 徽章 | grantBadge(id),按条件自动授予 |
| 错因本 | logErr(tag, src) / renderErrbook() |
| 持久化 | localStorage 存储全部状态 |
| 顶栏同步 | syncTopbar():进度条 / 得分 / 步骤数 |
7.5 UI 与设置
顶栏(品牌+进度+金币+步骤)、侧栏导航(分组/高亮/已完成标记)、
移动端抽屉导航、Toast 提示、Modal 弹窗、resetAll() 重置、
20px 字号硬下限、6 档响应式断点。
7.6 音效(sound.js)
Web Audio API 实时合成(无音频文件):
翻牌 / 配对成功 / 配错 / 洗牌 / 滴答 / 失败 / 通关;
含双层防叠加(时间窗节流 + 并发声部预算)、音量控制与静音开关。
8. 调用方式
8.1 对话式调用(推荐)
@skill:vr-courseware-builder
标题:规则·八上 Unit 5 Play by the rules
学科:english 学段:junior
风格:classic
种子:unit5
课文正文:
After a busy day at work, I got on the train to go home.
A girl opposite him started playing loud music on her phone.
A man next to him took out smelly food.
A boy behind him kicked his seat; his mother excused him.
At last the writer stood up and cleared his throat.
8.2 命令行三步
S=~/.workbuddy/skills/vr-courseware-builder
# ① 解析 → 骨架
cat > /tmp/lesson.json <<'JSON'
{ "title":"规则·八上 Unit 5", "subject":"english", "stage":"junior",
"seed":"unit5",
"text":"After a busy day at work, I got on the train to go home. A girl opposite him started playing loud music." }
JSON
node $S/scripts/parse_textbook.js --in /tmp/lesson.json --pretty > /tmp/skeleton.json
# ② 组装 → 课件
node $S/scripts/assemble.js --skeleton /tmp/skeleton.json --out ./unit5-course
# ③ 校验
node $S/verify/check_courseware.js --dir ./unit5-course
8.3 结果
./unit5-course/index.html —— 双击即可离线运行,无需服务器、无需联网。
9. 扩展指南
9.1 新增题型
在 assets/engine/engine.core.js 加一个 mkXxx(host, id, t, onOk),
然后在 parse_textbook.js 的 SUBJECT_PROFILES[subject].stages[*].taskMix 里加入 'xxx',
并在 makeTask() 里加一个 case。无需改动引擎其他部分。
契约:mkXxx 内部必须调用 taskShell(id, stem, pts, body) 包外壳,
用 fbOf(el, "ok"|"no", html) 写反馈,用 markDone/addScore/logErr 上报。
9.2 适配新学科
在 parse_textbook.js 的 SUBJECT_PROFILES 加一项:
physics: {
label: '物理', lang: 'mixed',
stages: {
junior: { sceneCount: 6, spotsPerScene: 5,
taskMix: ['choice','fill','tf','order'],
chain: ['现象观察','规律探究','实验设计','迁移应用'] },
…
},
keywords: { force:/力|压力|浮力/, energy:/动能|势能|能量/ }
}
9.3 新增样式主题
courseware.css 顶部的 :root 变量即设计令牌(配色/字号/间距/圆角/阴影)。
改 --pri / --ok / --no / --fs-* 即可整体换肤,字号 20px 下限由
verify/check_courseware.js 的 S4 规则守护。
9.4 新增页面类型
- 在
assemble.js的defaultPages()里加页面骨架 - 在
buildNav()里加对应导航项 - 在
app.core.js的COURSE_PAGE_INIT里加渲染函数 - 若需要在骨架里传内容,扩展
COURSE_PAGES
10. 已知约束
- AI 生图需外部能力 ——
gen_pano.py默认产出占位 SVG;
真实全景图需接入fmg-vrkj或gen_pano_ai之类技能。 - 解析为规则驱动 —— 无 LLM 参与,陌生语言/体裁的解析质量有限;
复杂教材建议先生成骨架、人工校对knowledge与task,再组装。 file://缓存 —— 调试时用本地 HTTP 服务,不要直接双击打开后看控制台。- 题型选项质量 ——
makeTask()的干扰项是启发式生成的
(形近词、随机偏移);正式使用前建议人工复核task.opts。