dsh-creation-mode 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client.js +213 -1282
- package/data.js +34 -2
- package/flow.js +82 -157
- package/index.js +115 -5
- package/package.json +5 -2
- package/references/audio.md +167 -0
- package/references/camera.md +168 -0
- package/references/color.md +272 -0
- package/references/copy.md +121 -0
- package/references/data-viz.md +189 -0
- package/references/effects.md +236 -0
- package/references/motion.md +322 -0
- package/references/particles.md +166 -0
- package/references/sequencing.md +143 -0
- package/references/text-animation.md +213 -0
- package/references/timbre.md +182 -0
- package/references/transitions.md +231 -0
- package/references/typography.md +276 -0
- package/tools.js +227 -33
package/data.js
CHANGED
|
@@ -22,6 +22,38 @@ export const APPROVED_FILE = fileURLToPath(new URL('./stills/approved.json', HER
|
|
|
22
22
|
export const OUTPUT_DIR = fileURLToPath(new URL('./created/', HERE));
|
|
23
23
|
/** Subdirectory an export is written into, under the session directory. */
|
|
24
24
|
export const EXPORT_DIR_NAME = 'code-animation';
|
|
25
|
+
/**
|
|
26
|
+
* 只留成品:返工不留中间稿。
|
|
27
|
+
*
|
|
28
|
+
* 每次 save_animation / save_storyboard 之后,同类旧稿会被删掉,created/ 里只保留
|
|
29
|
+
* 最近 KEEP_SAVES 份(1 = 只留最新的一版动画工程、配乐工程与分镜)。
|
|
30
|
+
* 调大可以留几版做对比,但用户要的是「不保留中间过程」,所以默认 1。
|
|
31
|
+
*/
|
|
32
|
+
export const KEEP_SAVES = 1;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 工艺参考:按需加载的 Markdown,放在 references/ 下。
|
|
36
|
+
*
|
|
37
|
+
* 原来这些内容全压在 flow.js 里,每轮都要占满上下文;拆出来后由 get_reference
|
|
38
|
+
* 按需取。13 份参考对应 13 个工艺方向,和 Remotion 的 skill 组织方式一致。
|
|
39
|
+
*/
|
|
40
|
+
export const REFERENCES_DIR = fileURLToPath(new URL('./references/', HERE));
|
|
41
|
+
|
|
42
|
+
export const REFERENCES = {
|
|
43
|
+
motion: '运动与缓动',
|
|
44
|
+
transitions: '转场',
|
|
45
|
+
typography: '中文排版',
|
|
46
|
+
color: '调色与质感',
|
|
47
|
+
effects: '视觉特效',
|
|
48
|
+
'text-animation': '文字动画',
|
|
49
|
+
particles: '粒子系统',
|
|
50
|
+
camera: '镜头与空间',
|
|
51
|
+
'data-viz': '数据可视化',
|
|
52
|
+
sequencing: '编排',
|
|
53
|
+
audio: '音画同步',
|
|
54
|
+
timbre: '音色配方',
|
|
55
|
+
copy: '文案标准',
|
|
56
|
+
};
|
|
25
57
|
|
|
26
58
|
/**
|
|
27
59
|
* Build marker surfaced by the health route.
|
|
@@ -30,10 +62,10 @@ export const EXPORT_DIR_NAME = 'code-animation';
|
|
|
30
62
|
* module once at process start, so a running host can still serve an older revision
|
|
31
63
|
* than the one on disk, and this is how that shows up.
|
|
32
64
|
*/
|
|
33
|
-
export const BUILD_REV =
|
|
65
|
+
export const BUILD_REV = 24;
|
|
34
66
|
|
|
35
67
|
/** Tool names this revision registers, in the order they are declared. */
|
|
36
|
-
export const TOOL_NAMES = ['list_styles', 'show_stills', 'get_style', 'save_storyboard', 'save_animation'];
|
|
68
|
+
export const TOOL_NAMES = ['list_styles', 'show_stills', 'get_style', 'save_storyboard', 'save_animation', 'get_reference'];
|
|
37
69
|
|
|
38
70
|
let cached;
|
|
39
71
|
let approvedCache;
|
package/flow.js
CHANGED
|
@@ -39,64 +39,45 @@ export const FLOW_PROMPT = `## 创作流程
|
|
|
39
39
|
在 ask_user_question 的 options 里列出展示的各款风格(例如 label: "C1 水墨晕染"、label: "L19 写意山水"、label: "R6 东方赛博"、以及 label: "换一批风格")。
|
|
40
40
|
**绝对禁止用普通文本让用户回复,必须调用 ask_user_question 工具生成点击按钮!**
|
|
41
41
|
|
|
42
|
-
- 用户点击选定了 → get_style
|
|
42
|
+
- 用户点击选定了 → get_style 取完整规范,然后**先取两份参考再写分镜**:
|
|
43
|
+
get_reference("color") 定调色板,get_reference("typography") 定字号阶梯。
|
|
44
|
+
这两件事必须在分镜之前定,因为分镜里的「画面内容」和「字幕长度」都依赖它们——
|
|
45
|
+
字号没定,你根本不知道一条字幕能放几个字。
|
|
46
|
+
- 定完不用给用户看,自己记住即可,进入第 4 步。
|
|
43
47
|
- 都不满意 → 回第 2 步换一批候选,或问清他想要什么感觉再选。
|
|
44
48
|
- 用户说"你定" → 你选最匹配的一条,但仍要把选择说清楚并给出理由。
|
|
45
49
|
|
|
46
50
|
### 第 4 步 · 出分镜草稿与配乐卡点谱
|
|
47
|
-
|
|
51
|
+
|
|
52
|
+
**先取两份参考,再拆镜头:**
|
|
53
|
+
|
|
54
|
+
- get_reference("sequencing") —— 分镜就是时间轴。每场多长、节奏曲线怎么走、
|
|
55
|
+
哪里密哪里疏、开场三秒怎么抓人、收尾留几秒,都在这一份里。**不先读它,
|
|
56
|
+
分镜的时长和顺序一定是拍脑袋定的。**
|
|
57
|
+
- get_reference("audio") —— 分镜要同时给出配乐卡点谱,那就得知道卡点怎么落、
|
|
58
|
+
重音怎么分档。
|
|
59
|
+
|
|
60
|
+
拆成 3–8 个镜头,调用 save_storyboard 提交草稿。每个镜头写清:
|
|
48
61
|
- 起止时间(秒)与时长
|
|
49
62
|
- 画面内容:谁 / 在哪 / 在做什么
|
|
50
63
|
- 画面上的文字:字幕与标题,写原文
|
|
51
64
|
- 运镜与转场
|
|
52
|
-
- **镜头内关键动作与卡点(cues)**:除了镜头切点外,镜头内部若有关键视觉动作(如「0.8s 粒子汇聚成核心标题」、「2.4s 数据狂飙冲顶」、「4.1s 画面反转炸开」),必须在 cues 数组中精确标注时间秒数与类型(impact 重击、drop
|
|
53
|
-
|
|
54
|
-
#### 文案标准(字幕与标题)
|
|
55
|
-
|
|
56
|
-
字幕不是文章。观众只有这条字幕停留的那一两秒,读不完也没有耐心读第二遍。
|
|
57
|
-
默认要**具体**,不是**抒情**,也不是**口语**——口语只是换一种空话。
|
|
58
|
-
|
|
59
|
-
**硬规则(save_storyboard 会校验)**
|
|
60
|
-
|
|
61
|
-
1. 单条字幕 ≤ 镜头时长 × 5.5 字。2 秒镜头约 11 字,5 秒镜头约 27 字。
|
|
62
|
-
超了就删内容,不要缩字号、不要压行、不要用「…」拖过去。
|
|
63
|
-
2. 一条字幕只说一件事。两件事就拆两场。
|
|
64
|
-
|
|
65
|
-
**写法**
|
|
66
|
-
|
|
67
|
-
3. 至少一个可核查的具体信息:数字、专名、时间、地点、动作、前后对比。
|
|
68
|
-
一个都没有,说明这条还没想清楚,回去想清楚再写。
|
|
69
|
-
4. 动词开头优先。能写「改到第七版」就不写「支持多次修改」。
|
|
70
|
-
5. 句长要有变化。整条都是四字格或六字格,读起来就像广告词。
|
|
71
|
-
6. **保留事实的强度。**「从 3.2 秒压到 0.8 秒」不许改写成「大幅提速」;
|
|
72
|
-
反过来,「大幅提速」也不许擅自补成具体数字——那是编数据。
|
|
65
|
+
- **镜头内关键动作与卡点(cues)**:除了镜头切点外,镜头内部若有关键视觉动作(如「0.8s 粒子汇聚成核心标题」、「2.4s 数据狂飙冲顶」、「4.1s 画面反转炸开」),必须在 cues 数组中精确标注时间秒数与类型(impact 重击、drop 段落转折、hit 清脆打击、rise 聚能上扬、whoosh 呼啸)!
|
|
73
66
|
|
|
74
|
-
|
|
67
|
+
#### 文案标准
|
|
75
68
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
9. 空泛评价:极致、震撼、完美、惊艳、颠覆、前所未有。
|
|
79
|
-
10. 连接词开场:首先、其次、此外、综上所述、值得一提的是。
|
|
80
|
-
11. 万能句式:让我们一起…/这不仅…更是…/从…到…。
|
|
81
|
-
12. 语气词与网络流行语:啊呢吧啦、绝绝子、yyds、狠狠爱了。
|
|
82
|
-
|
|
83
|
-
**改写时只做减法。** 删掉不承载信息的包装语,不换同义词,不把几个词压成一个概括词。
|
|
84
|
-
用户没有明确说「写平实点」,就不要降低原有判断的强度。片名(title)可以留白、可以有态度,
|
|
85
|
-
但同样不许堆形容词。
|
|
86
|
-
|
|
87
|
-
**对照**(左边这种会被打回)
|
|
88
|
-
|
|
89
|
-
- 「让我们一起探索这款产品如何赋能创作流程」→「三秒一版分镜。改到第七版,它还记着第一版」
|
|
90
|
-
- 「以极致性能带来前所未有的体验」→「首屏从 3.2 秒压到 0.8 秒」
|
|
91
|
-
- 「化繁为简,让复杂工作变得轻松」→「以前开四个窗口对着抄,现在一屏」
|
|
69
|
+
字幕与标题怎么写,见 get_reference("copy")。三条会被 save_storyboard 直接打回:
|
|
70
|
+
单条字幕超过「镜头时长 × 5.5 字」、一条字幕说两件事、通篇没有一个可核查的具体信息。
|
|
92
71
|
|
|
93
72
|
#### 音乐风格提示词(music)的推导规则(无任何预设限制):
|
|
94
73
|
不要使用机械死板的固定预设!音乐必须像专业电影原声一样,根据【视频画风 + 剧情情绪起伏】量身定制自由的音乐提示词:
|
|
95
|
-
1.
|
|
74
|
+
1. **调性对齐画风**:国风/水墨应是「古筝泛音悠扬、竹笛空灵铺底、兼有温润大堂鼓」;赛博霓虹应是「失真酸性贝斯、极速脉冲琶音、冷冽数字合成音」;扁平科普应是「轻快顿音拨弦、明朗马林巴、清脆鼓点」;史诗宏大应是「深邃弦乐群长音、大堂鼓重击与铜管齐鸣」。
|
|
96
75
|
2. **情绪动态曲线**:开头如何铺垫、高潮在第几秒爆发、收尾如何淡出。
|
|
97
|
-
3.
|
|
76
|
+
3. **不许写低频扫频底鼓**:把正弦从一两百赫兹滑到三四十赫兹那种声音,听感像放屁,
|
|
77
|
+
不是鼓。要重音就用固定频率的低正弦加鼓皮噪声,靠包络做冲击。这条没有例外。
|
|
78
|
+
4. 把这段自由定制的音乐描述填入 save_storyboard 与 save_animation 的 music 字段,作为写 score_js 的依据。
|
|
98
79
|
|
|
99
|
-
**每个镜头切点与内部 cues 都会成为配乐的绝对卡点!**
|
|
80
|
+
**每个镜头切点与内部 cues 都会成为配乐的绝对卡点!** 你在 score_js 里按这些毫秒落声学重音与转场冲击,画面动、声音同时炸裂。
|
|
100
81
|
|
|
101
82
|
### 第 5 步 · 等用户拍板分镜(硬门禁)
|
|
102
83
|
提交分镜后,**必须调用 ask_user_question 工具**让用户确认分镜,提供选项(如 label: "确认分镜,开始制作"、label: "需要调整分镜或文案")。
|
|
@@ -114,6 +95,57 @@ export const FLOW_PROMPT = `## 创作流程
|
|
|
114
95
|
画面必须满足:
|
|
115
96
|
- 页面 HTML 必须显式包含一个 <canvas> 元素,画面全部用 Canvas 2D 或 WebGL 绘制,绝对禁止写纯 DOM/CSS 动效(DOM 元素无法被浏览器 MediaRecorder 捕获导出为视频)。
|
|
116
97
|
- 必须实现 window.renderAt(t) 接口(t 为秒,同一时刻 t 渲染同一帧)。
|
|
98
|
+
- 画布可以按 1920×1080 或 1280×720 这样的固定像素画,但页面要能自适应:
|
|
99
|
+
给 canvas 加 max-width:100% / max-height:100%,让它按原比例缩进预览框。
|
|
100
|
+
写死 width:1280px 又没有任何约束,卡片里就会溢出、出滚动条、画面看不全。
|
|
101
|
+
|
|
102
|
+
#### 交付之后的修改:必须重新调用 save_animation
|
|
103
|
+
|
|
104
|
+
卡片只在 save_animation 的返回里出现。**只改磁盘上的文件、或只回一句「改好了」,用户那边什么都不会变。**
|
|
105
|
+
|
|
106
|
+
所以交付后用户提出任何改动——配乐、时长、字幕、画面、节奏——都按这个来:
|
|
107
|
+
|
|
108
|
+
1. **只改一部分时,传 base_id。** 上一版的 id 在 save_animation 的返回值里。
|
|
109
|
+
系统会读回上一版的正文与元数据,你只传这次要改的字段:
|
|
110
|
+
|
|
111
|
+
- 只换配乐 → base_id + 新的 score_js
|
|
112
|
+
- 只改画面 → base_id + 新的 html(配乐会自动沿用上一版)
|
|
113
|
+
- 改时长 → base_id + 新的 duration(会重新算切点)
|
|
114
|
+
|
|
115
|
+
2. **整份重写时**,才把完整 html 与 score_js 一起传。
|
|
116
|
+
3. 不要为了「省一次调用」去写文件然后说改完了——用户看不到,等于没改。
|
|
117
|
+
4. **工作区只留最新一版。** 每次保存都会删掉上一版动画与上一版分镜,中间稿不保留;
|
|
118
|
+
所以不要引用旧 id 的文件路径,也不要在回复里提「历史版本」。每次提交生成新卡片,
|
|
119
|
+
旧卡片会随旧稿一起失效,这是预期行为。
|
|
120
|
+
5. 修改阶段**不必重走风格与分镜门禁**;只有用户要换风格或改分镜结构时才回到第 3、5 步。
|
|
121
|
+
6. 改完说清楚改了什么,别只说「已优化」。
|
|
122
|
+
|
|
123
|
+
#### 写动画前要取的参考
|
|
124
|
+
|
|
125
|
+
前面几步已经取过 sequencing / color / typography / audio / copy,不用重取。
|
|
126
|
+
下面这些是写代码时才用得上的,按这条片子实际需要取,**至少取 motion**:
|
|
127
|
+
|
|
128
|
+
| 调用 | 什么时候取 |
|
|
129
|
+
|---|---|
|
|
130
|
+
| get_reference("motion") | 必取。缓动、夹紧、帧预算、运动模糊、预备-动作-跟随 |
|
|
131
|
+
| get_reference("transitions") | 有 2 个以上镜头时。硬切/叠化/擦除,叠化会吃时长 |
|
|
132
|
+
| get_reference("text-animation") | 文字要动的时候。进场、出场、强调、数字滚动 |
|
|
133
|
+
| get_reference("effects") | 需要模糊、发光、故障、拖影、扫描线时 |
|
|
134
|
+
| get_reference("particles") | 用粒子做氛围或特效时 |
|
|
135
|
+
| get_reference("camera") | 推拉摇移、视差、伪 3D 时 |
|
|
136
|
+
| get_reference("data-viz") | 画面里有图表、数字对比时 |
|
|
137
|
+
| get_reference("timbre") | 写 score_js 之前。28 条音色配方 + 编曲层次 |
|
|
138
|
+
|
|
139
|
+
一条 30 秒的片子,通常取 3–5 份就够。取太多反而抓不住重点。
|
|
140
|
+
|
|
141
|
+
#### 交付前自检
|
|
142
|
+
|
|
143
|
+
保存之前,用取过的那几份参考末尾的自检清单过一遍。至少这三条必须成立:
|
|
144
|
+
|
|
145
|
+
1. renderAt(0)、renderAt(duration/2)、renderAt(duration-0.05) 三帧都不是黑屏、
|
|
146
|
+
不是静止、不是被截断的半截动作。
|
|
147
|
+
2. 没有任何 Math.random() 或 Date.now()——它们会让导出和预览不一致。
|
|
148
|
+
3. 每个 beats 里的 impact 在 score_js 里都有对应的声音事件。
|
|
117
149
|
|
|
118
150
|
#### 卡点:先把 beats 导出来,再写配乐
|
|
119
151
|
|
|
@@ -162,10 +194,11 @@ export const FLOW_PROMPT = `## 创作流程
|
|
|
162
194
|
渐出不是硬切——引擎已经把铺底做成分段下降(快→慢→贴近静音),听感上是音乐让位而不是断带。
|
|
163
195
|
你不要在 score_js 里自己再做一遍这件事,只要把 power 标对即可。
|
|
164
196
|
|
|
165
|
-
####
|
|
197
|
+
#### 配乐:必须由你当场写 score_js
|
|
166
198
|
|
|
167
199
|
save_animation 的 score_js 参数是一段 Web Audio 合成代码,直接决定这条片子听起来是什么声音。
|
|
168
|
-
|
|
200
|
+
**它是必填项,没有兜底引擎**:不写就没有声音,保存会直接报错打回。没有现成音色库可以套,
|
|
201
|
+
每一段配乐都要按这条片子的节奏、情绪、卡点现写。
|
|
169
202
|
|
|
170
203
|
代码是一个**函数体**(不要写 \`function(...)\` 头,不要 return),可用形参:
|
|
171
204
|
- \`ctx\` — OfflineAudioContext
|
|
@@ -188,119 +221,11 @@ save_animation 的 score_js 参数是一段 Web Audio 合成代码,直接决
|
|
|
188
221
|
- 背景铺底要比卡点轻很多(大约 1/10),否则重击会被淹掉。
|
|
189
222
|
- 所有节点必须在 duration 内收尾,函数里不要 await。
|
|
190
223
|
|
|
191
|
-
####
|
|
192
|
-
|
|
193
|
-
「听感」那栏是它应该听起来像什么。如果你写出来的东西和这栏不符,说明配方用错了,重写。
|
|
194
|
-
|
|
195
|
-
| 音色 | 听感 | 做法 |
|
|
196
|
-
|---|---|---|
|
|
197
|
-
| 底鼓 kick | 低频冲击,几乎无音高感,短促 | 正弦 160→44Hz 在 70ms 内扫完,指数衰减 0.16s,叠 6ms 噪声瞬态 |
|
|
198
|
-
| 军鼓 snare | 中频「沙」加一点鼓皮音 | 2200Hz 带通噪声衰 0.07s,叠 190Hz 与 330Hz 正弦短衰减 |
|
|
199
|
-
| 闭合踩镲 | 极短极亮,只有「呲」一声 | 7000Hz 高通噪声,衰减 0.012s |
|
|
200
|
-
| 开镲 open hat | 同样高频,但尾巴拖长 | 同上,衰减 0.18s |
|
|
201
|
-
| 通鼓 tom | 有明确音高的鼓,比军鼓低沉 | 正弦 170→105Hz,衰减 0.22s |
|
|
202
|
-
| 吊镲 crash | 宽频金属噪声,尾巴很长 | 4200Hz 高通噪声乘非谐波调制,衰减 0.42s |
|
|
203
|
-
| 古筝拨弦 | 明亮拨弦,「啪」的起音加金属余韵 | Karplus-Strong,循环反馈 0.998,见示例一 |
|
|
204
|
-
| 清音吉他 | 同一类拨弦但更闷、衰更快 | Karplus-Strong 反馈 0.982,输出再过 1800Hz 低通 |
|
|
205
|
-
| 失真主音 | 沙哑持续,有攻击性 | 两路失谐锯齿叠加,tanh 硬削波,再过 900Hz 带通 |
|
|
206
|
-
| 竹笛 | 柔和持续音,带明显气声和轻微颤动 | 正弦加 0.4% 颤音,混 2400Hz 带通噪声 0.35,60ms 渐入 |
|
|
207
|
-
| 弦乐铺底 | 慢起音、宽而厚、无节奏 | 三路失谐锯齿加一个八度,1400Hz 低通,350ms 渐入 |
|
|
208
|
-
| 低频贝斯 sub | 极低频,音高明确但更像震动 | 55Hz 正弦加 110Hz 二次谐波 0.25 |
|
|
209
|
-
| 钟铃 bell | 金属非谐波,不是乐器音 | FM:载波 660Hz,调制比 3.51,调制指数从 6 指数衰到 0 |
|
|
210
|
-
| 马林巴 | 木头味,短促,有余音共鸣 | 523Hz 正弦衰 0.22,叠 4 倍频衰 0.06 与 9.2 倍频衰 0.02 |
|
|
211
|
-
| 上行扫频 riser | 亮度和音高一起往上冲 | 噪声过 400→7400Hz 扫频低通,叠 200→1100Hz 正弦 |
|
|
212
|
-
| 落重音 impact | 比底鼓更重更长,用在切点 | 90→30Hz 正弦衰 0.45,叠 2600Hz 高通噪声衰 0.3 与 240→60Hz 短瞬态 |
|
|
213
|
-
| 808 底鼓 | 长拖尾,有音高,像「轰」 | 72→30Hz 正弦,550ms 扫频,衰减 0.62s,轻微饱和 |
|
|
214
|
-
| 酸性贝斯 303 | 锯齿过强共振,每个音「呜」一下 | 锯齿过 Q=11 状态变量滤波,截止 240→2840Hz 指数衰 0.075s,见示例二 |
|
|
215
|
-
| 超级锯齿 supersaw | 多层失谐,宽、亮、有合唱感 | 七路锯齿失谐 ±0.09 半音叠加,6000Hz 低通,见示例三 |
|
|
216
|
-
| 合成拨弦 pluck | 起音很亮然后瞬间变闷 | 锯齿过 Q=3 状态变量滤波,截止 400→5600Hz 衰 0.05s |
|
|
217
|
-
| 琶音 arp | 十六分音符快速跑动,每个音很短 | 方波每个十六分一个音,滤波截止瞬开瞬关 |
|
|
218
|
-
| FM 贝斯 | 低频但起音带金属「叮」 | 载波 55Hz,调制比 2,调制指数 420 指数衰 0.035s |
|
|
219
|
-
| 门限铺底 trance gate | 和弦被节奏切成一格一格 | 锯齿和弦过 2200Hz 低通,再乘八分音符硬门限 |
|
|
220
|
-
| 芯片方波 chip | 单薄、刺耳,红白机味 | 25% 占空比方波,短音符 |
|
|
221
|
-
| 抖动贝斯 wobble | 周期性「哇—哇—」 | 锯齿过 Q=9 状态变量滤波,截止被 4Hz LFO 在 200–2400Hz 扫动 |
|
|
222
|
-
| 下坠噪声 downlifter | 亮度和音高一起往下掉 | 噪声过 7000→500Hz 反向扫频低通,叠 700→100Hz 正弦 |
|
|
223
|
-
| 侧链泵动 sidechain | 铺底每拍被压下去再弹回 | 铺底增益按 0.18 + 0.82 × (拍内相位 / 0.55) 起伏 |
|
|
224
|
-
| 四踩律动 four-on-floor | 每拍一记底鼓,二四拍军鼓 | 底鼓每拍,二四拍噪声军鼓,八分音符踩镲 |
|
|
225
|
-
|
|
226
|
-
#### 完整示例一:拨弦(古筝 / 吉他)
|
|
227
|
-
|
|
228
|
-
~~~js
|
|
229
|
-
// Karplus-Strong:噪声填一个周期长度的循环缓冲,每绕一圈丢一点高频。
|
|
230
|
-
// 这个衰减过程就是「被拨动的弦」和「一个 beep」的区别。
|
|
231
|
-
function pluckBuffer(freq, seconds, feedback) {
|
|
232
|
-
const n = Math.floor(seconds * ctx.sampleRate);
|
|
233
|
-
const buf = ctx.createBuffer(1, n, ctx.sampleRate);
|
|
234
|
-
const d = buf.getChannelData(0);
|
|
235
|
-
const period = Math.max(2, Math.round(ctx.sampleRate / freq));
|
|
236
|
-
const line = new Float32Array(period);
|
|
237
|
-
for (let i = 0; i < period; i++) line[i] = rand() * 2 - 1;
|
|
238
|
-
let prev = 0;
|
|
239
|
-
let idx = 0;
|
|
240
|
-
for (let i = 0; i < n; i++) {
|
|
241
|
-
const cur = line[idx];
|
|
242
|
-
line[idx] = 0.5 * (cur + prev) * feedback;
|
|
243
|
-
prev = cur;
|
|
244
|
-
idx = (idx + 1) % period;
|
|
245
|
-
d[i] = cur;
|
|
246
|
-
}
|
|
247
|
-
return buf;
|
|
248
|
-
}
|
|
249
|
-
// 古筝:feedback 0.998,余音长且带金属感
|
|
250
|
-
// 清音吉他:feedback 0.982,再接一个 1800Hz 低通,更闷更短
|
|
251
|
-
~~~
|
|
252
|
-
|
|
253
|
-
#### 完整示例二:酸性贝斯(滤波器扫动)
|
|
254
|
-
|
|
255
|
-
~~~js
|
|
256
|
-
// 状态变量滤波器,截止频率可以逐样本变化。
|
|
257
|
-
// biquad 做不到平滑扫频(系数按 buffer 固定),而扫频正是 acid 和 wobble 的灵魂。
|
|
258
|
-
function svf(input, cutoffAt, q) {
|
|
259
|
-
const out = new Float32Array(input.length);
|
|
260
|
-
const maxFc = ctx.sampleRate / 8; // 再高会不稳定
|
|
261
|
-
let low = 0;
|
|
262
|
-
let band = 0;
|
|
263
|
-
for (let i = 0; i < input.length; i++) {
|
|
264
|
-
const fc = Math.max(20, Math.min(maxFc, cutoffAt(i / ctx.sampleRate)));
|
|
265
|
-
const f = 2 * Math.sin((Math.PI * fc) / ctx.sampleRate);
|
|
266
|
-
const high = input[i] - low - (1 / q) * band;
|
|
267
|
-
band += f * high;
|
|
268
|
-
low += f * band;
|
|
269
|
-
out[i] = low;
|
|
270
|
-
}
|
|
271
|
-
return out;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function sawBuffer(freq, seconds) {
|
|
275
|
-
const n = Math.floor(seconds * ctx.sampleRate);
|
|
276
|
-
const buf = ctx.createBuffer(1, n, ctx.sampleRate);
|
|
277
|
-
const d = buf.getChannelData(0);
|
|
278
|
-
for (let i = 0; i < n; i++) d[i] = 2 * (((freq * i) / ctx.sampleRate) % 1) - 1;
|
|
279
|
-
return buf;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// 303:锯齿过 Q=11 的滤波器,截止从 2640Hz 在 75ms 内指数关掉
|
|
283
|
-
const raw = sawBuffer(55, 0.25).getChannelData(0);
|
|
284
|
-
const acid = svf(raw, (t) => 240 + 2400 * Math.exp(-t / 0.075), 11);
|
|
285
|
-
~~~
|
|
286
|
-
|
|
287
|
-
#### 完整示例三:超级锯齿(宽亮主音)
|
|
288
|
-
|
|
289
|
-
~~~js
|
|
290
|
-
// 七路失谐锯齿叠加,这是 trance / EDM 那种「宽」的来源。
|
|
291
|
-
const detunes = [-0.09, -0.06, -0.03, 0, 0.03, 0.06, 0.09];
|
|
292
|
-
const mix = new Float32Array(Math.floor(duration * ctx.sampleRate));
|
|
293
|
-
for (const d of detunes) {
|
|
294
|
-
const v = sawBuffer(261.6 * Math.pow(2, d / 12), duration).getChannelData(0);
|
|
295
|
-
for (let i = 0; i < mix.length; i++) mix[i] += v[i] / detunes.length;
|
|
296
|
-
}
|
|
297
|
-
// 再接 6000Hz 低通削掉刺耳高频
|
|
298
|
-
~~~
|
|
299
|
-
|
|
300
|
-
上面的示例只演示关键部分:master、sawBuffer 这些辅助函数要在你自己的代码里完整定义。
|
|
224
|
+
#### 音色配方
|
|
301
225
|
|
|
302
|
-
|
|
303
|
-
|
|
226
|
+
28 条配方和三个完整代码示例在 get_reference("timbre"),写 score_js 前先取。
|
|
227
|
+
里面含一条硬禁令:任何音色都不许做低频扫频(正弦从一两百赫兹滑到三四十赫兹),
|
|
228
|
+
要重音就给固定频率的低正弦加鼓皮噪声,让包络做冲击。
|
|
304
229
|
|
|
305
230
|
### 不要自己造轮子
|
|
306
231
|
|
package/index.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Routes are profile-wide, so only the root row registers them; the preset row is
|
|
11
11
|
* mounted per preset and would otherwise collide on the same prefix.
|
|
12
12
|
*/
|
|
13
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
14
14
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
|
|
@@ -85,6 +85,66 @@ function resolveExportDir(requested, fallbackRoot) {
|
|
|
85
85
|
return join(OUTPUT_DIR, 'exports');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* 让动画在预览框里按原比例缩放到完整可见。
|
|
90
|
+
*
|
|
91
|
+
* 生成的动画多半把画布写成固定像素(`canvas{width:1280px;height:720px}`),而卡片
|
|
92
|
+
* 里的预览框只有几百像素宽,于是 iframe 内部出现滚动条、画面被切掉。
|
|
93
|
+
*
|
|
94
|
+
* 补的这层样式只影响显示尺寸:canvas 的 width/height 属性(像素缓冲区)不动,
|
|
95
|
+
* 所以 recordAnimation 走 captureStream 导出时拿到的仍是原始分辨率。
|
|
96
|
+
*
|
|
97
|
+
* 用 !important 是因为动画自己那条 `canvas{width:1280px}` 和本规则同等特异性,
|
|
98
|
+
* 而它的 <style> 可能排在后面;不压制的话自适应会失效。
|
|
99
|
+
*/
|
|
100
|
+
const FIT_STYLE = [
|
|
101
|
+
'<style data-creation-mode-fit>',
|
|
102
|
+
'html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#05070b}',
|
|
103
|
+
'body{display:flex;align-items:center;justify-content:center}',
|
|
104
|
+
'canvas{max-width:100%!important;max-height:100%!important;',
|
|
105
|
+
'width:auto!important;height:auto!important;object-fit:contain}',
|
|
106
|
+
'</style>',
|
|
107
|
+
].join('');
|
|
108
|
+
|
|
109
|
+
function fitToFrame(html) {
|
|
110
|
+
if (/<\/head>/i.test(html)) return html.replace(/<\/head>/i, `${FIT_STYLE}</head>`);
|
|
111
|
+
if (/<body[^>]*>/i.test(html)) return html.replace(/<body[^>]*>/i, (tag) => `${tag}${FIT_STYLE}`);
|
|
112
|
+
return FIT_STYLE + html;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 导出目录里记录「本插件写过哪些成片」的清单,只用来删自己写过的文件。 */
|
|
116
|
+
const EXPORT_INDEX = '.creation-mode-exports.json';
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 只留成品:导出目录里也只保留这一次的成片。
|
|
120
|
+
*
|
|
121
|
+
* 只删清单里记过的、由本插件自己写出来的文件,用户放在同一目录的其他视频不会被碰。
|
|
122
|
+
* 清单本身用点号开头,导出卡片里不会显示它。
|
|
123
|
+
*
|
|
124
|
+
* @returns 被删掉的旧成片文件名。
|
|
125
|
+
*/
|
|
126
|
+
function pruneExports(dir, keepName) {
|
|
127
|
+
const index = join(dir, EXPORT_INDEX);
|
|
128
|
+
let previous = [];
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(readFileSync(index, 'utf8'));
|
|
131
|
+
if (Array.isArray(parsed?.files)) previous = parsed.files.filter((n) => typeof n === 'string');
|
|
132
|
+
} catch { /* 第一次导出:还没有清单 */ }
|
|
133
|
+
|
|
134
|
+
const removed = [];
|
|
135
|
+
for (const name of previous) {
|
|
136
|
+
if (name === keepName) continue;
|
|
137
|
+
const file = join(dir, name);
|
|
138
|
+
if (!existsSync(file)) continue;
|
|
139
|
+
try { unlinkSync(file); removed.push(name); } catch { /* 删不掉就留着 */ }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
writeFileSync(index, JSON.stringify({ files: [keepName] }, null, 2), 'utf8');
|
|
144
|
+
} catch { /* 清单写不了不影响导出本身 */ }
|
|
145
|
+
return removed;
|
|
146
|
+
}
|
|
147
|
+
|
|
88
148
|
function readBody(req, limit) {
|
|
89
149
|
return new Promise((resolvePromise, reject) => {
|
|
90
150
|
const chunks = [];
|
|
@@ -160,14 +220,42 @@ export function apply(ctx, config = {}) {
|
|
|
160
220
|
mkdirSync(dir, { recursive: true });
|
|
161
221
|
const file = join(dir, name);
|
|
162
222
|
writeFileSync(file, body);
|
|
163
|
-
|
|
223
|
+
const removed = pruneExports(dir, name);
|
|
224
|
+
trace(`export ${name} ${body.length}B -> ${file}${removed.length ? `(清理旧成片 ${removed.length} 个)` : ''}`);
|
|
164
225
|
return send(res, 200, JSON.stringify({
|
|
165
226
|
path: file,
|
|
166
227
|
dir,
|
|
167
228
|
bytes: body.length,
|
|
229
|
+
removed,
|
|
168
230
|
}), 'application/json; charset=utf-8');
|
|
169
231
|
}
|
|
170
232
|
|
|
233
|
+
// 样式库面板用的一次性目录快照:只给已上架的 193 种,含参考图地址。
|
|
234
|
+
if (url.startsWith(`${PREFIX}/styles`) && req.method === 'GET') {
|
|
235
|
+
const { series, styles } = catalog();
|
|
236
|
+
const body = JSON.stringify({
|
|
237
|
+
series: Object.entries(series || {}).map(([id, name]) => ({ id, name })),
|
|
238
|
+
styles: styles.map((style) => ({
|
|
239
|
+
id: style.id,
|
|
240
|
+
name: style.name,
|
|
241
|
+
en: style.en || '',
|
|
242
|
+
series: style.series,
|
|
243
|
+
seriesName: style.seriesName,
|
|
244
|
+
category: style.category,
|
|
245
|
+
description: style.desc,
|
|
246
|
+
fit: style.fit,
|
|
247
|
+
tags: style.tags || [],
|
|
248
|
+
stillUrl: `${PREFIX}/still/${style.id}`,
|
|
249
|
+
})),
|
|
250
|
+
});
|
|
251
|
+
res.writeHead(200, {
|
|
252
|
+
'content-type': 'application/json; charset=utf-8',
|
|
253
|
+
'content-length': Buffer.byteLength(body),
|
|
254
|
+
'cache-control': 'no-store',
|
|
255
|
+
});
|
|
256
|
+
return res.end(body);
|
|
257
|
+
}
|
|
258
|
+
|
|
171
259
|
if (url.startsWith(`${PREFIX}/export-dir`) && req.method === 'GET') {
|
|
172
260
|
const query = new URL(url, 'http://localhost').searchParams;
|
|
173
261
|
const fallbackRoot = scoped.get('workingDirectory')?.defaultDirectory;
|
|
@@ -209,14 +297,36 @@ export function apply(ctx, config = {}) {
|
|
|
209
297
|
if (url.startsWith(`${PREFIX}/animation/`)) {
|
|
210
298
|
const id = safeId(url.slice(`${PREFIX}/animation/`.length));
|
|
211
299
|
const file = id ? join(OUTPUT_DIR, `${id}.html`) : '';
|
|
212
|
-
|
|
300
|
+
// 旧稿会被清理(只留最新一版),所以历史卡片点开会落到这里:给一句话,
|
|
301
|
+
// 而不是让 iframe 里空着显示 not found。
|
|
302
|
+
if (!file || !existsSync(file)) {
|
|
303
|
+
const gone = [
|
|
304
|
+
'<!doctype html><meta charset="utf-8">',
|
|
305
|
+
'<body style="margin:0;display:flex;align-items:center;justify-content:center;',
|
|
306
|
+
'height:100vh;background:#111;color:#888;',
|
|
307
|
+
'font:14px/1.6 system-ui,sans-serif;text-align:center">',
|
|
308
|
+
'<div>这一版已经被清理<br><span style="font-size:12px;opacity:.6">',
|
|
309
|
+
'工作区只保留最新的成品,请看对话里最新那张卡片</span></div></body>',
|
|
310
|
+
].join('');
|
|
311
|
+
res.writeHead(404, {
|
|
312
|
+
'content-type': 'text/html; charset=utf-8',
|
|
313
|
+
'content-length': Buffer.byteLength(gone),
|
|
314
|
+
'cache-control': 'no-store',
|
|
315
|
+
});
|
|
316
|
+
return res.end(gone);
|
|
317
|
+
}
|
|
213
318
|
const html = readFileSync(file, 'utf8');
|
|
319
|
+
// 生成出来的动画常常把画布写死成 1280×720 或 1920×1080,塞进卡片那个
|
|
320
|
+
// 几百像素宽的预览框就会溢出、出滚动条、画面看不全。这里统一补一层
|
|
321
|
+
// 自适应样式:画布按原比例缩到框里,不再滚动。
|
|
322
|
+
// 只改显示尺寸,canvas 的像素缓冲区不动,导出仍然是原始分辨率。
|
|
323
|
+
const fitted = fitToFrame(html);
|
|
214
324
|
res.writeHead(200, {
|
|
215
325
|
'content-type': 'text/html; charset=utf-8',
|
|
216
|
-
'content-length': Buffer.byteLength(
|
|
326
|
+
'content-length': Buffer.byteLength(fitted),
|
|
217
327
|
'cache-control': 'no-store',
|
|
218
328
|
});
|
|
219
|
-
return res.end(
|
|
329
|
+
return res.end(fitted);
|
|
220
330
|
}
|
|
221
331
|
|
|
222
332
|
return send(res, 404, 'not found');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-creation-mode",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "创作模式 / Creation Mode:193 种代码动画风格,在 DeepSeek Harness 对话里直接出片。",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"cordis.patch.yml",
|
|
37
37
|
"styles.json",
|
|
38
38
|
"stills",
|
|
39
|
+
"references",
|
|
39
40
|
"locale/*.json",
|
|
40
41
|
"icon.svg"
|
|
41
42
|
],
|
|
@@ -46,7 +47,9 @@
|
|
|
46
47
|
"client": {
|
|
47
48
|
"platform": "web",
|
|
48
49
|
"immediately": true,
|
|
49
|
-
"inject": [
|
|
50
|
+
"inject": [
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
52
|
+
]
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
}
|