@ninkch/dsh-infinite-canvas 0.18.2 → 0.18.4
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 +25 -13
- package/assets/canvas-studio/SKILL.md +4 -2
- package/client.js +83 -12
- package/package.json +1 -1
|
@@ -39,19 +39,31 @@ B:……
|
|
|
39
39
|
面板文字只允许 ≤4 字短标签(面板名 / 表情名 / 颜色名)。同一资产在所有分格中外观**完全一致**——
|
|
40
40
|
**已连接参考图时,参考图形象是唯一事实**,逐格严格复制;未连接参考图时逐格复用设定集外观关键词。
|
|
41
41
|
|
|
42
|
-
### 角色设定图(16:9
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
### 角色设定图(16:9 横版——按「六段式」结构写提示词,实测出图最稳)
|
|
43
|
+
|
|
44
|
+
提示词按以下六段组织,**方括号段名照写**(模型对显式分段结构的遵循度显著更好):
|
|
45
|
+
|
|
46
|
+
- **【总起】**一句话交代:16:9 横版专业设定集排版、深色简洁背景、风格基调(如 写实电影感)、
|
|
47
|
+
同一角色在所有分格中外观完全一致。
|
|
48
|
+
- **【身份锚】**声明参考图是同一角色的真实照片:严格保持脸型 / 眼型 / 耳形 / 毛量 / 口鼻比例 / 体型,
|
|
49
|
+
只改变姿势与视角;**忽略照片里的临时配饰**(狗绳、挂牌等),配饰一律按提示词文字描述绘制。
|
|
50
|
+
绑定几张参考图就写几个 [图N]。
|
|
51
|
+
- **【主体】**中央大幅全身立绘(正面站姿):体型写量化并加反例(如「约 3 公斤、肩高 20 厘米;
|
|
52
|
+
不是中型犬,不是萨摩耶/银狐/柴犬」——易混淆品种必须显式排除)、毛色毛发、五官、四肢尾巴,
|
|
53
|
+
以及固定配饰的完整描述。
|
|
54
|
+
- **【主图下方·三视图(必须项)】**正面/侧面/背面三格全身站姿——同一角色只换角度,
|
|
55
|
+
交代侧背形态(后续视频一致性靠它)。
|
|
56
|
+
- **【右侧窄栏·自上而下分块;每块内容全图只出现一次】**
|
|
57
|
+
① 脸部特写一格 + 一行 ≤12 字的五官短标注;
|
|
58
|
+
② 配饰格(有固定配饰时):正 / 背两张小图并排,可刻短字,并注明「该物在本图内只出现这一处」;
|
|
59
|
+
③ 表情格:6 个头部小头像分两行(每行 3 个),每个下方只标两字表情名。
|
|
60
|
+
- **【文字规则】**图内文字极少:只保留两三个字的短标签,黑色细体、笔画清楚、字要写大;
|
|
61
|
+
不写任何大标题、说明段或长句。
|
|
62
|
+
- **【画面白名单】**明确写出「画面中只有这一只角色和上述分格内容;没有人物、没有其他动物、
|
|
63
|
+
没有乐器、没有文字水印」——反幻觉白名单实测能压住乱入元素。
|
|
64
|
+
|
|
65
|
+
要点:**每块内容全图只出现一次**(配饰在配饰格画过,主图描述就不再重复强调它);
|
|
66
|
+
反例约束有效——易混淆的品种 / 风格务必写进【主体】的排除句。
|
|
55
67
|
|
|
56
68
|
### 道具设定图(横竖皆可——按道具特性选,不限制)
|
|
57
69
|
- **画幅选择**:长条形/单件道具(剑、箫、杖)竖版更能展示完整形态;
|
|
@@ -141,8 +141,10 @@ description: 用无限画布(Infinite Canvas)做短剧/视频创作时使用
|
|
|
141
141
|
(命名规范「角色-<名> 设定图」/「场景-<名> 设定图」/「道具-<名> 设定图」),
|
|
142
142
|
**prompt 必须按 `canvas-craft`「资产设定图版式」一节的排版模板写详细版式提示词,
|
|
143
143
|
禁止只写一句三视图/外观关键词**——信息量不足,撑不起后续视频的角色一致性。要点(完整模板见 canvas-craft):
|
|
144
|
-
- **角色设定图**(16:9
|
|
145
|
-
|
|
144
|
+
- **角色设定图**(16:9 横版):按 canvas-craft 的「六段式」结构写——【总起】(版式/背景/风格/
|
|
145
|
+
全格一致)→【身份锚】(参考图=唯一形象事实,忽略照片配饰)→【主体】(量化体型+反例排除)→
|
|
146
|
+
【三视图·必须项】→【右侧窄栏:脸部特写 / 配饰格 / 表情两行×3,每格只标两字标签,
|
|
147
|
+
每块内容全图只出现一次】→【文字规则 + 画面白名单】
|
|
146
148
|
- **道具设定图**(横竖皆可,按道具特性选):顶部道具名大标题(≤6 字);中央主图(斜 45° 悬浮+氛围光);
|
|
147
149
|
细节特写 2–3 格(格内无文字);侧栏侧视图;底部圆形色卡 3–5 枚(色名 ≤3 字)
|
|
148
150
|
- **场景设定图**(主图+环绕小图):一张主场景大图占主体,周边环绕补充小图(多角度 / 其他时间段 / 细节格,3–6 张各配 ≤6 字短标注)——大致参考不强约束
|
package/client.js
CHANGED
|
@@ -2243,12 +2243,52 @@ window.__ModuleLoader__.load({
|
|
|
2243
2243
|
// 参考图走三步上传(upload_urls → PUT 签名直传 → assets 登记)换 asset_id;
|
|
2244
2244
|
// 登记结果缓存在画布资产元数据(compAssetIdCache):同一张 CAS 资产不重复上传。
|
|
2245
2245
|
const compAssetIdCache = new Map(); // 画布资产 id → 优云 asset_id
|
|
2246
|
-
/**
|
|
2246
|
+
/** 从文件头字节解析像素尺寸(PNG IHDR / JPEG SOF)——与平台服务端解析口径一致:
|
|
2247
|
+
* createImageBitmap 会按 EXIF 方向旋转并交换宽高,平台读原始文件,两者不一致时登记会被拒。 */
|
|
2248
|
+
function rawImageDims(u8) {
|
|
2249
|
+
try {
|
|
2250
|
+
// PNG:签名 8 字节 + 长度 4 + 'IHDR' → 偏移 16 起是宽高(大端 32 位)
|
|
2251
|
+
if (u8[0] === 0x89 && u8[1] === 0x50) {
|
|
2252
|
+
return {
|
|
2253
|
+
w: ((u8[16] << 24) | (u8[17] << 16) | (u8[18] << 8) | u8[19]) >>> 0,
|
|
2254
|
+
h: ((u8[20] << 24) | (u8[21] << 16) | (u8[22] << 8) | u8[23]) >>> 0,
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
// JPEG:顺序扫描段,命中 SOF0..SOF15(排除 DHT/DAC/RSTn)后取段内高/宽
|
|
2258
|
+
if (u8[0] === 0xff && u8[1] === 0xd8) {
|
|
2259
|
+
let i = 2;
|
|
2260
|
+
while (i + 9 < u8.length) {
|
|
2261
|
+
if (u8[i] !== 0xff) { i++; continue; }
|
|
2262
|
+
const marker = u8[i + 1];
|
|
2263
|
+
if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) { i += 2; continue; }
|
|
2264
|
+
const len = (u8[i + 2] << 8) | u8[i + 3];
|
|
2265
|
+
if (len < 2) break;
|
|
2266
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
2267
|
+
return { w: (u8[i + 7] << 8) | u8[i + 8], h: (u8[i + 5] << 8) | u8[i + 6] };
|
|
2268
|
+
}
|
|
2269
|
+
i += 2 + len;
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
} catch { /* 解析失败返回 null */ }
|
|
2273
|
+
return null;
|
|
2274
|
+
}
|
|
2275
|
+
/** 参考图上传前置:魔数嗅探真实编码(平台仅收 JPG/PNG/WebP,且校验声明 MIME 与实际字节一致——
|
|
2276
|
+
* 改后缀的 AVIF/HEIC 会在登记时报 "Params [Width/Height] not available")+ 测像素尺寸
|
|
2277
|
+
* (同时给出解码尺寸与文件原始尺寸,登记不一致时用原始尺寸重试)。 */
|
|
2247
2278
|
async function imageBlobMeta(input) {
|
|
2248
2279
|
const blob = input.blob || input;
|
|
2249
|
-
const
|
|
2250
|
-
const
|
|
2251
|
-
const
|
|
2280
|
+
const head = new Uint8Array(await blob.slice(0, 16).arrayBuffer());
|
|
2281
|
+
const isPng = head[0] === 0x89 && head[1] === 0x50 && head[2] === 0x4e && head[3] === 0x47;
|
|
2282
|
+
const isJpg = head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff;
|
|
2283
|
+
const isWebp = head[0] === 0x52 && head[1] === 0x49 && head[2] === 0x46 && head[3] === 0x46
|
|
2284
|
+
&& head[8] === 0x57 && head[9] === 0x45 && head[10] === 0x42 && head[11] === 0x50;
|
|
2285
|
+
if (!isPng && !isJpg && !isWebp) {
|
|
2286
|
+
throw new Error('参考图编码不受支持:平台仅接受 JPG/PNG/WebP 原生编码(当前文件的实际字节不是这三类,常见于改后缀的 AVIF/HEIC)——请转成 JPG/PNG 后重新挂参考图');
|
|
2287
|
+
}
|
|
2288
|
+
const mime = isPng ? 'image/png' : isJpg ? 'image/jpeg' : 'image/webp';
|
|
2289
|
+
const ext = isPng ? 'png' : isJpg ? 'jpg' : 'webp';
|
|
2290
|
+
let raw = null;
|
|
2291
|
+
try { raw = rawImageDims(new Uint8Array(await blob.slice(0, 262144).arrayBuffer())); } catch { /* 忽略 */ }
|
|
2252
2292
|
let width = 0;
|
|
2253
2293
|
let height = 0;
|
|
2254
2294
|
try {
|
|
@@ -2256,9 +2296,10 @@ window.__ModuleLoader__.load({
|
|
|
2256
2296
|
width = bmp.width;
|
|
2257
2297
|
height = bmp.height;
|
|
2258
2298
|
try { bmp.close && bmp.close(); } catch { /* 忽略 */ }
|
|
2259
|
-
} catch { /*
|
|
2260
|
-
if (!width || !height)
|
|
2261
|
-
|
|
2299
|
+
} catch { /* 解码失败时退回文件头尺寸 */ }
|
|
2300
|
+
if (!width || !height) { if (raw) { width = raw.w; height = raw.h; } }
|
|
2301
|
+
if (!width || !height) throw new Error('参考图尺寸测量失败');
|
|
2302
|
+
return { blob, mime, ext, width, height, rawWidth: raw ? raw.w : width, rawHeight: raw ? raw.h : height };
|
|
2262
2303
|
}
|
|
2263
2304
|
async function compshareImageTask(prompt, refs, s, opts) {
|
|
2264
2305
|
const o = opts || {};
|
|
@@ -2267,15 +2308,29 @@ window.__ModuleLoader__.load({
|
|
|
2267
2308
|
if (!base) throw new Error('未配置图片 API 地址(优云智算:https://cp.compshare.cn)');
|
|
2268
2309
|
const headers = { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + String(s.apiKey || '').trim() };
|
|
2269
2310
|
const extra = parseExtraParams(s.extra); // 透传:optimize_prompt / use_context_ir 等任务级字段
|
|
2311
|
+
// 报错附带实际发出的参数:登记/提交被拒时一眼看到缺什么、值是多少(不再靠猜)
|
|
2312
|
+
const bodySummary = (b) => {
|
|
2313
|
+
if (!b || typeof b !== 'object') return '';
|
|
2314
|
+
const parts = [];
|
|
2315
|
+
for (const [k, v] of Object.entries(b)) {
|
|
2316
|
+
if (v === undefined) { parts.push(k + '=undefined'); continue; }
|
|
2317
|
+
if (v === null) { parts.push(k + '=null'); continue; }
|
|
2318
|
+
if (typeof v === 'object') { parts.push(k + '=[' + (Array.isArray(v) ? v.length + ' 项' : '对象') + ']'); continue; }
|
|
2319
|
+
const t = String(v);
|
|
2320
|
+
parts.push(k + '=' + (t.length > 40 ? t.slice(0, 40) + '…' : t));
|
|
2321
|
+
}
|
|
2322
|
+
return parts.join(' ');
|
|
2323
|
+
};
|
|
2270
2324
|
const api = async (method, path, body, idem) => {
|
|
2271
2325
|
const h = { ...headers };
|
|
2272
2326
|
if (idem) h['Idempotency-Key'] = idem;
|
|
2273
2327
|
const r = await fetchViaProxy(base + path, { method, headers: h, ...(body !== undefined ? { body } : {}), timeoutMs: 120000, label: '优云图片 ' + method });
|
|
2274
|
-
if (!r.ok) throw new Error(
|
|
2275
|
-
let j = null; try { j = JSON.parse(r.text); } catch { throw new Error(
|
|
2328
|
+
if (!r.ok) throw new Error(`优云图片 API ${method} ${path} HTTP ${r.status}:` + r.text.slice(0, 300));
|
|
2329
|
+
let j = null; try { j = JSON.parse(r.text); } catch { throw new Error(`优云图片 API ${method} ${path} 响应非 JSON:` + r.text.slice(0, 200)); }
|
|
2276
2330
|
// 网关业务错误可能 HTTP 200:RetCode≠0 或 {type:'error'} 都算失败
|
|
2277
2331
|
if ((j.RetCode != null && Number(j.RetCode) !== 0) || j.type === 'error') {
|
|
2278
|
-
throw new Error(
|
|
2332
|
+
throw new Error(`优云图片 API ${method} ${path} 失败:` + ((j.error && j.error.message) || j.Message || JSON.stringify(j).slice(0, 200))
|
|
2333
|
+
+ (body ? '|已发送参数:' + bodySummary(body) : ''));
|
|
2279
2334
|
}
|
|
2280
2335
|
return j;
|
|
2281
2336
|
};
|
|
@@ -2296,7 +2351,23 @@ window.__ModuleLoader__.load({
|
|
|
2296
2351
|
label: '参考图上传',
|
|
2297
2352
|
});
|
|
2298
2353
|
if (!put.ok) throw new Error('参考图上传失败 HTTP ' + put.status + ':' + put.text.slice(0, 200));
|
|
2299
|
-
|
|
2354
|
+
// 登记:宽高必须与平台解析出的文件尺寸一致。浏览器解码尺寸与文件原始尺寸在
|
|
2355
|
+
// EXIF 旋转图上不同 → 先用解码尺寸,被拒(Params [Width/Height] not available)时用原始尺寸重试一次
|
|
2356
|
+
const regPayload = (w, h) => ({ object_key: up.object_key, file_name: fileName, content_type: meta.mime, file_size: meta.blob.size, width: w, height: h });
|
|
2357
|
+
let reg = null;
|
|
2358
|
+
try {
|
|
2359
|
+
reg = await api('POST', '/image/v1/assets', regPayload(meta.width, meta.height));
|
|
2360
|
+
} catch (e) {
|
|
2361
|
+
const msg = String((e && e.message) || e);
|
|
2362
|
+
const alt = meta.rawWidth && meta.rawHeight && (meta.rawWidth !== meta.width || meta.rawHeight !== meta.height);
|
|
2363
|
+
if (!/width\/?height/i.test(msg) || !alt) throw e;
|
|
2364
|
+
try {
|
|
2365
|
+
reg = await api('POST', '/image/v1/assets', regPayload(meta.rawWidth, meta.rawHeight));
|
|
2366
|
+
console.info('[infinite-canvas] 参考图登记改用文件原始尺寸成功:' + meta.rawWidth + '×' + meta.rawHeight);
|
|
2367
|
+
} catch (e2) {
|
|
2368
|
+
throw new Error(msg + ';改用文件原始尺寸 ' + meta.rawWidth + '×' + meta.rawHeight + ' 重试仍失败:' + ((e2 && e2.message) || e2));
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2300
2371
|
const aid = reg && reg.reference && reg.reference.asset_id;
|
|
2301
2372
|
if (!aid) throw new Error('参考图登记失败:响应中没有 reference.asset_id');
|
|
2302
2373
|
if (ref.assetId) compAssetIdCache.set(ref.assetId, aid);
|
|
@@ -8546,7 +8617,7 @@ window.__ModuleLoader__.load({
|
|
|
8546
8617
|
h('input', { type: 'checkbox', checked: p.generateAudio === true, onChange: (e) => patchProvider(p.id, 'generateAudio', e.target.checked), onPointerDown: (e) => e.stopPropagation() }),
|
|
8547
8618
|
h('span', {}, T('vidAudioOpt'))) : null,
|
|
8548
8619
|
],
|
|
8549
|
-
(kind === 'image' ||
|
|
8620
|
+
(kind === 'image' || kind === 'video') ? [
|
|
8550
8621
|
h(Field, { key: 'extra', label: T('cfgExtra'), value: p.extra || '', onChange: (v) => patchProvider(p.id, 'extra', v), placeholder: '{"watermark":false,"prompt_extend":false}', tip: T('cfgExtraHint') }),
|
|
8551
8622
|
] : null,
|
|
8552
8623
|
h('div', { className: 'dshic-row', style: { marginTop: 4 } },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ninkch/dsh-infinite-canvas",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.4",
|
|
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",
|