@ninkch/dsh-infinite-canvas 0.17.16 → 0.17.17
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/assets/canvas-craft/SKILL.md +44 -0
- package/assets/canvas-studio/SKILL.md +11 -3
- package/client.js +52 -6
- package/package.json +1 -1
|
@@ -31,6 +31,50 @@ B:……
|
|
|
31
31
|
- **一致性**:人名、地名、道具名与设定集**逐字一致**,禁止擅自改名或新增设定。
|
|
32
32
|
- **输出格式**:直接给可用剧本正文,不要解释、不要提纲、不要"待生成"占位。
|
|
33
33
|
|
|
34
|
+
|
|
35
|
+
## 二·五、资产设定图版式(角色/道具/场景的专业排版,拒绝简单三视图)
|
|
36
|
+
|
|
37
|
+
简单三视图信息量不足——设定图要做成**专业设定集排版**:文字信息 + 主图 + 特写 + 表情/色卡
|
|
38
|
+
分区呈现,一图读懂资产的全部视觉设定。所有面板文字一律**简体中文、清晰可读**;
|
|
39
|
+
同一资产在所有分格中外观**完全一致**(发型/服装/配色逐格复用设定集关键词)。
|
|
40
|
+
|
|
41
|
+
### 角色设定图(16:9 横版)
|
|
42
|
+
- **左侧信息面板**:「核心人物信息」——姓名 / 性别 / 年龄 / 身份 / 性格 / 配色(逐行列出);
|
|
43
|
+
旁边「角色设定」小面板:两三句背景设定。
|
|
44
|
+
- **中央主图**:全身立绘,正面站姿,完整呈现体型与服装(占画面约 1/3 宽)。
|
|
45
|
+
- **右上脸部特征**:正面特写 + 侧面特写并排,下方 3–4 行文字标注五官要点
|
|
46
|
+
(如「杏眼温润 / 鼻梁秀挺 / 唇色自然 / 侧颜柔和」)。
|
|
47
|
+
- **右上服饰特征**:服装要点逐行标注(如「月白浅交领长裙 / 绣兰草纹样 / 腰间系青玉佩」)。
|
|
48
|
+
- **左下细节展示**:2×3 六格特写——发饰 / 耳饰 / 腰带 / 袖口纹样 / 下摆纹样 / 随身道具,
|
|
49
|
+
每格下方一行小字标注名称(如「发饰细节:兰花银饰」)。
|
|
50
|
+
- **右下表情排列**:2×5 共 10 个头像素描格,逐格标注表情名
|
|
51
|
+
(参考序列:温柔、微笑、沉思、惊讶、端庄、凛然、悠然、羞涩、坚定、冷淡——按角色性格取舍)。
|
|
52
|
+
- 提示词结构示例:「角色人设图(16:9 横版专业设定集排版):左侧『核心人物信息』文字面板
|
|
53
|
+
(姓名:沈清蘅 / 性别:女 / 年龄:28 / 身份:兰衣医修 / 性格:温柔沉静 / 配色:月白浅绿青玉);
|
|
54
|
+
中央全身立绘……;右上脸部特征……;左下细节展示六格……;右下表情排列十格……」
|
|
55
|
+
——面板名与标注文字全部写进提示词,模型才能排出完整版式。
|
|
56
|
+
|
|
57
|
+
### 道具设定图(横竖皆可——按道具特性选,不限制)
|
|
58
|
+
- **画幅选择**:长条形/单件道具(剑、箫、杖)竖版更能展示完整形态;
|
|
59
|
+
组合道具、带场景感或横向结构的道具用横版能并列更多细节与色卡——排版逻辑不变,画幅自由。
|
|
60
|
+
- **顶部**:道具名大标题 + 一行类别副标题(如「火属性上古仙兵」)。
|
|
61
|
+
- **中央主图**:道具斜 45° 悬浮展示,带氛围光/粒子效果,占画面主体。
|
|
62
|
+
- **细节特写 2–3 格**:材质纹理、关键部件(如「箫身细节:赤玉雕金」),各配一行小字。
|
|
63
|
+
- **侧栏侧视图**:同一道具的正侧面完整形态,标题「侧视图」。
|
|
64
|
+
- **底部颜色参考**:3–5 个圆形色卡,每个下方标注颜色名(如「赤红 / 熔金 / 绛紫」)。
|
|
65
|
+
- 点缀一两句道具铭文/短诗式文案(如「一簫起,万火归墟」)。
|
|
66
|
+
|
|
67
|
+
### 场景设定图(主场景图 + 环绕补充小图)
|
|
68
|
+
- **主场景图占画面主体**(全景建立镜头,交代空间结构与光线方向)——不要切成几张等大的长条格,
|
|
69
|
+
那样每格都是局部长图,场景信息反而看不全。
|
|
70
|
+
- **环绕补充小图(3–6 张,位置与数量不拘)**,参考版式:
|
|
71
|
+
- 右侧一列:**多角度图**(俯瞰、贴地、第一人称视线等其他机位)+ **其他时间图**(夜景 / 雨雾 /
|
|
72
|
+
逆光等不同时间段或天气下的同一场景);
|
|
73
|
+
- 底部一排:**细节格**(标志物 / 入口 / 材质纹理 / 关键道具在场景中的位置)。
|
|
74
|
+
- 每张小图下方一行标注(机位 / 时间段 / 内容,如「入口 · 夜」「雨中 · 逆光」)。
|
|
75
|
+
- 上述布局是大致参考、不强制;所有格子的光线方向、天气、色调须与主图统一
|
|
76
|
+
(展示不同时间段时在标注中说明)。
|
|
77
|
+
|
|
34
78
|
### 画面感要点
|
|
35
79
|
- 用一个**具体的可视细节**替代形容词:不写"他很悲伤",写"他把没送出的信折了三次,塞回口袋"。
|
|
36
80
|
- 每场至少一个"能被记住的画面"(记忆点)。
|
|
@@ -137,9 +137,17 @@ description: 用无限画布(Infinite Canvas)做短剧/视频创作时使用
|
|
|
137
137
|
时间窗归属靠分镜的 `dialogue` 标注(对白逐字来自剧本、按镜头分配,一镜尽量单说话人);
|
|
138
138
|
分镜缺少台词标注时按钮会降级为手动模式并提示重新生成分镜。
|
|
139
139
|
- 触发:`canvas_run_node` → 轮询;整段视频一次生成完成
|
|
140
|
-
5.
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
5. **生成角色/场景/道具参考图**:为设定集里的每个条目创建 image-gen 节点
|
|
141
|
+
(命名规范「角色-<名> 设定图」/「场景-<名> 设定图」/「道具-<名> 设定图」),
|
|
142
|
+
**prompt 必须按 `canvas-craft`「资产设定图版式」一节的排版模板写详细版式提示词,
|
|
143
|
+
禁止只写一句三视图/外观关键词**——信息量不足,撑不起后续视频的角色一致性。要点(完整模板见 canvas-craft):
|
|
144
|
+
- **角色设定图**(16:9 横版):左侧文字信息面板(姓名/性别/年龄/身份/性格/配色)+ 中央全身立绘
|
|
145
|
+
+ 脸部特征(正面+侧面特写带文字标注)+ 服饰特征标注 + 细节展示(6 格特写各配名称小字)
|
|
146
|
+
+ 表情排列(2×5 十格逐格标注表情名);所有分格中同一角色外观完全一致
|
|
147
|
+
- **道具设定图**(横竖皆可,按道具特性选):顶部道具名大标题+类别副标题;中央主图(斜 45° 悬浮+氛围光);
|
|
148
|
+
材质/部件细节特写带名称小字;侧栏侧视图;底部圆形色卡 3–5 枚带颜色名
|
|
149
|
+
- **场景设定图**(主图+环绕小图):一张主场景大图占主体,周边环绕补充小图(多角度 / 其他时间段 / 细节格,3–6 张各配标注)——大致参考不强约束
|
|
150
|
+
- 面板文字一律简体中文且清晰可读;设定集外观关键词逐格复用,保证跨格一致
|
|
143
151
|
- 需要补连任意两节点时用 `canvas_connect({ projectId, fromNode, toNode })`(端口类型必须匹配;
|
|
144
152
|
连分镜表时系统按语义自动选口:剧本/文本 → `script`,分镜表 → `prev`,不用手写端口)
|
|
145
153
|
- **布局乱了/节点重叠**:`canvas_relayout({ projectId })` 一键重排——横向数据流决定列
|
package/client.js
CHANGED
|
@@ -504,6 +504,9 @@ window.__ModuleLoader__.load({
|
|
|
504
504
|
'bitrate': '码率',
|
|
505
505
|
'bitrateAuto': '自动(浏览器默认)',
|
|
506
506
|
'runRequestFailed': '对话触发的生成未能执行',
|
|
507
|
+
'stoppedNote': '已手动停止',
|
|
508
|
+
'stoppedTaskKept': '视频任务 id 已保留,可凭 id 回查结果',
|
|
509
|
+
'stoppedToast': '已手动停止生成',
|
|
507
510
|
'apiTimeout': '请求超时(毫秒)',
|
|
508
511
|
'uploadFailed': '导入失败',
|
|
509
512
|
'syncFailed': '与 Host 同步失败',
|
|
@@ -908,6 +911,9 @@ window.__ModuleLoader__.load({
|
|
|
908
911
|
'bitrate': 'Bitrate',
|
|
909
912
|
'bitrateAuto': 'Auto (browser default)',
|
|
910
913
|
'runRequestFailed': 'Chat-triggered generation failed',
|
|
914
|
+
'stoppedNote': 'Manually stopped',
|
|
915
|
+
'stoppedTaskKept': 'video task id kept for lookup',
|
|
916
|
+
'stoppedToast': 'Generation stopped',
|
|
911
917
|
'apiTimeout': 'Request timeout (ms)',
|
|
912
918
|
'uploadFailed': 'Import failed',
|
|
913
919
|
'syncFailed': 'Failed to sync with Host',
|
|
@@ -2254,6 +2260,7 @@ window.__ModuleLoader__.load({
|
|
|
2254
2260
|
const deadline = Date.now() + 120 * 60 * 1000; // 2 小时:视频任务含排队可能很久,查询层绝不先放弃
|
|
2255
2261
|
while (Date.now() < deadline) {
|
|
2256
2262
|
await sleep(10000);
|
|
2263
|
+
if (genAborted()) throw new Error(T('stoppedNote'));
|
|
2257
2264
|
let r2 = null;
|
|
2258
2265
|
try { r2 = await fetchViaProxy(base + '/' + taskId, { method: 'GET', headers, timeoutMs: 60000, label: '查询 Seedance 任务' }); } catch { /* 网络抖动重试 */ }
|
|
2259
2266
|
if (!r2 || !r2.ok) continue; // 查询层任何失败(网关 5xx/429/断连)都不算任务失败——只等生成任务自身的终态
|
|
@@ -2304,6 +2311,7 @@ window.__ModuleLoader__.load({
|
|
|
2304
2311
|
const deadline = Date.now() + 120 * 60 * 1000; // 2 小时:查询层绝不先放弃
|
|
2305
2312
|
while (Date.now() < deadline) {
|
|
2306
2313
|
await sleep(6000);
|
|
2314
|
+
if (genAborted()) throw new Error(T('stoppedNote'));
|
|
2307
2315
|
let r2 = null;
|
|
2308
2316
|
try { r2 = await fetchViaProxy(queryEp + '?task_id=' + encodeURIComponent(taskId), { method: 'GET', headers, timeoutMs: 60000, label: '查询视频任务' }); } catch { /* 网络抖动重试 */ }
|
|
2309
2317
|
if (!r2 || !r2.ok) continue; // 查询层失败 ≠ 任务失败,只等生成任务自身的终态
|
|
@@ -2340,6 +2348,7 @@ window.__ModuleLoader__.load({
|
|
|
2340
2348
|
const deadline = Date.now() + 120 * 60 * 1000; // 2 小时:查询层绝不先放弃
|
|
2341
2349
|
while (Date.now() < deadline) {
|
|
2342
2350
|
await sleep(6000);
|
|
2351
|
+
if (genAborted()) throw new Error(T('stoppedNote'));
|
|
2343
2352
|
let r2 = null;
|
|
2344
2353
|
try { r2 = await fetchViaProxy(ep + '/' + taskId, { method: 'GET', headers, timeoutMs: 60000, label: '查询视频任务' }); } catch { /* 网络抖动重试 */ }
|
|
2345
2354
|
if (!r2 || !r2.ok) continue; // 查询层失败 ≠ 任务失败,只等生成任务自身的终态
|
|
@@ -2480,6 +2489,7 @@ window.__ModuleLoader__.load({
|
|
|
2480
2489
|
const deadline = Date.now() + 120 * 60 * 1000;
|
|
2481
2490
|
while (Date.now() < deadline) {
|
|
2482
2491
|
await sleep(step);
|
|
2492
|
+
if (genAborted()) throw new Error(T('stoppedNote'));
|
|
2483
2493
|
let r = null;
|
|
2484
2494
|
try { r = await query(); } catch { continue; }
|
|
2485
2495
|
if (!r) continue;
|
|
@@ -2913,6 +2923,7 @@ window.__ModuleLoader__.load({
|
|
|
2913
2923
|
const deadline = Date.now() + 120 * 60 * 1000; // 2 小时:查询层绝不先放弃
|
|
2914
2924
|
while (Date.now() < deadline) {
|
|
2915
2925
|
await sleep(3000);
|
|
2926
|
+
if (genAborted()) throw new Error(T('stoppedNote'));
|
|
2916
2927
|
let r2 = null;
|
|
2917
2928
|
try { r2 = await fetchViaProxy(base + '/' + taskId, { method: 'GET', headers, timeoutMs: 60000, label: '查询视频任务' }); } catch { /* 网络抖动重试 */ }
|
|
2918
2929
|
if (!r2 || !r2.ok) continue; // 查询层失败 ≠ 任务失败,只等生成任务自身的终态
|
|
@@ -3970,8 +3981,10 @@ window.__ModuleLoader__.load({
|
|
|
3970
3981
|
method: 'POST',
|
|
3971
3982
|
headers: { 'content-type': 'application/json' },
|
|
3972
3983
|
body: JSON.stringify(args),
|
|
3984
|
+
signal: genAbort.ctl ? genAbort.ctl.signal : undefined, // 停止按钮:立即中断流式读取
|
|
3973
3985
|
});
|
|
3974
3986
|
} catch (e) {
|
|
3987
|
+
if (genAborted()) throw e; // 手动打断:保持 abort 语义供上层识别
|
|
3975
3988
|
throw new Error(T('connectFailed') + ':' + String((e && e.message) || e));
|
|
3976
3989
|
}
|
|
3977
3990
|
if (res.status === 404 || !res.body) return await aiComplete({ system, user, model }); // 旧 Host 回退
|
|
@@ -3999,6 +4012,7 @@ window.__ModuleLoader__.load({
|
|
|
3999
4012
|
else if (msg.t === 'fail') throw new Error(String(msg.d || T('rpcFail')));
|
|
4000
4013
|
}
|
|
4001
4014
|
}
|
|
4015
|
+
if (genAborted()) throw new Error(T('stoppedNote')); // 流结束后仍被打断 → 走打断路径
|
|
4002
4016
|
if (!final) throw new Error(T('rpcFail'));
|
|
4003
4017
|
return final.text || '';
|
|
4004
4018
|
}
|
|
@@ -4209,6 +4223,7 @@ window.__ModuleLoader__.load({
|
|
|
4209
4223
|
onReasoning: (d) => onDelta && onDelta('reasoning', d),
|
|
4210
4224
|
onText: (d) => onDelta && onDelta('text', d),
|
|
4211
4225
|
});
|
|
4226
|
+
if (genAborted()) throw new Error(T('stoppedNote'));
|
|
4212
4227
|
// 解析 + 修复重试:模型常把剧本里的引用文字(如 喷漆"拆"字)原样写进 JSON 字符串值,
|
|
4213
4228
|
// 半角引号未转义 → JSON 非法。修复策略:把"闭合引号后不跟 JSON 结构符"的引号对换成「」。
|
|
4214
4229
|
let parsed = parseJsonBalanced(text);
|
|
@@ -4877,6 +4892,13 @@ window.__ModuleLoader__.load({
|
|
|
4877
4892
|
// 工作流执行
|
|
4878
4893
|
// ============================================================
|
|
4879
4894
|
const cancelRef = { cancelled: false };
|
|
4895
|
+
const genAbort = { ctl: null }; // 当前生成的 AbortController(停止按钮真正中断底层流/轮询)
|
|
4896
|
+
let genActive = 0; // 活跃生成计数(工作流与单节点触发统一计数)
|
|
4897
|
+
function genAborted() { return !!(genAbort.ctl && genAbort.ctl.signal && genAbort.ctl.signal.aborted); }
|
|
4898
|
+
function stopActiveGen() {
|
|
4899
|
+
cancelRef.cancelled = true;
|
|
4900
|
+
if (genAbort.ctl) { try { genAbort.ctl.abort(); } catch { /* 忽略 */ } }
|
|
4901
|
+
}
|
|
4880
4902
|
|
|
4881
4903
|
/** 分镜节点的剧本来源:**连线上游与手动粘贴二选一**——
|
|
4882
4904
|
* 连了线就只用连线(上游没生成完会明确报错,绝不偷用粘贴的文本混着来)。 */
|
|
@@ -5062,9 +5084,13 @@ window.__ModuleLoader__.load({
|
|
|
5062
5084
|
async function runGenNode(n) {
|
|
5063
5085
|
const type = n.type;
|
|
5064
5086
|
const settings = loadSettings(); // 模型解析(供应商+模型)用
|
|
5087
|
+
const ctl = new AbortController();
|
|
5088
|
+
genAbort.ctl = ctl;
|
|
5089
|
+
genActive++;
|
|
5065
5090
|
update((s) => {
|
|
5066
5091
|
const nd = s.graph.nodes[n.id];
|
|
5067
|
-
if (nd) { nd.data.status = 'running'; nd.data.error = null; }
|
|
5092
|
+
if (nd) { nd.data.status = 'running'; nd.data.error = null; nd.data.stopNote = null; }
|
|
5093
|
+
s.running = true; // 单节点触发(对话/节点按钮)也显示停止按钮
|
|
5068
5094
|
});
|
|
5069
5095
|
flushSave(); // 立刻落盘 + 推送 Host:切走画面/对话查询都能看到"运行中"
|
|
5070
5096
|
try {
|
|
@@ -5152,12 +5178,28 @@ window.__ModuleLoader__.load({
|
|
|
5152
5178
|
}
|
|
5153
5179
|
return true;
|
|
5154
5180
|
} catch (err) {
|
|
5181
|
+
const stopped = !!(ctl.signal && ctl.signal.aborted);
|
|
5155
5182
|
update((s) => {
|
|
5156
5183
|
const nd = s.graph.nodes[n.id];
|
|
5157
|
-
if (nd)
|
|
5184
|
+
if (!nd) return;
|
|
5185
|
+
if (stopped) {
|
|
5186
|
+
// 手动打断:回到待命而不是报错;视频任务 id 已持久化,可凭 id 回查结果
|
|
5187
|
+
nd.data.status = 'idle';
|
|
5188
|
+
nd.data.error = null;
|
|
5189
|
+
if (nd.type === 'script' || nd.type === 'storyboard') nd.data.aiStatus = 'idle';
|
|
5190
|
+
nd.data.stopNote = T('stoppedNote') + (nd.data.taskId ? ';' + T('stoppedTaskKept') : '');
|
|
5191
|
+
} else {
|
|
5192
|
+
nd.data.status = 'error';
|
|
5193
|
+
nd.data.error = String((err && err.message) || err);
|
|
5194
|
+
}
|
|
5158
5195
|
});
|
|
5159
5196
|
flushSave(); // 让对话侧的 canvas_node_status 也能读到失败原因
|
|
5197
|
+
if (stopped) toast(T('stoppedToast'));
|
|
5160
5198
|
return false;
|
|
5199
|
+
} finally {
|
|
5200
|
+
genActive = Math.max(0, genActive - 1);
|
|
5201
|
+
if (genActive === 0) update((s) => { s.running = false; });
|
|
5202
|
+
if (genAbort.ctl === ctl) genAbort.ctl = null;
|
|
5161
5203
|
}
|
|
5162
5204
|
}
|
|
5163
5205
|
function commitGenResult(nodeId, assetId, demo, kind) {
|
|
@@ -5209,7 +5251,10 @@ window.__ModuleLoader__.load({
|
|
|
5209
5251
|
flushSave();
|
|
5210
5252
|
}
|
|
5211
5253
|
}
|
|
5212
|
-
function stopRun() {
|
|
5254
|
+
function stopRun() {
|
|
5255
|
+
stopActiveGen();
|
|
5256
|
+
toast(T('stoppedToast'));
|
|
5257
|
+
}
|
|
5213
5258
|
|
|
5214
5259
|
// ============================================================
|
|
5215
5260
|
// 轻量提示
|
|
@@ -6211,10 +6256,10 @@ window.__ModuleLoader__.load({
|
|
|
6211
6256
|
}),
|
|
6212
6257
|
h(GenSettingsRow, { key: 'set', n, kind }),
|
|
6213
6258
|
h('button', {
|
|
6214
|
-
key: 'gen', type: 'button', className: 'dshic-gen',
|
|
6259
|
+
key: 'gen', type: 'button', className: 'dshic-gen',
|
|
6215
6260
|
onPointerDown: (e) => e.stopPropagation(),
|
|
6216
|
-
onClick: () => { runGenNode(n); },
|
|
6217
|
-
}, n.data.status === 'running' ?
|
|
6261
|
+
onClick: () => { if (n.data.status === 'running') { stopActiveGen(); toast(T('stoppedToast')); } else { runGenNode(n); } },
|
|
6262
|
+
}, n.data.status === 'running' ? [Ic('stop', 12), T('stop')] : [Ic('play', 12), T('generate')]),
|
|
6218
6263
|
h('div', { key: 'r', className: 'dshic-node-row' },
|
|
6219
6264
|
h('div', { className: 'dshic-flex' }),
|
|
6220
6265
|
n.data.demo && n.data.assetId ? h('span', { className: 'dshic-tag demo' }, T('demoTag')) : null,
|
|
@@ -6243,6 +6288,7 @@ window.__ModuleLoader__.load({
|
|
|
6243
6288
|
h(RefRows, { key: 'refrows', n, portId: 'ref' }),
|
|
6244
6289
|
h(MentionInfo, { key: 'mn', n }),
|
|
6245
6290
|
n.data.genWarn ? h('div', { key: 'gw', className: 'dshic-hint', style: { color: 'var(--dsw-alias-state-warning-primary, #d08700)' } }, '⚠ ' + n.data.genWarn) : null,
|
|
6291
|
+
n.data.stopNote ? h('div', { key: 'sn', className: 'dshic-hint' }, '⏹ ' + n.data.stopNote) : null,
|
|
6246
6292
|
n.data.error ? h('div', { key: 'err', className: 'dshic-err' }, n.data.error) : null,
|
|
6247
6293
|
assetUrlV && kind === 'image'
|
|
6248
6294
|
? h('img', { key: 'img', className: 'dshic-preview', src: assetUrlV, draggable: false, 'data-no-drag': '1', style: { cursor: 'zoom-in' }, onPointerDown: (e) => e.stopPropagation(), onClick: (e) => { e.stopPropagation(); openLightbox('image', assetUrlV, n.data.label); } })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ninkch/dsh-infinite-canvas",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.17",
|
|
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",
|