@ninkch/dsh-infinite-canvas 0.19.5 → 0.19.6

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/README.md CHANGED
@@ -96,7 +96,7 @@ Assets are shared with the canvas: the characters, trees, furniture and building
96
96
 
97
97
  Full walkthrough — asset tabs, gizmo, keyboard map, shots and export: **[User guide → 3D Director Stage](./docs/guide.en.md#8-3d-director-stage)**.
98
98
 
99
- ## Craft page: director / genre / delivery form / look
99
+ ## Craft page: director / genre / spec / look
100
100
 
101
101
  The third tab of the right sidebar decides **how the whole piece is shot**. It is not a prompt template — it is a binding constraint that gets composed into every script, storyboard, image and video generation. **There are no dropdowns on the page**: every option is an icon tile; click to select, click again to clear.
102
102
 
@@ -106,12 +106,12 @@ The third tab of the right sidebar decides **how the whole piece is shot**. It i
106
106
  |---|---|---|---|
107
107
  | **Director** | 24 | Camera language, lighting, cutting rhythm, style parameters | It constrains the shot's camera fields, the keyframe, and the video's camera move at once — the least replaceable decision in the whole piece |
108
108
  | **Genre** | 21 narrative / 10 sector | Pressure mechanics, audience payoff, scene granules, episode hooks | The same "revenge" premise is fragmented neon under Wong Kar-wai and hard-lit slow motion under Tsui Hark; folding them together poisons both |
109
- | **Delivery form** | 8 | Structure blocks, ASL floor, rhythm, quality bar | It decides "short drama vs cinematic texture vs commercial", which is orthogonal to genre |
109
+ | **Spec** | 8 | Structure blocks, ASL floor, rhythm, quality bar | It decides "short drama vs cinematic texture vs commercial", which is orthogonal to genre |
110
110
  | **Look** | 9 | Imaging base, material, render texture | 2D thick paint / guoman 3D / game CG are imaging domains, not directors; mixing them into the director layer makes "how to cut" fight "what it looks like" |
111
111
 
112
112
  **Directors (24)**: Chinese 8 (Wong Kar-wai, Zhang Yimou, Hou Hsiao-hsien, Bi Gan, Tsui Hark, Lu Yang, Yuen Woo-ping, Wu Ershan) · Japan/Korea 2 (Kurosawa, Park Chan-wook) · Western 14 (Spielberg, Hitchcock, Kubrick, Scorsese, Fellini, Bergman, Tarkovsky, Nolan, Villeneuve, Fincher, Refn, Malick, Michael Mann, Coen Brothers).
113
113
 
114
- **Delivery forms (8)**: Short drama · Cinematic texture · Commercial · Documentary · Music video · Trailer · Explainer · Interview.
114
+ **Specs (8)**: Short drama · Cinematic texture · Commercial · Documentary · Music video · Trailer · Explainer · Interview.
115
115
 
116
116
  - A **short drama** is a self-contained 60–180 s unit and **covers both single episodes and multi-episode series** — a series is just episode after episode, so there is no separate "series" form.
117
117
  - **Cinematic texture** is a *texture* spec, not a feature-length one: it constrains how each shot must be made to read as cinema (ASL floor 1.5 s, 24 fps, motivated light, real materials), and its duration tiers stop at 30–300 s. The reason is practical — **a video model emits only 3–8 s per clip** (5 s by default here), so "a 120-minute film" can only be hundreds of clips stitched together.
@@ -119,7 +119,7 @@ The third tab of the right sidebar decides **how the whole piece is shot**. It i
119
119
 
120
120
  ### Aspect ratio is not decided here
121
121
 
122
- Vertical, horizontal, square or widescreen all work with any delivery form — **aspect and resolution are chosen on the video node at generation time**. Vertical delivery is common for short drama, but that is a distribution choice, not a craft spec; **a short drama can equally be shot horizontal and to a cinematic standard**.
122
+ Vertical, horizontal, square or widescreen all work with any spec — **aspect and resolution are chosen on the video node at generation time**. Vertical delivery is common for short drama, but that is a distribution choice, not a craft spec; **a short drama can equally be shot horizontal and to a cinematic standard**.
123
123
 
124
124
  ### Where it is injected
125
125
 
package/README.zh-CN.md CHANGED
@@ -96,7 +96,7 @@
96
96
 
97
97
  完整操作(资产页签、操作轴、快捷键表、分镜、导出):**[操作手册 → 3D 导演台](./docs/guide.zh-CN.md#8-3d-导演台)**。
98
98
 
99
- ## 技能页:导演 / 题材 / 类型规格 / 影调
99
+ ## 技能页:导演 / 题材 / 规格 / 影调
100
100
 
101
101
  右栏第三个页签「技能」决定**整部片怎么拍**。它不是提示词模板,而是会被自动拼进每一次剧本、分镜、图片、视频生成的硬约束。**页面里没有任何下拉框**,全部是图标瓦片,点一下选中、再点一下取消。
102
102
 
@@ -106,12 +106,12 @@
106
106
  |---|---|---|---|
107
107
  | **导演** | 24 | 镜头语言、灯光色彩、剪辑节奏、风格参数 | 它同时约束分镜的机位字段、图片关键帧、视频运镜——整部片最不可替换的决策 |
108
108
  | **题材** | 叙事 21 / 行业 10 | 压力机制、观众回报、场面颗粒、集尾钩子 | 同一个「复仇打脸」,王家卫拍是抽帧暧昧,徐克拍是凌厉升格;混进导演层会互相污染 |
109
- | **类型规格** | 8 | 结构块、平均镜长下限、节奏、质量档 | 它决定「这是短剧、电影质感还是商业片」,与题材正交 |
109
+ | **规格** | 8 | 结构块、平均镜长下限、节奏、质量档 | 它决定「这是短剧、电影质感还是商业片」,与题材正交 |
110
110
  | **影调** | 9 | 成像底层、材质、渲染质感 | 2D 厚涂 / 国漫 3D / 游戏 CG 是成像域,不是导演;塞进导演会让「怎么剪」和「长什么样」打架 |
111
111
 
112
112
  **导演(24 位)**:华语 8(王家卫、张艺谋、侯孝贤、毕赣、徐克、路阳、袁和平、乌尔善)· 日韩 2(黑泽明、朴赞郁)· 欧美 14(斯皮尔伯格、希区柯克、库布里克、斯科塞斯、费里尼、伯格曼、塔可夫斯基、诺兰、维伦纽瓦、芬奇、雷弗恩、马力克、迈克尔·曼、科恩兄弟)。
113
113
 
114
- **类型规格(8 种)**:短剧 · 电影质感 · 商业片 · 纪实 · 音乐视频 · 预告 · 解说 · 访谈。
114
+ **规格(8 种)**:短剧 · 电影质感 · 商业片 · 纪实 · 音乐视频 · 预告 · 解说 · 访谈。
115
115
 
116
116
  - **短剧**是单集 60–180 秒的自足单元,**同时覆盖单集与连续多集**——一部剧就是一集一集往下做,不需要单独的「连续剧」规格。
117
117
  - **电影质感**是**质感规格,不是长片规格**:它约束每一镜怎么做才像电影(ASL 1.5 秒以上、24 帧、动机光、真材质),成片长度档只到 30–300 秒。原因很实际——**AI 视频模型单次只出 3–8 秒**(本插件默认 5 秒),所谓「一部 120 分钟的电影」只能是几百上千个片段接起来。
@@ -119,14 +119,14 @@
119
119
 
120
120
  ### 画幅不在技能里决定
121
121
 
122
- 横屏、竖屏、方屏、宽银幕都能用任何类型规格——**画幅与分辨率是视频节点生成时的选项**。短剧竖屏投放很常见,但那是发行选择,不是创作规格;**短剧同样可以横屏、可以按电影标准拍**。
122
+ 横屏、竖屏、方屏、宽银幕都能用任何规格——**画幅与分辨率是视频节点生成时的选项**。短剧竖屏投放很常见,但那是发行选择,不是创作规格;**短剧同样可以横屏、可以按电影标准拍**。
123
123
 
124
124
  ### 注入到哪
125
125
 
126
126
  | 节点 / 能力 | 拿到的内容 |
127
127
  |---|---|
128
- | 剧本节点 | 题材卡 + 导演的叙事/节奏/表演节 + 类型规格的结构块 |
129
- | 分镜表节点 | 导演的镜头语言/灯光/风格参数/拍摄清单 + 类型规格 + 运镜词典 |
128
+ | 剧本节点 | 题材卡 + 导演的叙事/节奏/表演节 + 规格的结构块 |
129
+ | 分镜表节点 | 导演的镜头语言/灯光/风格参数/拍摄清单 + 规格 + 运镜词典 |
130
130
  | 图片节点 | 「风格增强锁」:风格参数 + 影调成像材质 + 题材颗粒 + 三份负向词 |
131
131
  | 视频节点 | 同上(有上游分镜表时也追加,不打断结构化字段) |
132
132
  | 角色/场景/道具设定图 | 导演的灯光色彩 + 影调卡 |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: canvas-director
3
- description: 画布导演套件:24 位导演透镜(管镜头语言)、21 个叙事题材 + 10 个行业题材(管压力机制与观众回报)、8 种类型规格(管结构块/ASL/节奏/质量档;画幅不归它管)、9 种影调质感(管成像与材质)、运镜词典与打斗节奏。在无限画布里写任何剧本、分镜、图片提示词、视频提示词之前加载本技能,用 canvas_skill_pack 取对应切片。Load before writing any script, storyboard, image or video prompt on the canvas.
3
+ description: 画布导演套件:24 位导演透镜(管镜头语言)、21 个叙事题材 + 10 个行业题材(管压力机制与观众回报)、8 种规格(管结构块/ASL/节奏/质量档;画幅不归它管)、9 种影调质感(管成像与材质)、运镜词典与打斗节奏。在无限画布里写任何剧本、分镜、图片提示词、视频提示词之前加载本技能,用 canvas_skill_pack 取对应切片。Load before writing any script, storyboard, image or video prompt on the canvas.
4
4
  ---
5
5
 
6
6
  # 画布导演套件
@@ -14,9 +14,9 @@ description: 画布导演套件:24 位导演透镜(管镜头语言)、21
14
14
  | `canvas-studio` | 画布操作:建项目、写节点、触发生成 | 任何时候先读 |
15
15
  | `canvas-craft` | 创作方法论:剧本四要素、分镜六要素、提示词规范 | 写剧本 / 拆分镜前 |
16
16
  | `canvas-layout` | 节点布局纪律 | 批量建节点前 |
17
- | **`canvas-director`(本技能)** | **导演透镜 / 题材 / 类型规格 / 影调 / 运镜** | **写任何提示词前** |
17
+ | **`canvas-director`(本技能)** | **导演透镜 / 题材 / 规格 / 影调 / 运镜** | **写任何提示词前** |
18
18
 
19
- **优先级(冲突时按此裁决)**:`用户显式指令 > 导演透镜 > 类型规格(仅单镜时长与结构上它压过透镜)> 题材 > 影调 > 技能默认`
19
+ **优先级(冲突时按此裁决)**:`用户显式指令 > 导演透镜 > 规格(仅单镜时长与结构上它压过透镜)> 题材 > 影调 > 技能默认`
20
20
 
21
21
  ---
22
22
 
@@ -25,26 +25,26 @@ description: 画布导演套件:24 位导演透镜(管镜头语言)、21
25
25
  ```
26
26
  L1 导演透镜 单选 24 → 镜头语言 / 灯光色彩 / 剪辑节奏 / 风格参数 「怎么拍」
27
27
  L2 题材标签 多选 → 压力机制 / 观众回报 / 场面颗粒 / 集尾钩子 「讲什么」
28
- 叙事题材 21(叙事类类型规格时用)
28
+ 叙事题材 21(叙事类规格时用)
29
29
  行业题材 10(宣传/广告/产品类时用)
30
- L3 类型规格 单选 8 → 结构块 / ASL 下限 / 节奏 / 质量档 「按什么规格交付」
30
+ L3 规格 单选 8 → 结构块 / ASL 下限 / 节奏 / 质量档 「按什么规格交付」
31
31
  L4 影调质感 单选 9 → 成像底层 / 材质 / 渲染质感(默认跟随导演) 「长什么样」
32
32
  ```
33
33
 
34
- **八种类型规格**:短剧 · 电影质感 · 商业片 · 纪实 · 音乐视频 · 预告 · 解说 · 访谈。
34
+ **八种规格**:短剧 · 电影质感 · 商业片 · 纪实 · 音乐视频 · 预告 · 解说 · 访谈。
35
35
 
36
36
  - **短剧**=单集 60–180 秒的自足单元,**同时覆盖单集与连续多集**(一部剧就是一集一集往下做,不需要单独的「连续剧」规格)。质量要求按电影标准,不按短视频标准。
37
37
  - **电影质感**=**质感规格,不是长片规格**。它约束每一镜怎么做才像电影(动机光、真材质、成立景深、24 帧),成片长度档只到 30–300 秒——因为 AI 单次只出 3–8 秒,写一个 120 分钟的档是假的。要长片就按集按段推进,本规格负责每段都过电影线。
38
38
  - **商业片**=宣传片 / 广告片 / 产品片合并(相近需求,靠段位配比内部分流,不再拆三张)。
39
- - **画幅不归任何一层。** 横屏、竖屏、方屏、宽银幕都能配任何类型规格;**画幅与分辨率在视频节点生成时选**。短剧竖屏投放是发行选择,不是创作规格——短剧同样可以横屏、可以按电影标准拍。
39
+ - **画幅不归任何一层。** 横屏、竖屏、方屏、宽银幕都能配任何规格;**画幅与分辨率在视频节点生成时选**。短剧竖屏投放是发行选择,不是创作规格——短剧同样可以横屏、可以按电影标准拍。
40
40
 
41
41
  为什么这样分:
42
42
 
43
43
  - **导演独占第一层**:它同时约束分镜的 camera 字段、图片关键帧、视频运镜——是整部片最不可替换的决策。
44
44
  - **题材与导演分开**:同一个「复仇打脸」,王家卫拍是抽帧暧昧,徐克拍是凌厉升格。混一层会互相污染。
45
- - **类型规格独立**:它决定结构块、ASL 下限与质量档,和题材是正交的两个轴。**画幅不属于任何一层**——横竖屏是视频节点生成时的选项,不是创作规格。短剧可以竖屏也可以横屏,都要按电影标准拍。
45
+ - **规格独立**:它决定结构块、ASL 下限与质量档,和题材是正交的两个轴。**画幅不属于任何一层**——横竖屏是视频节点生成时的选项,不是创作规格。短剧可以竖屏也可以横屏,都要按电影标准拍。
46
46
  - **影调独立**:2D 厚涂 / 国漫 3D / 游戏 CG 是**成像域**,不是导演。塞进导演会让「怎么剪」和「长什么样」打架。
47
- - **运镜不独立成层**:运镜是导演的语言,独立成层会出现「王家卫 + 摇镜」的自相矛盾。它作为卡内数据存在(`moves/`),只被透镜与类型规格引用。
47
+ - **运镜不独立成层**:运镜是导演的语言,独立成层会出现「王家卫 + 摇镜」的自相矛盾。它作为卡内数据存在(`moves/`),只被透镜与规格引用。
48
48
 
49
49
  ---
50
50
 
@@ -57,7 +57,7 @@ assets/canvas-director/
57
57
  ├── lenses/ 24 份导演透镜(每份 14 小节 + 风格参数 YAML)
58
58
  ├── genres/ 21 份叙事题材卡
59
59
  ├── industries/ 10 份行业题材卡
60
- ├── forms/ 8 份类型规格卡(含 spec 数值块:durations / asl_floor_s / asl_target_s / platforms)
60
+ ├── forms/ 8 份规格卡(含 spec 数值块:durations / asl_floor_s / asl_target_s / platforms)
61
61
  ├── looks/ 9 份影调质感卡
62
62
  └── moves/ camera-moves.md 运镜词典 / fight-rhythm.md 打斗节奏
63
63
  ```
@@ -71,7 +71,7 @@ assets/canvas-director/
71
71
  **`canvas_list_skills`** —— 开始画布工作前先调它。它会告诉你:
72
72
  - 本插件提供的 4 份技能及各自什么时候用;
73
73
  - 四层技能包的完整清单(每层有多少张、都有哪些 id 与名称、各自一句话定位);
74
- - 当前项目已经选定的配置(导演 / 题材 / 类型规格 / 影调)。
74
+ - 当前项目已经选定的配置(导演 / 题材 / 规格 / 影调)。
75
75
 
76
76
  **`canvas_skill_pack`** —— 真正取切片:
77
77
 
@@ -100,8 +100,8 @@ canvas_skill_pack({ projectId, kind: 'prompt', lens: 'wong_kar_wai', genres: ['r
100
100
 
101
101
  | kind | 内容 | 典型字节 |
102
102
  |---|---|---|
103
- | `script` | 题材卡全部 + 透镜的叙事/节奏/表演节 + 类型规格的结构块 | ~9–12KB |
104
- | `storyboard` | 透镜的镜头语言/灯光/风格参数/拍摄清单 + 类型规格卡 + 运镜词典 | ~12–16KB |
103
+ | `script` | 题材卡全部 + 透镜的叙事/节奏/表演节 + 规格的结构块 | ~9–12KB |
104
+ | `storyboard` | 透镜的镜头语言/灯光/风格参数/拍摄清单 + 规格卡 + 运镜词典 | ~12–16KB |
105
105
  | `prompt` | 透镜的风格参数/灯光/提示词模板 + 影调卡 + 该类型的 ASL 与负向词 | ~8–12KB |
106
106
  | `assets` | 透镜的风格参数/灯光 + 影调卡 | ~4–6KB |
107
107
 
@@ -121,7 +121,7 @@ canvas_skill_pack({ projectId, kind: 'prompt', lens: 'wong_kar_wai', genres: ['r
121
121
 
122
122
  视觉命题必须具体可测:"男孩随着房间变得更审判而变小",不是"孤独与命运"。写不成可拍摄的变化,就还不是命题。
123
123
 
124
- 非叙事片(选「商业片」类型规格:宣传片/广告片/产品片)**跳过本步**:用类型规格卡里的「观众契约」替代,等价物是"主张 + 那一个可见的证明"。不要为一只口红推导潜文本。
124
+ 非叙事片(选「商业片」规格:宣传片/广告片/产品片)**跳过本步**:用规格卡里的「观众契约」替代,等价物是"主张 + 那一个可见的证明"。不要为一只口红推导潜文本。
125
125
 
126
126
  ### 3. 节拍表
127
127
  按**压力变化**分节拍,不按机位分。每拍写:叙事功能、进出状态、视觉动作、压力差、可能的镜头族(建立/关系/特写/插入/反应/过渡/余波/主观/揭示)。
@@ -48,7 +48,7 @@ spec:
48
48
  - **慎借**:柏格曼之外的「作者性」透镜。库布里克式对称会让受访者变成标本;张艺谋式色块会让背景抢戏。
49
49
  - **基本不要借**:王家卫式手持与前景遮挡(挡住嘴和眼睛等于毁掉访谈)、雷弗恩式仪式构图、毕赣式长镜游走(观众会以为摄影机要跑掉)。
50
50
  - **必须放弃的透镜参数**:任何要求「手持晃动」或「主体偏心到画幅边缘」的设定。访谈的第一原则是**脸在可读位置且稳定**。
51
- - **唯一的例外**:如果这部访谈片本身就是「风格化的肖像纪录片」,可以挂一个强透镜——但那时它更接近**电影**或**纪实**规格,应该换类型规格,而不是在访谈规格里硬套。
51
+ - **唯一的例外**:如果这部访谈片本身就是「风格化的肖像纪录片」,可以挂一个强透镜——但那时它更接近**电影**或**纪实**规格,应该换规格,而不是在访谈规格里硬套。
52
52
 
53
53
  <!-- slice:prompt -->
54
54
  ## 类型级负向词
@@ -462,7 +462,7 @@
462
462
  "file": "short-drama.md",
463
463
  "glyph": "short-drama",
464
464
  "oneliner": "单集或多集的短剧:60-180 秒自足单元,10 秒立钩;按电影标准拍,不是低配",
465
- "group": "类型"
465
+ "group": "规格"
466
466
  },
467
467
  {
468
468
  "id": "cinematic",
@@ -470,7 +470,7 @@
470
470
  "file": "cinematic.md",
471
471
  "glyph": "cinematic",
472
472
  "oneliner": "电影质感规格(不是长片):每镜都要过电影线——动机光、真材质、成立景深",
473
- "group": "类型"
473
+ "group": "规格"
474
474
  },
475
475
  {
476
476
  "id": "commercial",
@@ -478,7 +478,7 @@
478
478
  "file": "commercial.md",
479
479
  "glyph": "commercial",
480
480
  "oneliner": "宣传片 / 广告片 / 产品片合一:一条片只承诺一件事,证据必须看得见",
481
- "group": "类型"
481
+ "group": "规格"
482
482
  },
483
483
  {
484
484
  "id": "documentary",
@@ -486,7 +486,7 @@
486
486
  "file": "documentary.md",
487
487
  "glyph": "documentary",
488
488
  "oneliner": "可信优先于好看:现场光、真环境音、允许承认不知道",
489
- "group": "类型"
489
+ "group": "规格"
490
490
  },
491
491
  {
492
492
  "id": "music-video",
@@ -494,7 +494,7 @@
494
494
  "file": "music-video.md",
495
495
  "glyph": "music-video",
496
496
  "oneliner": "节奏单位是拍不是秒:切点对齐 BPM,全片必有一次「停」",
497
- "group": "类型"
497
+ "group": "规格"
498
498
  },
499
499
  {
500
500
  "id": "trailer",
@@ -502,7 +502,7 @@
502
502
  "file": "trailer.md",
503
503
  "glyph": "trailer",
504
504
  "oneliner": "设局—加压—爆发—按钮:只给一个信息,藏住结局",
505
- "group": "类型"
505
+ "group": "规格"
506
506
  },
507
507
  {
508
508
  "id": "explainer",
@@ -510,7 +510,7 @@
510
510
  "file": "explainer.md",
511
511
  "glyph": "explainer",
512
512
  "oneliner": "一块屏一条信息:语速 4.5-6.5 字/秒,画面必须与旁白同步",
513
- "group": "类型"
513
+ "group": "规格"
514
514
  },
515
515
  {
516
516
  "id": "interview",
@@ -518,7 +518,7 @@
518
518
  "file": "interview.md",
519
519
  "glyph": "interview",
520
520
  "oneliner": "脸与话是全部资产:ASL 8-20 秒,犹豫和沉默都要留着",
521
- "group": "类型"
521
+ "group": "规格"
522
522
  }
523
523
  ],
524
524
  "looks": [
@@ -9,7 +9,7 @@
9
9
  * 标记缺失时回落到纯标题匹配(保证「复制过来就能用」的鲁棒性)。
10
10
  *
11
11
  * 术语:kind = 生成能力(script / storyboard / prompt / assets),
12
- * layer = 数据层(lens 导演 / genre 叙事题材 / industry 行业题材 / form 类型规格 / look 影调 / moves 运镜)。
12
+ * layer = 数据层(lens 导演 / genre 叙事题材 / industry 行业题材 / form 规格 / look 影调 / moves 运镜)。
13
13
  */
14
14
 
15
15
  /** Markdown 里 slice 标记的格式:`<!-- slice:story prompt -->`(一个标记可声明归属多个能力)。 */
@@ -49,7 +49,7 @@ export const SECTION_DEFAULT_SLICE = {
49
49
  '常见承诺': 'story prompt',
50
50
  '禁项': 'prompt',
51
51
  '参考镜头': 'prompt',
52
- // 类型规格卡
52
+ // 规格卡
53
53
  '交付规格': 'story shot prompt',
54
54
  '结构块': 'story',
55
55
  '这个类型的观众契约': 'story shot',
@@ -288,6 +288,9 @@ export function buildPackText(kind, items, opts) {
288
288
  blocks.push({ title: layerLabel(it.layer, zh) + ':' + name + (meta.oneliner ? ' —— ' + meta.oneliner : ''), body });
289
289
  used.push({ layer: it.layer, id: meta.id || '', bytes: utf8Bytes(body) });
290
290
  }
291
+ // 一条切片都没切出来 → 返回空:否则调用方只会拿到一段光秃秃的头部
292
+ // (典型场景:kind=assets 时某层没有 assets 小节,不该产出「只有标题」的空壳)。
293
+ if (!blocks.length) return { text: '', bytes: 0, used: [] };
291
294
  const head = o.head || '';
292
295
  const r = composePackText(blocks, { head });
293
296
  return { text: r.text, bytes: r.bytes, used };
@@ -295,8 +298,8 @@ export function buildPackText(kind, items, opts) {
295
298
 
296
299
  /** 层的中文标签。 */
297
300
  export function layerLabel(layer, zh) {
298
- const ZH = { lens: '导演透镜', genre: '题材', industry: '行业题材', form: '类型规格', look: '影调', moves: '运镜与节奏' };
299
- const EN = { lens: 'Director lens', genre: 'Genre', industry: 'Industry', form: 'Delivery form', look: 'Look', moves: 'Camera & rhythm' };
301
+ const ZH = { lens: '导演透镜', genre: '题材', industry: '行业题材', form: '规格', look: '影调', moves: '运镜与节奏' };
302
+ const EN = { lens: 'Director lens', genre: 'Genre', industry: 'Industry', form: 'Spec', look: 'Look', moves: 'Camera & rhythm' };
300
303
  const m = zh === false ? EN : ZH;
301
304
  return m[layer] || layer;
302
305
  }
@@ -305,10 +308,7 @@ export function layerLabel(layer, zh) {
305
308
  export const DEFAULT_PROFILE = {
306
309
  lens: '',
307
310
  genres: [],
308
- form: 'short-drama',
309
- aspect: '',
310
- targetSec: 0,
311
- platform: '',
311
+ form: '',
312
312
  look: '',
313
313
  genreMode: 'auto',
314
314
  };
@@ -328,14 +328,10 @@ export function normalizeProfile(raw, allowed) {
328
328
  .filter((x) => !Array.isArray(a.genres) || !a.genres.length || a.genres.includes(x))
329
329
  .slice(0, 12)
330
330
  : [];
331
- const sec = Number(b.targetSec);
332
331
  return {
333
332
  lens: pick(b.lens, a.lenses),
334
333
  genres,
335
334
  form: pick(b.form, a.forms) || DEFAULT_PROFILE.form,
336
- aspect: pick(b.aspect, a.aspects),
337
- targetSec: Number.isFinite(sec) && sec > 0 ? Math.round(sec) : 0,
338
- platform: pick(b.platform, a.platforms),
339
335
  look: pick(b.look, a.looks),
340
336
  genreMode: (b.genreMode === 'narrative' || b.genreMode === 'industry') ? b.genreMode : 'auto',
341
337
  };
@@ -17,7 +17,7 @@ description: 用无限画布(Infinite Canvas)做短剧/视频创作时使用
17
17
  | `canvas-studio` | 画布操作流程:建项目 / 写节点 / 触发生成 / 查状态 | 任何时候先读(本文件) |
18
18
  | `canvas-craft` | 创作方法论:剧本四要素、分镜六要素、提示词规范 | 写剧本或拆分镜前 |
19
19
  | `canvas-layout` | 节点布局纪律:一类型一列、绝不重叠 | 批量建节点前 |
20
- | `canvas-director` | **导演透镜 24 / 叙事题材 21 + 行业题材 10 / 类型规格 8 / 影调 9 / 运镜词典** | **写任何提示词前** |
20
+ | `canvas-director` | **导演透镜 24 / 叙事题材 21 + 行业题材 10 / 规格 8 / 影调 9 / 运镜词典** | **写任何提示词前** |
21
21
 
22
22
  ### 怎么知道有哪些可用
23
23
 
@@ -139,7 +139,7 @@ description: 用无限画布(Infinite Canvas)做短剧/视频创作时使用
139
139
  - **创作硬规则只有一份:`canvas-craft` 技能正文**(六要素 / 每场 1 个大特写 /
140
140
  motion「起点→方式→终点」/ 同场连续三镜不同运动 / 每场冲撞点 / 因果钩子 / 术语勘误,全在里面)。
141
141
  原先另有一份「专家人设」复述同一批规则,已移除——两条并存会给出互相矛盾的指令。
142
- 风格与题材侧(导演 / 题材 / 类型规格 / 影调)由 `canvas-director` 技能包的切片提供,
142
+ 风格与题材侧(导演 / 题材 / 规格 / 影调)由 `canvas-director` 技能包的切片提供,
143
143
  用 `canvas_skill_pack` 取,见本节开头的技能清单。
144
144
 
145
145
  ## 二、标准工作流(对话填提示词+连线+触发,生成由节点完成)
package/client.js CHANGED
@@ -1289,8 +1289,8 @@ window.__ModuleLoader__.load({
1289
1289
  'skillGenre': "题材",
1290
1290
  'skillIndustry': "行业题材",
1291
1291
  'skillGenreHint': "决定压力机制与观众回报,可多选",
1292
- 'skillIndustryHint': "按类型规格自动切换为行业题材,可多选",
1293
- 'skillForm': "类型规格",
1292
+ 'skillIndustryHint': "按规格自动切换为行业题材,可多选",
1293
+ 'skillForm': "规格",
1294
1294
  'skillFormHint': "决定结构块、节奏与质量档;画幅在视频生成时选",
1295
1295
  'skillLook': "影调",
1296
1296
  'skillLookHint': "成像与材质质感;不选则跟随导演",
@@ -2214,8 +2214,8 @@ window.__ModuleLoader__.load({
2214
2214
  'skillGenre': "Genre",
2215
2215
  'skillIndustry': "Sector",
2216
2216
  'skillGenreHint': "Sets pressure mechanics and audience payoff; multi-select",
2217
- 'skillIndustryHint': "Switches to sector cards for this delivery form; multi-select",
2218
- 'skillForm': "Form",
2217
+ 'skillIndustryHint': "Switches to sector cards for this spec; multi-select",
2218
+ 'skillForm': "Spec",
2219
2219
  'skillFormHint': "Sets structure, rhythm and quality bar; aspect is chosen at video generation",
2220
2220
  'skillLook': "Look",
2221
2221
  'skillLookHint': "Imaging and material texture; leave empty to follow the director",
@@ -7833,6 +7833,17 @@ function queueThumb(assetId) {
7833
7833
  saveBibleLocal(pid, full.bible);
7834
7834
  if (full.bibleUpdatedAt) lsSet(LS_BIBLE_SYNC + pid, full.bibleUpdatedAt);
7835
7835
  }
7836
+ // 技能配置随项目文档一起回写:对话侧用 canvas_skill_pack set:true 改的配置,
7837
+ // 面板要能看见(否则「两端同源」只成立一半)。比本地戳新才覆盖,防面板刚改的被旧副本冲掉。
7838
+ if (full.skillProfile && typeof full.skillProfile === 'object') {
7839
+ const hostTs = Number(full.skillUpdatedAt) || 0;
7840
+ const localTs = Number(lsGet(LS_SKILL_SYNC + pid, 0)) || 0;
7841
+ if (hostTs > localTs) {
7842
+ saveSkillProfileLocal(pid, loadSkillProfileFromRaw(full.skillProfile));
7843
+ if (hostTs) lsSet(LS_SKILL_SYNC + pid, String(hostTs));
7844
+ if (pid === S.activeId) update((s) => { s.skillsTick = (s.skillsTick || 0) + 1; });
7845
+ }
7846
+ }
7836
7847
  if (isActiveCanvas) update((s) => { s.graph = { nodes, edges: keep, view: s.graph.view, tombstones: tombOut, edgeTombstones: local.edgeTombstones || [] }; s.assetsTick++; });
7837
7848
  }
7838
7849
  async function hostSyncTick() {
@@ -8295,7 +8306,7 @@ function queueThumb(assetId) {
8295
8306
  * 统一的创作上下文:剧本 / 分镜 / 设定集 / 图片与视频提示词都从这里取。
8296
8307
  * 两块内容,缺一不可:
8297
8308
  * ① rules —— canvas-craft 技能全文(剧本四要素、分镜六要素、提示词规范、连续性)
8298
- * ② packText —— 四层技能包切片(导演 / 题材 / 类型规格 / 影调),按生成能力切
8309
+ * ② packText —— 四层技能包切片(导演 / 题材 / 规格 / 影调),按生成能力切
8299
8310
  * 不再有「专家人设」:它原本只是 canvas-craft 的下位复述(四要素、六要素、大特写、
8300
8311
  * 因果钩子、冲撞点、180° 轴线、术语勘误全都在技能里),两条并存只会给出互相矛盾的指令。
8301
8312
  */
@@ -9685,8 +9696,11 @@ function queueThumb(assetId) {
9685
9696
  const metas = refs.map((r) => ({ blob: r.blob, name: r.name, category: r.category, assetId: r.assetId }));
9686
9697
  let p = buildEffectivePrompt(prompt, refs, entities, bible, true); // light:图片不内联设定全文(防文字渲染污染 + 参考图约束稀释)
9687
9698
  // 技能包的「镜头语言锁」:只追加风格参数与负向词,不内联叙事与剪辑(不污染图片提示词)
9699
+ // 资产设定图(label 以「设定图」结尾,由 assetSheetNodeSpec 生成)走 assets 切片:
9700
+ // 只要成像与材质,不要分镜/剪辑;普通图片仍走 prompt 切片。判定失败自动退化为 prompt。
9701
+ const lockKind = /设定图\s*$/.test(String((n.data && n.data.label) || '')) ? 'assets' : 'prompt';
9688
9702
  try {
9689
- const lock = await buildShotLockText('prompt');
9703
+ const lock = await buildShotLockText(lockKind);
9690
9704
  if (lock) p = p + '\n' + lock;
9691
9705
  } catch { /* 技能包不可用时不影响生成 */ }
9692
9706
  // 模型解析:节点选择优先,未指定路由到星标默认模型;都没有 = 明确报错(不再静默走演示模式)
@@ -13587,11 +13601,13 @@ function queueThumb(assetId) {
13587
13601
 
13588
13602
  // ============================================================
13589
13603
  // 技能包(canvas-director):四层配置 + 按生成能力切片
13590
- // L1 导演透镜 24 / L2 题材(叙事 21 + 行业 10)/ L3 类型规格 9 / L4 影调 9
13604
+ // L1 导演透镜 24 / L2 题材(叙事 21 + 行业 10)/ L3 规格 9 / L4 影调 9
13591
13605
  // 配置是**项目级**:本地 localStorage 缓存 + 随项目文档同步到 Host(对话侧可读)
13592
13606
  // 切片用 assets/canvas-director/pack.js 的纯函数(与 Host 同源,保证口径一致)
13593
13607
  // ============================================================
13594
13608
  const LS_SKILL_PROFILE = 'dsh-ic:skills:';
13609
+ // 技能配置的同步戳:对话侧写项目文档时带 skillUpdatedAt,比本地新才回写面板(双向同步用)
13610
+ const LS_SKILL_SYNC = 'dsh-ic:skillsync:';
13595
13611
  const SKILL_PACK_DIR = 'assets/canvas-director/';
13596
13612
  let PACK_MOD = null; // pack.js 模块
13597
13613
  let PACK_MANIFEST = null; // manifest.json 解析结果
@@ -13645,24 +13661,30 @@ function queueThumb(assetId) {
13645
13661
  }
13646
13662
 
13647
13663
  /** 当前项目的技能配置(本地缓存;缺字段回落默认;非法值静默回落)。 */
13648
- function loadSkillProfile(pid) {
13649
- if (!pid) return { lens: '', genres: [], form: 'short-drama', aspect: '', targetSec: 0, platform: '', look: '', genreMode: 'auto' };
13650
- const raw = lsGet(LS_SKILL_PROFILE + pid, null);
13664
+ const EMPTY_SKILL_PROFILE = { lens: '', genres: [], form: '', look: '', genreMode: 'auto' };
13665
+ /** 归一技能配置(纯函数):只接受字符串标量,数字/对象/数组一律非法回落,不抛错。
13666
+ * 抽出来是为了「从项目文档回写面板」也能走同一套规则(对话侧写的是原始字段)。 */
13667
+ function loadSkillProfileFromRaw(raw) {
13651
13668
  const b = (raw && typeof raw === 'object' && !Array.isArray(raw)) ? raw : {};
13652
- // 只接受字符串标量:数字/对象/数组一律视为非法 → 回落(与形式校验同款策略:非法即回落,不抛错)
13653
13669
  const str = (v) => (typeof v === 'string' ? v.trim() : '');
13654
- const sec = Number(b.targetSec);
13655
13670
  return {
13656
13671
  lens: str(b.lens),
13657
13672
  genres: Array.isArray(b.genres) ? [...new Set(b.genres.map((x) => (typeof x === 'string' ? x.trim() : '')).filter(Boolean))].slice(0, 12) : [],
13658
- form: str(b.form) || 'short-drama',
13659
- aspect: str(b.aspect),
13660
- targetSec: Number.isFinite(sec) && sec > 0 ? Math.round(sec) : 0,
13661
- platform: str(b.platform),
13673
+ form: str(b.form),
13662
13674
  look: str(b.look),
13663
13675
  genreMode: (b.genreMode === 'narrative' || b.genreMode === 'industry') ? b.genreMode : 'auto',
13664
13676
  };
13665
13677
  }
13678
+ function loadSkillProfile(pid) {
13679
+ if (!pid) return { ...EMPTY_SKILL_PROFILE };
13680
+ return loadSkillProfileFromRaw(lsGet(LS_SKILL_PROFILE + pid, null));
13681
+ }
13682
+ /** 只写本地(用于从项目文档回写面板配置,不再推 Host —— 否则会形成推送回环)。 */
13683
+ function saveSkillProfileLocal(pid, profile) {
13684
+ if (!pid) return;
13685
+ lsSet(LS_SKILL_PROFILE + pid, profile);
13686
+ notify();
13687
+ }
13666
13688
  /** 写技能配置:本地立即落盘 + 防抖推 Host(与设定集同一套思路)。 */
13667
13689
  let skillPushTimer = null;
13668
13690
  function saveSkillProfile(pid, profile) {
@@ -13674,7 +13696,10 @@ function queueThumb(assetId) {
13674
13696
  try {
13675
13697
  const proj = S.projects.find((p) => p.id === pid);
13676
13698
  const deleted = defaultCanvasDeleted(pid);
13677
- const doc = deleted ? { skillProfile: profile } : { ...(loadGraphDoc(pid) || emptyGraph()), skillProfile: profile };
13699
+ // skillUpdatedAt:对话侧据此判断哪边更新(与 Host 的 canvas_skill_pack set:true 同一字段)
13700
+ const stamp = Date.now();
13701
+ lsSet(LS_SKILL_SYNC + pid, String(stamp));
13702
+ const doc = deleted ? { skillProfile: profile, skillUpdatedAt: stamp } : { ...(loadGraphDoc(pid) || emptyGraph()), skillProfile: profile, skillUpdatedAt: stamp };
13678
13703
  hostRpc('projectsPut', {
13679
13704
  id: pid, name: (proj && proj.name) || pid,
13680
13705
  canvases: proj && Array.isArray(proj.canvases) ? proj.canvases : [],
@@ -13699,8 +13724,10 @@ function queueThumb(assetId) {
13699
13724
  const manifest = await loadPackManifest();
13700
13725
  if (!manifest) return { text: '', bytes: 0 };
13701
13726
  const profile = o.profile || loadSkillProfile(pid);
13702
- // 四层都没选 → 不注入(老项目行为完全不变)
13703
- if (!profile.lens && !(profile.genres || []).length && !profile.look && !o.force) return { text: '', bytes: 0 };
13727
+ // 四层都没选 → 不注入(老项目行为完全不变)。**规格也算一层**:只选「规格」时同样要注入,
13728
+ // 否则对话侧带约束、画布侧不带,两端产出分叉。
13729
+ const hasAnyLayer = !!(profile.lens || profile.form || profile.look || (profile.genres || []).length);
13730
+ if (!hasAnyLayer && !o.force) return { text: '', bytes: 0 };
13704
13731
  let P;
13705
13732
  try { P = await loadPackModule(); } catch { return { text: '', bytes: 0 }; }
13706
13733
  const zh = effLang() !== 'en';
@@ -13719,10 +13746,12 @@ function queueThumb(assetId) {
13719
13746
  }
13720
13747
  if (profile.form) await push('form', (manifest.forms || []).find((x) => x.id === profile.form));
13721
13748
  if (profile.look) await push('look', (manifest.looks || []).find((x) => x.id === profile.look));
13722
- // 运镜词典跟着分镜与提示词走
13749
+ // 运镜词典跟着分镜与提示词走;打斗节奏只在分镜(它讲的是镜头组织手法)
13723
13750
  if (kind === 'storyboard' || kind === 'prompt') {
13724
- const mv = (manifest.moves || []).find((x) => x.id === 'camera-moves');
13725
- if (mv) await push('moves', { ...mv, oneliner: mv.oneliner || '' });
13751
+ for (const mid of (kind === 'storyboard' ? ['camera-moves', 'fight-rhythm'] : ['camera-moves'])) {
13752
+ const mv = (manifest.moves || []).find((x) => x.id === mid);
13753
+ if (mv) await push('moves', { ...mv, oneliner: mv.oneliner || '' });
13754
+ }
13726
13755
  }
13727
13756
  if (!items.length) return { text: '', bytes: 0 };
13728
13757
  const head = packHeadLine(kind, profile, manifest, zh);
@@ -13748,18 +13777,18 @@ function queueThumb(assetId) {
13748
13777
  '【画布技能包 · ' + kl + '】',
13749
13778
  '导演透镜:' + (nameOf('lenses', profile.lens) || '未选')
13750
13779
  + ' 题材:' + (genreNames.join('、') || '未选')
13751
- + ' 类型规格:' + (nameOf('forms', profile.form) || '未选')
13780
+ + ' 规格:' + (nameOf('forms', profile.form) || '未选')
13752
13781
  + ' 影调:' + (profile.look ? nameOf('looks', profile.look) : '跟随导演'),
13753
- '优先级:用户显式指令 > 导演透镜 > 类型规格(仅画幅与单镜时长上压过透镜)> 题材 > 影调。以下为对应切片,写提示词时按它执行;与本块冲突的通用规则让位于本块。',
13782
+ '优先级:用户显式指令 > 导演透镜 > 规格(仅单镜时长与结构上压过透镜;画幅由视频节点生成时决定,不在此列)> 题材 > 影调。以下为对应切片,写提示词时按它执行;与本块冲突的通用规则让位于本块。',
13754
13783
  ].join('\n');
13755
13784
  }
13756
13785
  return [
13757
13786
  '[Canvas skill pack · ' + kl + ']',
13758
13787
  'Director lens: ' + (nameOf('lenses', profile.lens) || 'none')
13759
13788
  + ' Genre: ' + (genreNames.join(', ') || 'none')
13760
- + ' Delivery form: ' + (nameOf('forms', profile.form) || 'none')
13789
+ + ' Spec: ' + (nameOf('forms', profile.form) || 'none')
13761
13790
  + ' Look: ' + (profile.look ? nameOf('looks', profile.look) : 'follow the lens'),
13762
- 'Precedence: explicit user instruction > director lens > delivery form (aspect and clip length only) > genre > look. The slices below are binding; generic rules that disagree give way.',
13791
+ 'Precedence: explicit user instruction > director lens > spec (clip length and structure only; aspect is set on the video node) > genre > look. The slices below are binding; generic rules that disagree give way.',
13763
13792
  ].join('\n');
13764
13793
  }
13765
13794
 
@@ -13769,11 +13798,11 @@ function queueThumb(assetId) {
13769
13798
  *
13770
13799
  * 这是整条链路最容易掉队的一环:剧本与分镜走的是 AI 生成(拿到完整切片),
13771
13800
  * 但图片与视频是「拼装提示词」——如果只带风格参数与负向词,影调的成像/材质/光学、
13772
- * 类型规格的节奏、题材的场面颗粒就全丢了,于是第一镜对、后面越走越偏。
13801
+ * 规格的节奏、题材的场面颗粒就全丢了,于是第一镜对、后面越走越偏。
13773
13802
  *
13774
13803
  * 按生成能力取切片:
13775
13804
  * kind='prompt' —— 图片与视频。风格参数 + 影调(成像/光比/色彩/介质/光学/材质/特效)
13776
- * + 类型规格(ASL 与负向词)+ 题材的「常见场面颗粒」+ 三份负向词
13805
+ * + 规格(ASL 与负向词)+ 题材的「常见场面颗粒」+ 三份负向词
13777
13806
  * kind='assets' —— 角色/场景/道具设定图。影调的成像与色彩(不含叙事与剪辑)
13778
13807
  *
13779
13808
  * 不内联:叙事方法、剪辑节奏、人物表演(这些只对剧本/分镜有意义)。
@@ -13784,7 +13813,8 @@ function queueThumb(assetId) {
13784
13813
  const manifest = await loadPackManifest();
13785
13814
  if (!manifest) return '';
13786
13815
  const profile = loadSkillProfile(pid);
13787
- if (!profile.lens && !profile.look && !(profile.genres || []).length) return '';
13816
+ // 规格也算一层(同 buildSkillPackText 说明)
13817
+ if (!profile.lens && !profile.look && !profile.form && !(profile.genres || []).length) return '';
13788
13818
  let P;
13789
13819
  try { P = await loadPackModule(); } catch { return ''; }
13790
13820
  const k = kind === 'assets' ? 'assets' : 'prompt';
@@ -13828,7 +13858,7 @@ function queueThumb(assetId) {
13828
13858
  if (granules) push((ge ? '题材的场面颗粒' : '行业视觉母题') + '(' + entry.name + '):', granules);
13829
13859
  }
13830
13860
 
13831
- // ④ 类型规格:节奏与负向词(画幅不在这里定,由视频节点选)
13861
+ // ④ 规格:节奏与负向词(画幅不在这里定,由视频节点选)
13832
13862
  const formEntry = profile.form ? (manifest.forms || []).find((x) => x.id === profile.form) : null;
13833
13863
  if (formEntry) {
13834
13864
  const md = await readPackCard('form', formEntry.file);
@@ -18452,7 +18482,7 @@ const DIRECTOR_KEY_HELP = [
18452
18482
 
18453
18483
  // ============================================================
18454
18484
  // 技能页(craft):四层创作控制台
18455
- // L1 导演透镜(24)/ L2 题材(叙事21 + 行业10)/ L3 类型规格(9)/ L4 影调(9)+ 专家
18485
+ // L1 导演透镜(24)/ L2 题材(叙事21 + 行业10)/ L3 规格(9)/ L4 影调(9)+ 专家
18456
18486
  // 全部是**图标瓦片**,页面内不含任何 <select>(下拉框在小面板里没法扫读)
18457
18487
  // ============================================================
18458
18488
 
@@ -18516,7 +18546,7 @@ const DIRECTOR_KEY_HELP = [
18516
18546
  travel: ['M12 4l7 4v8l-7 4-7-4V8z', 'M12 8v8M8 14l4 2 4-2'],
18517
18547
  realestate: ['M12 4l7 4v8l-7 4-7-4V8z', 'M8 15v-5l4-2 4 2v5'],
18518
18548
  food: ['M12 4l7 4v8l-7 4-7-4V8z', 'M9 9v6M12 9v6M15 9v6'],
18519
- // ── L3 类型规格:不再用画幅做底形(画幅由视频生成时选),改用「用法」的符号 ──
18549
+ // ── L3 规格:不再用画幅做底形(画幅由视频生成时选),改用「用法」的符号 ──
18520
18550
  // 短剧:手机轮廓(最常见发行形态,但不代表锁死画幅)
18521
18551
  'short-drama': ['M9 3h6v18H9z', 'M11 6h2M11 18h2'],
18522
18552
  // 电影质感:取景框 + 聚光(强调「每镜都过电影线」,不是「长片」)
@@ -18665,7 +18695,7 @@ const DIRECTOR_KEY_HELP = [
18665
18695
  };
18666
18696
 
18667
18697
  const m = view || { lenses: [], genres: [], industries: [], forms: [], looks: [] };
18668
- // 类型规格是叙事类还是商业类:决定题材卡显示哪一组
18698
+ // 规格是叙事类还是商业类:决定题材卡显示哪一组
18669
18699
  const formEntry = (m.forms || []).find((x) => x.id === profile.form);
18670
18700
  // 商业类(宣传片/广告片/产品片已合并为 commercial)→ 题材层切到行业题材
18671
18701
  const COMMERCIAL_FORMS = new Set(['commercial']);
@@ -18702,7 +18732,7 @@ const DIRECTOR_KEY_HELP = [
18702
18732
  const names = profile.genres.map((g) => ((m.genres || []).find((x) => x.id === g) || (m.industries || []).find((x) => x.id === g) || {}).name || g);
18703
18733
  bits.push('题材:' + names.join('、'));
18704
18734
  }
18705
- if (formEntry) bits.push('类型规格:' + formEntry.name);
18735
+ if (formEntry) bits.push('规格:' + formEntry.name);
18706
18736
  if (profile.look) {
18707
18737
  const lo = (m.looks || []).find((x) => x.id === profile.look);
18708
18738
  if (lo) bits.push('影调:' + lo.name);
@@ -18714,7 +18744,7 @@ const DIRECTOR_KEY_HELP = [
18714
18744
  setTick((x) => x + 1);
18715
18745
  };
18716
18746
  const doClear = () => {
18717
- saveSkillProfile(S.activeId, { lens: '', genres: [], form: '', aspect: '', targetSec: 0, platform: '', look: '', genreMode: 'auto' });
18747
+ saveSkillProfile(S.activeId, { lens: '', genres: [], form: '', look: '', genreMode: 'auto' });
18718
18748
  setPreview(null);
18719
18749
  setTick((x) => x + 1);
18720
18750
  };
@@ -18752,7 +18782,7 @@ const DIRECTOR_KEY_HELP = [
18752
18782
  },
18753
18783
  })),
18754
18784
 
18755
- // ── L3 类型规格 ──
18785
+ // ── L3 规格 ──
18756
18786
  h(SkillCard, {
18757
18787
  key: 'form', id: 'form', icon: 'film', title: T('skillForm'),
18758
18788
  summary: formEntry ? formEntry.name : T('skillUnselected'),
@@ -18818,7 +18848,7 @@ const DIRECTOR_KEY_HELP = [
18818
18848
 
18819
18849
  function Rightbar() {
18820
18850
  // 资产 / 设定集已合并进统一弹窗(左下图标列打开),右侧面板保留项目 / 技能 / 设置。
18821
- // 技能页(craft)= 四层创作控制台:导演透镜 / 题材 / 类型规格 / 影调(全图标瓦片,无下拉框)。
18851
+ // 技能页(craft)= 四层创作控制台:导演透镜 / 题材 / 规格 / 影调(全图标瓦片,无下拉框)。
18822
18852
  // 快捷键/帮助反馈/检查更新已回到右上角工具栏(纯图标按钮)——右侧面板不再有动作行。
18823
18853
  const tabs = [
18824
18854
  { id: 'projects', label: T('projects'), icon: 'folder' },
package/docs/guide.en.md CHANGED
@@ -309,22 +309,22 @@ There are **no dropdowns** on the page. Every option is an icon tile: click to s
309
309
  |---|---|---|---|
310
310
  | **Director** | 24 | Camera language, lighting, cutting rhythm, style parameters | It constrains the shot's camera fields, the keyframe and the video's camera move at once — the least replaceable decision in the whole piece |
311
311
  | **Genre** | 21 narrative / 10 sector | Pressure mechanics, audience payoff, scene granules, episode hooks | The same "revenge" premise is fragmented neon under Wong Kar-wai and hard-lit slow motion under Tsui Hark; folding them together poisons both |
312
- | **Delivery form** | 8 | Structure blocks, ASL floor, rhythm, quality bar | It decides "short drama vs cinematic texture vs commercial", which is orthogonal to genre |
312
+ | **Spec** | 8 | Structure blocks, ASL floor, rhythm, quality bar | It decides "short drama vs cinematic texture vs commercial", which is orthogonal to genre |
313
313
  | **Look** | 9 | Imaging base, material, render texture | 2D thick paint / guoman 3D / game CG are imaging domains, not directors |
314
314
 
315
- **Eight delivery forms**: Short drama · Cinematic texture · Commercial · Documentary · Music video · Trailer · Explainer · Interview.
315
+ **Eight specs**: Short drama · Cinematic texture · Commercial · Documentary · Music video · Trailer · Explainer · Interview.
316
316
 
317
317
  - A **short drama** is a self-contained 60–180 s unit and **covers both single episodes and multi-episode series** — a series is just episode after episode, so there is no separate "series" form. Its quality bar is "shoot it like film", not "shoot it like a short video".
318
318
  - **Cinematic texture** is a *texture* spec, not a feature-length one. It constrains how each shot must be made to read as cinema (ASL floor 1.5 s, 24 fps, motivated light, real materials, readable depth), and its duration tiers stop at 30–300 s. The reason is practical: **a video model emits only 3–8 s per clip** (5 s by default here), so "a 120-minute film" can only be hundreds of clips stitched together — a 120-minute tier in the spec would be fiction. For a feature, advance episode by episode; this spec makes each segment pass the cinema line.
319
319
  - **Commercial** merges 宣传片 (brand film) / 广告片 (ad) / 产品片 (product film) into one card — near-identical needs, split internally by block ratios instead of three cards.
320
320
 
321
- The genre layer **switches automatically with the delivery form**: pick Commercial and it shows sector cards (tech, finance, medical, beauty, auto, education, FMCG, travel, real estate, food); every other form shows narrative genres.
321
+ The genre layer **switches automatically with the spec**: pick Commercial and it shows sector cards (tech, finance, medical, beauty, auto, education, FMCG, travel, real estate, food); every other form shows narrative genres.
322
322
 
323
- **Priority**: explicit user instruction > director > delivery form (only on ASL and structure) > genre > look.
323
+ **Priority**: explicit user instruction > director > spec (only on ASL and structure) > genre > look.
324
324
 
325
325
  ### 9.2 Aspect ratio is not decided here
326
326
 
327
- Vertical, horizontal, square or widescreen all work with any delivery form — **aspect and resolution are chosen on the video node at generation time**. Vertical delivery is common for short drama, but that is a distribution choice, not a craft spec; **a short drama can equally be shot horizontal and to a cinematic standard**. The delivery form constrains **structure, rhythm and quality bar**, not proportion.
327
+ Vertical, horizontal, square or widescreen all work with any spec — **aspect and resolution are chosen on the video node at generation time**. Vertical delivery is common for short drama, but that is a distribution choice, not a craft spec; **a short drama can equally be shot horizontal and to a cinematic standard**. The spec constrains **structure, rhythm and quality bar**, not proportion.
328
328
 
329
329
  ### 9.3 Three buttons
330
330
 
@@ -313,26 +313,26 @@
313
313
  |---|---|---|---|
314
314
  | **导演** | 24 | 镜头语言、灯光色彩、剪辑节奏、风格参数 | 它同时约束分镜的机位字段、图片关键帧、视频运镜——整部片最不可替换的决策 |
315
315
  | **题材** | 叙事 21 / 行业 10 | 压力机制、观众回报、场面颗粒、集尾钩子 | 同一个「复仇打脸」,王家卫拍是抽帧暧昧,徐克拍是凌厉升格;混进导演层会互相污染 |
316
- | **类型规格** | 8 | 结构块、平均镜长下限、节奏、质量档 | 它决定「这是短剧、电影还是商业片」,与题材正交 |
316
+ | **规格** | 8 | 结构块、平均镜长下限、节奏、质量档 | 它决定「这是短剧、电影还是商业片」,与题材正交 |
317
317
  | **影调** | 9 | 成像底层、材质、渲染质感 | 2D 厚涂 / 国漫 3D / 游戏 CG 是成像域,不是导演;塞进导演会让「怎么剪」和「长什么样」打架 |
318
318
 
319
- **八种类型规格**:短剧 · 电影质感 · 商业片 · 纪实 · 音乐视频 · 预告 · 解说 · 访谈。
319
+ **八种规格**:短剧 · 电影质感 · 商业片 · 纪实 · 音乐视频 · 预告 · 解说 · 访谈。
320
320
 
321
321
  其中:
322
322
  - **短剧**是单集 60–180 秒的自足单元,**同时覆盖单集与连续多集**——一部剧就是一集一集往下做,不需要单独的「连续剧」规格。它对质量的要求是「按电影标准拍」,不是「短视频标准」:真实光源与光比、有依据的焦段、可读的景深。
323
- - **电影质感**是**质感规格,不是长片规格**。它约束每一镜怎么做才像电影(ASL 1.5 秒以上、24 帧、动机光、真材质、成立构图),成片长度档只到 30–300 秒。原因很实际:**AI 视频模型单次只出 3–8 秒**(本插件默认 5 秒),所谓「一部 120 分钟的电影」只能是几百上千个片段接起来,把它写成类型规格里的一个时长档没有意义。要拍长片,按集/按段推进,本规格负责每一段都过电影线。
323
+ - **电影质感**是**质感规格,不是长片规格**。它约束每一镜怎么做才像电影(ASL 1.5 秒以上、24 帧、动机光、真材质、成立构图),成片长度档只到 30–300 秒。原因很实际:**AI 视频模型单次只出 3–8 秒**(本插件默认 5 秒),所谓「一部 120 分钟的电影」只能是几百上千个片段接起来,把它写成规格里的一个时长档没有意义。要拍长片,按集/按段推进,本规格负责每一段都过电影线。
324
324
  - **商业片**把「宣传片 / 广告片 / 产品片」合成一张卡——它们是相近需求,靠段位配比内部分流,不再拆成三张。
325
325
 
326
- **画幅不在这里决定。** 横屏、竖屏、方屏、宽银幕都能用任何类型规格——**画幅与分辨率是视频节点生成时的选项**。短剧竖屏投放很常见,但那是发行选择,不是创作规格;短剧同样可以横屏、可以按电影标准拍。类型规格约束的是**结构、节奏、质量档**,不是比例。
326
+ **画幅不在这里决定。** 横屏、竖屏、方屏、宽银幕都能用任何规格——**画幅与分辨率是视频节点生成时的选项**。短剧竖屏投放很常见,但那是发行选择,不是创作规格;短剧同样可以横屏、可以按电影标准拍。规格约束的是**结构、节奏、质量档**,不是比例。
327
327
 
328
- 题材层会**随类型规格自动切换**:选「商业片」时换成行业题材(科技、金融、医疗、美妆、汽车、教育、快消、文旅、地产、餐饮);其余类型显示叙事题材。
328
+ 题材层会**随规格自动切换**:选「商业片」时换成行业题材(科技、金融、医疗、美妆、汽车、教育、快消、文旅、地产、餐饮);其余类型显示叙事题材。
329
329
 
330
- 优先级:**用户显式指令 > 导演 > 类型规格(仅单镜时长与结构上压过导演)> 题材 > 影调**。
330
+ 优先级:**用户显式指令 > 导演 > 规格(仅单镜时长与结构上压过导演)> 题材 > 影调**。
331
331
 
332
332
  ### 9.2 三个按钮
333
333
 
334
334
  - **预览**:显示当前配置注入到剧本的切片全文与字节数。用来确认「到底塞了什么进去」。
335
- - **写设定集**:把风格摘要(导演名 + 题材 + 类型规格 + 影调)写进设定集的「风格基调」,这样对话侧也能读到。
335
+ - **写设定集**:把风格摘要(导演名 + 题材 + 规格 + 影调)写进设定集的「风格基调」,这样对话侧也能读到。
336
336
  - **清空**:四层全归零,生成行为回到默认。
337
337
 
338
338
  ### 9.3 配置按项目保存
@@ -353,8 +353,8 @@
353
353
 
354
354
  | 节点 / 能力 | 拿到的切片 |
355
355
  |---|---|
356
- | 剧本节点 | 题材卡 + 导演的叙事/节奏/表演节 + 类型规格的结构块 |
357
- | 分镜表节点 | 导演的镜头语言/灯光/风格参数/拍摄清单 + 类型规格 + 运镜词典 |
356
+ | 剧本节点 | 题材卡 + 导演的叙事/节奏/表演节 + 规格的结构块 |
357
+ | 分镜表节点 | 导演的镜头语言/灯光/风格参数/拍摄清单 + 规格 + 运镜词典 |
358
358
  | 图片节点 | 只追加「镜头语言锁」:风格参数 + 影调负向词 |
359
359
  | 视频节点 | 同上(有上游分镜表时也追加,不打断结构化字段) |
360
360
  | 角色/场景/道具设定图 | 导演的灯光色彩 + 影调卡 |
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 技能配置(四层:导演透镜 / 题材 / 类型规格 / 影调)的字段规范化。
2
+ * 技能配置(四层:导演透镜 / 题材 / 规格 / 影调)的字段规范化。
3
3
  *
4
4
  * 单独成文件是为了让 index.js 与 skill-pack.js 都能引用而不产生循环依赖。
5
5
  * 与 normalizeBible 同风格:字符串裁剪、空值剔除、非法值静默回落(不抛错,老项目零迁移)。
@@ -19,15 +19,11 @@ export function normalizeSkillProfile(raw, allowed) {
19
19
  .filter((x) => !Array.isArray(a.genres) || !a.genres.length || a.genres.includes(x))
20
20
  .slice(0, 12)
21
21
  : [];
22
- const sec = Number(b.targetSec);
23
22
  const mode = (b.genreMode === 'narrative' || b.genreMode === 'industry') ? b.genreMode : 'auto';
24
23
  return {
25
24
  lens: pick(b.lens, a.lenses),
26
25
  genres,
27
26
  form: pick(b.form, a.forms),
28
- aspect: pick(b.aspect, a.aspects),
29
- targetSec: Number.isFinite(sec) && sec > 0 ? Math.min(3600, Math.round(sec)) : 0,
30
- platform: pick(b.platform, a.platforms),
31
27
  look: pick(b.look, a.looks),
32
28
  genreMode: mode,
33
29
  };
@@ -35,5 +31,5 @@ export function normalizeSkillProfile(raw, allowed) {
35
31
 
36
32
  /** 默认配置(老项目无该字段时就是它,保证行为与未引入技能包前完全一致)。 */
37
33
  export const DEFAULT_SKILL_PROFILE = {
38
- lens: '', genres: [], form: '', aspect: '', targetSec: 0, platform: '', look: '', genreMode: 'auto',
34
+ lens: '', genres: [], form: '', look: '', genreMode: 'auto',
39
35
  };
package/index.js CHANGED
@@ -934,7 +934,7 @@ function normalizeBible(raw) {
934
934
  };
935
935
  }
936
936
  /**
937
- * 技能配置(四层:导演透镜 / 题材 / 类型规格 / 影调)字段规范化。
937
+ * 技能配置(四层:导演透镜 / 题材 / 规格 / 影调)字段规范化。
938
938
  * 实现搬进 index-core-skill-profile.js(skill-pack.js 也要引用,避免循环依赖)。
939
939
  */
940
940
  /**
@@ -1299,7 +1299,7 @@ async function projectsPut(args) {
1299
1299
  out.bible = prev.bible;
1300
1300
  out.bibleUpdatedAt = prev.bibleUpdatedAt || 0;
1301
1301
  }
1302
- // 技能配置(四层:导演/题材/类型规格/影调)同样是项目级,与设定集同一套同步语义
1302
+ // 技能配置(四层:导演/题材/规格/影调)同样是项目级,与设定集同一套同步语义
1303
1303
  let canvasSkillProfile = null;
1304
1304
  if (canvasId) {
1305
1305
  const projectDoc = readDocOrNull(id) || {};
@@ -1618,7 +1618,7 @@ function nextNodePos(doc, type, connectFromId, size) {
1618
1618
  }
1619
1619
  // ---------- 内置技能(操作规程 + 创作方法论,约束并提升对话的画布创作)----------
1620
1620
  // canvas-studio=操作硬约束;canvas-craft=剧本/分镜/提示词创作方法论;canvas-layout=节点布局纪律;
1621
- // canvas-director=导演透镜/题材/类型规格/影调/运镜(创作控制层,按生成能力切片注入)
1621
+ // canvas-director=导演透镜/题材/规格/影调/运镜(创作控制层,按生成能力切片注入)
1622
1622
  const SKILL_NAMES = ['canvas-studio', 'canvas-craft', 'canvas-layout', 'canvas-director'];
1623
1623
  // = @deepseek-ai/dsh-skill 的 BUNDLED_SKILL_RANK(避免 import DSH 包;registry 要求 rank 必须是有限数字)
1624
1624
  const SKILL_RANK = 600;
@@ -3264,25 +3264,7 @@ export function apply(ctx) {
3264
3264
  text: String(text ?? ((def && typeof def === 'object') ? JSON.stringify(def) : '')).slice(0, 80000),
3265
3265
  };
3266
3266
  },
3267
- /** 读 canvas-director 技能包里的任意卡文件(Client 侧切片用)。
3268
- * 安全:白名单前缀 + 禁 `..` + 禁绝对路径/盘符 + 只许 .md/.js/.json + 落在包目录内 + ≤256KB。 */
3269
- async readSkillPack(args) {
3270
- const rel = String((args && args.rel) || '').replace(/\\/g, '/').replace(/^\/+/, '').trim();
3271
- if (!rel) throw new Error('缺少 rel');
3272
- if (rel.includes('..')) throw new Error('非法路径: ' + rel);
3273
- if (/^[a-z]:/i.test(rel) || rel.startsWith('/')) throw new Error('非法路径: ' + rel);
3274
- if (!/^canvas-director\/(?:[A-Za-z0-9_.\-\u4e00-\u9fa5]+\/)*[A-Za-z0-9_.\-\u4e00-\u9fa5]+\.(?:md|js|json)$/.test(rel)) {
3275
- throw new Error('非法路径: ' + rel + '(只允许 canvas-director 下的 .md/.js/.json)');
3276
- }
3277
- const baseDir = path.join(HERE, 'assets', 'canvas-director');
3278
- const abs = path.resolve(path.join(HERE, 'assets', rel));
3279
- if (abs !== baseDir && !abs.startsWith(baseDir + path.sep)) throw new Error('路径越界: ' + rel);
3280
- let st;
3281
- try { st = statSync(abs); } catch { throw new Error('文件不存在: ' + rel); }
3282
- if (!st.isFile()) throw new Error('不是文件: ' + rel);
3283
- if (st.size > 256 * 1024) throw new Error('文件过大: ' + rel + '(>' + (256 * 1024) + 'B)');
3284
- return { rel, text: readFileSync(abs, 'utf8'), bytes: st.size };
3285
- },
3267
+
3286
3268
  };
3287
3269
 
3288
3270
  const toolDisposers = [];
@@ -3818,7 +3800,7 @@ export function apply(ctx) {
3818
3800
  // ── 技能包:把「本插件有哪些技能、各管什么」和「当前该按什么口径写提示词」交给对话侧 ──
3819
3801
  reg({
3820
3802
  name: 'canvas_list_skills',
3821
- description: '列出本插件(无限画布)提供的全部技能,以及技能包(导演透镜 / 题材 / 类型规格 / 影调 / 运镜)的完整清单和当前项目的已选配置。【开始任何画布创作前先调它】:它会告诉你写剧本、分镜、图片提示词、视频提示词各自该加载哪份技能、可用哪些透镜与题材,随后用 canvas_skill_pack 取切片。',
3803
+ description: '列出本插件(无限画布)提供的全部技能,以及技能包(导演透镜 / 题材 / 规格 / 影调 / 运镜)的完整清单和当前项目的已选配置。【开始任何画布创作前先调它】:它会告诉你写剧本、分镜、图片提示词、视频提示词各自该加载哪份技能、可用哪些透镜与题材,随后用 canvas_skill_pack 取切片。',
3822
3804
  parameters: {
3823
3805
  type: 'object',
3824
3806
  properties: { projectId: { type: 'string', description: '可选:项目 id(给出时一并返回该项目的已选配置)' } },
@@ -3847,7 +3829,7 @@ export function apply(ctx) {
3847
3829
  });
3848
3830
  reg({
3849
3831
  name: 'canvas_skill_pack',
3850
- description: '按生成能力取「导演透镜 + 题材 + 类型规格 + 影调 + 运镜」的已拼装切片,用于写剧本(script)/ 分镜(storyboard)/ 图片与视频提示词(prompt)/ 角色场景道具设定图(assets)。返回切片全文、字节数与生效配置,可直接拼进你的系统提示或提示词。【这是让对话写出的内容与画布自生成保持同一口径的唯一入口】;也可以用 lens/genres/form/look 临时覆盖,或传 set:true 把配置写进项目(与画布 UI 选择同源)。',
3832
+ description: '按生成能力取「导演透镜 + 题材 + 规格 + 影调 + 运镜」的已拼装切片,用于写剧本(script)/ 分镜(storyboard)/ 图片与视频提示词(prompt)/ 角色场景道具设定图(assets)。返回切片全文、字节数与生效配置,可直接拼进你的系统提示或提示词。【这是让对话写出的内容与画布自生成保持同一口径的唯一入口】;也可以用 lens/genres/form/look 临时覆盖,或传 set:true 把配置写进项目(与画布 UI 选择同源)。',
3851
3833
  parameters: {
3852
3834
  type: 'object',
3853
3835
  properties: {
@@ -3855,7 +3837,7 @@ export function apply(ctx) {
3855
3837
  kind: { type: 'string', enum: ['script', 'storyboard', 'prompt', 'assets'], description: '生成能力:script=剧本,storyboard=分镜,prompt=图片/视频提示词,assets=资产设定图。缺省 script' },
3856
3838
  lens: { type: 'string', description: '可选:临时覆盖导演透镜 id(见 canvas_list_skills)' },
3857
3839
  genres: { type: 'array', items: { type: 'string' }, description: '可选:临时覆盖题材 id 数组(叙事题材与行业题材都可)' },
3858
- form: { type: 'string', description: '可选:临时覆盖类型规格 id' },
3840
+ form: { type: 'string', description: '可选:临时覆盖规格 id' },
3859
3841
  look: { type: 'string', description: '可选:临时覆盖影调 id;传空串表示跟随导演' },
3860
3842
  set: { type: 'boolean', description: '可选:true = 把本次生效的配置写进项目(之后画布自生成也用同一套)' },
3861
3843
  },
@@ -3878,6 +3860,9 @@ export function apply(ctx) {
3878
3860
  if (args.look !== undefined) override.look = args.look;
3879
3861
  const merged = normalizeSkillProfile({ ...base, ...override }, allow);
3880
3862
  const kind = String((args && args.kind) || 'script');
3863
+ if (!['script', 'storyboard', 'prompt', 'assets'].includes(kind)) {
3864
+ throw new Error('kind 必须是 script / storyboard / prompt / assets,收到:' + kind);
3865
+ }
3881
3866
  const built = await buildSkillPack({ kind, profile: merged, hereDir: HERE, zh: true });
3882
3867
  if (args.set) {
3883
3868
  doc.skillProfile = merged;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ninkch/dsh-infinite-canvas",
3
- "version": "0.19.5",
3
+ "version": "0.19.6",
4
4
  "description": "Node-based short-drama studio for DeepSeek Harness: script → storyboard → assets → shots → voiced, subtitled final cut on an infinite canvas.",
5
5
  "keywords": [
6
6
  "dsh",
package/skill-pack.js CHANGED
@@ -12,7 +12,8 @@ import { normalizeSkillProfile } from './index-core-skill-profile.js';
12
12
  /** moves 永远随 storyboard/prompt 走(运镜是这两类生成的必需品)。 */
13
13
  const MOVES_FOR_KIND = {
14
14
  script: [],
15
- storyboard: ['camera-moves'],
15
+ // 分镜要调度动作,运镜 + 打斗节奏两张卡都带;提示词只需运镜。
16
+ storyboard: ['camera-moves', 'fight-rhythm'],
16
17
  prompt: ['camera-moves'],
17
18
  assets: [],
18
19
  };
@@ -65,11 +66,15 @@ export async function buildSkillPack(opts) {
65
66
  const md = readCard(hereDir, 'looks', lookEntry.file);
66
67
  if (md) items.push({ layer: 'look', md, meta: lookEntry });
67
68
  }
68
- for (const mid of (MOVES_FOR_KIND[kind] || [])) {
69
- const mv = (inv.moves || []).find((x) => x.id === mid);
70
- if (!mv) continue;
71
- const md = readMove(hereDir, mv.file);
72
- if (md) items.push({ layer: 'moves', md, meta: mv });
69
+ // 运镜只在「已经有层卡命中」时才跟随注入。
70
+ // 否则四层全空的项目也会被塞进 5KB 运镜词典 —— 违反「未配置不注入」(老项目零变化)。
71
+ if (items.length) {
72
+ for (const mid of (MOVES_FOR_KIND[kind] || [])) {
73
+ const mv = (inv.moves || []).find((x) => x.id === mid);
74
+ if (!mv) continue;
75
+ const md = readMove(hereDir, mv.file);
76
+ if (md) items.push({ layer: 'moves', md, meta: mv });
77
+ }
73
78
  }
74
79
 
75
80
  const P = await getPackModule();
@@ -97,15 +102,15 @@ function headLine(kind, profile, inv, zh) {
97
102
  return [
98
103
  '【画布技能包 · ' + kindLabel(kind) + '】',
99
104
  '导演透镜:' + nameOf('lenses', profile.lens) + ' 题材:' + (genreNames.join('、') || '—')
100
- + ' 类型规格:' + nameOf('forms', profile.form) + ' 影调:' + (profile.look ? nameOf('looks', profile.look) : '跟随导演'),
101
- '优先级:用户显式指令 > 导演透镜 > 类型规格(仅画幅与单镜时长上压过透镜)> 题材 > 影调 > 技能默认。以下为对应切片,写提示词时必须遵守;与本块冲突的通则让位于本块。',
105
+ + ' 规格:' + nameOf('forms', profile.form) + ' 影调:' + (profile.look ? nameOf('looks', profile.look) : '跟随导演'),
106
+ '优先级:用户显式指令 > 导演透镜 > 规格(仅单镜时长与结构上压过透镜;画幅由视频节点生成时决定,不在此列)> 题材 > 影调 > 技能默认。以下为对应切片,写提示词时必须遵守;与本块冲突的通则让位于本块。',
102
107
  ].join('\n');
103
108
  }
104
109
  return [
105
110
  '[Canvas skill pack · ' + kindLabel(kind, false) + ']',
106
111
  'Director lens: ' + nameOf('lenses', profile.lens) + ' Genre: ' + (genreNames.join(', ') || '-')
107
- + ' Delivery form: ' + nameOf('forms', profile.form) + ' Look: ' + (profile.look ? nameOf('looks', profile.look) : 'follow the lens'),
108
- 'Precedence: explicit user instruction > director lens > delivery form (only aspect and clip length) > genre > look > skill defaults. The slices below are binding; where a generic rule disagrees, this block wins.',
112
+ + ' Spec: ' + nameOf('forms', profile.form) + ' Look: ' + (profile.look ? nameOf('looks', profile.look) : 'follow the lens'),
113
+ 'Precedence: explicit user instruction > director lens > spec (only clip length and structure; aspect is chosen on the video node at generation time) > genre > look > skill defaults. The slices below are binding; where a generic rule disagrees, this block wins.',
109
114
  ].join('\n');
110
115
  }
111
116
 
@@ -121,14 +126,14 @@ export const SKILL_CATALOG = [
121
126
  { name: 'canvas-studio', when: '任何时候先读:建项目 / 写节点 / 触发生成 / 查状态' },
122
127
  { name: 'canvas-craft', when: '写剧本或拆分镜前:剧本四要素、分镜六要素、提示词规范' },
123
128
  { name: 'canvas-layout', when: '批量建节点前:一类型一列、绝不重叠' },
124
- { name: 'canvas-director', when: '写任何提示词前:导演透镜 / 题材 / 类型规格 / 影调 / 运镜,先用 canvas_skill_pack 取切片' },
129
+ { name: 'canvas-director', when: '写任何提示词前:导演透镜 / 题材 / 规格 / 影调 / 运镜,先用 canvas_skill_pack 取切片' },
125
130
  ];
126
131
 
127
132
  /** 层清单的可读渲染(canvas_list_skills 的正文)。 */
128
133
  export function renderInventory(inv, profile, zh) {
129
134
  const lines = [];
130
- const ZH_LABEL = { lenses: '导演透镜', genres: '叙事题材', industries: '行业题材', forms: '类型规格', looks: '影调质感' };
131
- const EN_LABEL = { lenses: 'Director lenses', genres: 'Genres', industries: 'Industries', forms: 'Delivery forms', looks: 'Looks' };
135
+ const ZH_LABEL = { lenses: '导演透镜', genres: '叙事题材', industries: '行业题材', forms: '规格', looks: '影调质感' };
136
+ const EN_LABEL = { lenses: 'Director lenses', genres: 'Genres', industries: 'Industries', forms: 'Specs', looks: 'Looks' };
132
137
  const L = zh === false ? EN_LABEL : ZH_LABEL;
133
138
  for (const layer of ['lenses', 'genres', 'industries', 'forms', 'looks']) {
134
139
  const list = (inv && inv[layer]) || [];
@@ -172,7 +177,7 @@ export function renderProfile(profile, inv, zh) {
172
177
  }
173
178
  return '导演=' + (nameOf('lenses', profile.lens) || '(未选)')
174
179
  + ' 题材=' + (g.join('、') || '(未选)')
175
- + ' 类型规格=' + (nameOf('forms', profile.form) || '(未选)')
180
+ + ' 规格=' + (nameOf('forms', profile.form) || '(未选)')
176
181
  + ' 影调=' + (profile.look ? nameOf('looks', profile.look) : '跟随导演');
177
182
  }
178
183