@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.
@@ -39,19 +39,31 @@ B:……
39
39
  面板文字只允许 ≤4 字短标签(面板名 / 表情名 / 颜色名)。同一资产在所有分格中外观**完全一致**——
40
40
  **已连接参考图时,参考图形象是唯一事实**,逐格严格复制;未连接参考图时逐格复用设定集外观关键词。
41
41
 
42
- ### 角色设定图(16:9 横版——主图与三视图占大版面)
43
- - **版面主体(约三分之二)给画面**:中央大幅全身立绘主图(正面站姿),主图下方一排
44
- **三视图(正面 / 侧面 / 背面 全身)**——三视图是必须项,后续视频要靠它保证侧背面的准确形态。
45
- - **右侧窄栏做纯视觉辅助(不渲染文字段落)**:
46
- - 脸部特征:一格正面特写(或正 / 侧两小格);
47
- - 表情条:一行 4–6 个小头像,每格下方只标表情名(两字,按角色性格取舍,如 温柔 / 惊讶 / 坚定);
48
- - 细节特写(可选):1–2 格(项圈 / 挂牌 / 发饰),格内无文字。
49
- - **禁止**:姓名 / 性别 / 年龄等属性文本、服饰说明文字、旁白或诗句——这些属于设定集,
50
- 画进图里必成乱码,且会挤出角色形象的一致性注意力。
51
- - 所有分格中同一角色外观完全一致(以参考图形象为准逐格复制)。
52
- - 提示词结构示例:「角色人设图(16:9 横版设定集排版):中央大幅全身立绘,主图下方三视图一排
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 横版):**主图 + 三视图一排(正/侧/背全身,必须项)占大版面(约 2/3)**;
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
- /** 参考图上传前置:统一 MIME/扩展名并测真实像素尺寸(平台校验上传宽高必须与图片一致)。 */
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 type = (blob.type || 'image/png').toLowerCase();
2250
- const mime = /jpeg|jpg/.test(type) ? 'image/jpeg' : /webp/.test(type) ? 'image/webp' : 'image/png';
2251
- const ext = mime === 'image/jpeg' ? 'jpg' : mime === 'image/webp' ? 'webp' : 'png';
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) throw new Error('参考图尺寸测量失败(仅支持 PNG/JPG/WebP)');
2261
- return { blob, mime, ext, width, height };
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('优云图片 API HTTP ' + r.status + ':' + r.text.slice(0, 300));
2275
- let j = null; try { j = JSON.parse(r.text); } catch { throw new Error('优云图片 API 响应非 JSON:' + r.text.slice(0, 200)); }
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('优云图片 API 失败:' + ((j.error && j.error.message) || j.Message || JSON.stringify(j).slice(0, 200)));
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
- const reg = await api('POST', '/image/v1/assets', { object_key: up.object_key, file_name: fileName, content_type: meta.mime, file_size: meta.blob.size, width: meta.width, height: meta.height });
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' || detectVideoVendor(p) === 'openai' || detectVideoVendor(p) === 'seedance') ? [
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.2",
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",