Skip to content

Commit bd4ed99

Browse files
committed
spec(v0.22.0): add clip-first export expectation pack
session: codex130 What: Add a feature-scoped expectation pack with BDD scenarios, visual animation, TTS voice asset, expected keyframes, fixtures, and render_source contract draft for clip-first export. Why: The next development mode should let PM review the final feature outcome before module implementation. Feature acceptance becomes the organizing unit, while recorder/compiler/verifier contracts serve that acceptance flow. Co-Authored-By: Claude Opus 4.7
1 parent eef0b9a commit bd4ed99

21 files changed

Lines changed: 821 additions & 0 deletions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# nf.render_source.v1 Contract Draft
2+
3+
`render_source.json` is the recorder/runtime input. It is generated by the compiler from AI-authored `composition.json`.
4+
5+
Recorder accepts this contract only. Recorder must not read project storage, desktop state, selected clip state, or authoring-only anchor expressions.
6+
7+
Required fields:
8+
9+
- `schema_version`: `"nf.render_source.v1"`
10+
- `duration_ms`: whole video duration in milliseconds
11+
- `viewport`: `{ "w": number, "h": number }`
12+
- `theme.background`: stage background used behind transparent components
13+
- `tracks[]`: resolved render tracks with numeric `begin_ms` / `end_ms`
14+
- `components`: component source registry needed by component tracks
15+
- `assets[]`: explicit local or data assets needed for render/export
16+
17+
Primary commands:
18+
19+
```bash
20+
nf composition compile --project=<slug> --composition=<slug> --out=<source.json>
21+
nf-recorder validate-source --source=<source.json>
22+
nf-recorder export --source=<source.json> --profile=draft --out=<video.mp4> --diagnostics=<diagnostics.json>
23+
nf-recorder snapshot --source=<source.json> --t-ms=<ms> --out=<frame.png>
24+
nf verify-export --source=<source.json> --video=<video.mp4> --out=<report.json>
25+
```
26+
27+
Failure format should include `module`, `stage`, `code`, `message`, and when applicable `track_id`, `clip_id`, and `t_ms`.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { checks, contracts, slides, tracks } from "./mock.js";
2+
3+
const stage = document.querySelector("#stage");
4+
const caption = document.querySelector("#caption");
5+
const checkRoot = document.querySelector("#checks");
6+
const contractRoot = document.querySelector("#contracts");
7+
const trackRoot = document.querySelector("#tracks");
8+
const voice = document.querySelector("#voice");
9+
const voiceBtn = document.querySelector("#voiceBtn");
10+
11+
checkRoot.innerHTML = checks.map((item, index) => `
12+
<article class="check" data-index="${index}">
13+
<b>${item[0]}</b>
14+
<span>${item[1]}</span>
15+
</article>
16+
`).join("");
17+
18+
contractRoot.innerHTML = contracts.map((item, index) => `
19+
<article class="contract" data-index="${index}">
20+
<span>${item[0]}</span>
21+
<code>${item[1]}</code>
22+
</article>
23+
`).join("");
24+
25+
trackRoot.innerHTML = tracks.map((track) => `
26+
<div class="clip ${track.kind}" style="left:${track.left}%;width:${track.width}%">${track.label}</div>
27+
`).join("");
28+
29+
function renderSlide(index) {
30+
const slide = slides[index % slides.length];
31+
stage.innerHTML = `
32+
<div class="grid"></div>
33+
<section class="hero">
34+
<em>${slide.tag}</em>
35+
<h1>${slide.title}</h1>
36+
<p>${slide.caption}</p>
37+
</section>
38+
<div class="source-card">
39+
<strong>${index < 2 ? "composition.json" : "render_source.json"}</strong>
40+
<span>${index < 2 ? "AI 写创作协议" : "recorder 吃渲染协议"}</span>
41+
</div>
42+
<div class="frame-card">
43+
<strong>${index < 3 ? "preview" : "verify-export"}</strong>
44+
<span>${index < 3 ? "same runtime" : "sample clip frames"}</span>
45+
</div>
46+
`;
47+
caption.textContent = slide.caption;
48+
document.querySelectorAll(".check").forEach((el) => el.toggleAttribute("active", Number(el.dataset.index) === slide.activeCheck));
49+
document.querySelectorAll(".contract").forEach((el) => el.toggleAttribute("active", Number(el.dataset.index) === slide.activeContract));
50+
}
51+
52+
let current = 0;
53+
renderSlide(current);
54+
setInterval(() => {
55+
current += 1;
56+
renderSlide(current);
57+
}, 6500);
58+
59+
voiceBtn.addEventListener("click", async () => {
60+
if (voice.paused) {
61+
await voice.play();
62+
voiceBtn.textContent = "暂停语音";
63+
} else {
64+
voice.pause();
65+
voiceBtn.textContent = "播放语音";
66+
}
67+
});
68+
69+
voice.addEventListener("ended", () => {
70+
voiceBtn.textContent = "重播语音";
71+
});
72+
73+
setTimeout(() => {
74+
voice.play().then(() => {
75+
voiceBtn.textContent = "暂停语音";
76+
}).catch(() => {
77+
voiceBtn.textContent = "播放语音";
78+
});
79+
}, 600);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!doctype html>
2+
<html lang="zh-CN">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>clip-first-export expectation pack</title>
7+
<link rel="stylesheet" href="./style.css">
8+
</head>
9+
<body class="nf-bg">
10+
<main class="app" aria-label="Clip first export expectation pack">
11+
<header class="topbar">
12+
<div class="dots"><span></span><span></span><span></span></div>
13+
<strong>NextFrame / clip-first-export</strong>
14+
<nav><span>预期动画</span><span class="active">验收</span><span>Contract</span></nav>
15+
</header>
16+
<section class="workspace">
17+
<aside class="panel left"><h2>功能验收点</h2><div id="checks"></div></aside>
18+
<section class="preview"><div id="stage"></div><div id="caption"></div></section>
19+
<aside class="panel right"><h2>模块契约</h2><div id="contracts"></div></aside>
20+
</section>
21+
<footer class="timeline"><button id="voiceBtn">播放语音</button><div id="tracks"></div><div class="playhead"></div></footer>
22+
<div id="toast">本轮完成</div>
23+
<audio id="voice" src="./voice.aac" preload="auto"></audio>
24+
</main>
25+
<script type="module" src="./animation.js"></script>
26+
</body>
27+
</html>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
export const checks = [
2+
["Feature", "clip-first-export 是验收单位,不是 recorder 模块名。"],
3+
["Expectation", "先看动画、语音、关键帧,再确认要不要开工。"],
4+
["BDD", "每个验收点都有 given / when / then 和 ai_tools。"],
5+
["Contract", "recorder 只吃 render_source.json,不能读桌面端状态。"],
6+
["Visual E2E", "导出后抽每个 clip 中段和边界帧,检查不能粉屏。"]
7+
];
8+
9+
export const contracts = [
10+
["AI / Editor", "composition.json"],
11+
["Compiler", "composition -> render_source"],
12+
["Recorder", "render_source -> mp4 + diagnostics"],
13+
["Verifier", "source + mp4 -> report"]
14+
];
15+
16+
export const tracks = [
17+
{ id: "intro", label: "intro", left: 0, width: 32, kind: "scene" },
18+
{ id: "proof", label: "proof", left: 32, width: 38, kind: "component" },
19+
{ id: "outro", label: "outro", left: 70, width: 30, kind: "text" },
20+
{ id: "voice", label: "voice walkthrough", left: 4, width: 88, kind: "audio" }
21+
];
22+
23+
export const slides = [
24+
{
25+
tag: "FEATURE FIRST",
26+
title: "先看到终点,再开始写代码。",
27+
caption: "功能是组织单位。模块只是实现手段。",
28+
activeCheck: 0,
29+
activeContract: 0
30+
},
31+
{
32+
tag: "EXPECTATION PACK",
33+
title: "动画、语音、关键帧先给你看。",
34+
caption: "你确认最终效果后,BDD 才变成开发入口。",
35+
activeCheck: 1,
36+
activeContract: 1
37+
},
38+
{
39+
tag: "RECORDER CONTRACT",
40+
title: "recorder 只接受 render_source.json。",
41+
caption: "不读桌面端状态,不理解创作层 anchors。",
42+
activeCheck: 3,
43+
activeContract: 2
44+
},
45+
{
46+
tag: "VISUAL E2E",
47+
title: "导出后抽帧,机器先挡明显错误。",
48+
caption: "intro / proof / outro 都要不粉屏、不空画面。",
49+
activeCheck: 4,
50+
activeContract: 3
51+
}
52+
];

0 commit comments

Comments
 (0)