@petercjl/cloud-hug-style-transfer 0.5.2 → 0.6.0
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 +9 -3
- package/capabilities.json +3 -2
- package/npm/integrity.json +24 -15
- package/package.json +2 -2
- package/release-integrity.json +28 -19
- package/skills/cloud-hug-style-transfer/PROVENANCE.json +309 -0
- package/skills/cloud-hug-style-transfer/SKILL.md +70 -100
- package/skills/cloud-hug-style-transfer/agents/openai.yaml +8 -8
- package/skills/cloud-hug-style-transfer/capabilities.json +4 -4
- package/skills/cloud-hug-style-transfer/references/assets.md +21 -28
- package/skills/cloud-hug-style-transfer/references/brand-layout.md +16 -43
- package/skills/cloud-hug-style-transfer/references/canvas-comparison.md +39 -25
- package/skills/cloud-hug-style-transfer/references/cases.md +8 -8
- package/skills/cloud-hug-style-transfer/references/detail-final-20261016.md +28 -48
- package/skills/cloud-hug-style-transfer/references/maintenance.md +21 -7
- package/skills/cloud-hug-style-transfer/references/media-execution.md +54 -0
- package/skills/cloud-hug-style-transfer/references/model.md +3 -3
- package/skills/cloud-hug-style-transfer/references/page-patterns.md +43 -26
- package/skills/cloud-hug-style-transfer/references/photography-method.md +22 -19
- package/skills/cloud-hug-style-transfer/references/planning.md +86 -0
- package/skills/cloud-hug-style-transfer/references/products.md +89 -19
- package/skills/cloud-hug-style-transfer/references/source-page-three-variants.md +2 -6
- package/skills/cloud-hug-style-transfer/scripts/canvas-row.mjs +12 -0
- package/skills/cloud-hug-style-transfer/scripts/check-media.mjs +10 -0
- package/skills/cloud-hug-style-transfer/scripts/comparison-layout.mjs +30 -0
- package/skills/cloud-hug-style-transfer/scripts/media-dependency.mjs +49 -0
- package/skills/cloud-hug-style-transfer/scripts/plan-inventory.mjs +76 -0
- package/skills/cloud-hug-style-transfer/scripts/runtime.mjs +2 -0
|
@@ -1,29 +1,99 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 产品识别、设定卡与统一确认
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 产品的三个计数
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
1. **形态数**:轮廓、造型或关键结构不同的款式。兔与狗是两种形态;同一月亮的紫色和绿色是一种形态。
|
|
6
|
+
2. **款色数/卡片数**:每个形态内的各个已识别配色组合,一组合一张卡。纯光线差异、同品重复照片、场景中同款多只不产生新卡。
|
|
7
|
+
3. **展示模块页数**:07–10 这一组按形态数生成,每种形态一页。其余页按页面映射完成本轮产品表达,不按卡片数机械复制。
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
尺寸不同但形态相同时,先记为规格;若轮廓比例显著不同、是否同组影响出图,在统一确认里明确。场景里的普通靠垫、毯子、家具不算在售商品。
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
- **用途随源页走。** 保留有效卖点及其证据关系,例如压陷、靠抱、支撑、可拆洗结构或尺寸对照;不把原本没有的使用方式、材质性能和参数加给新商品。明显不清或互相矛盾的关键文字先核实;不能把视觉转换说成性能认证。
|
|
11
|
-
- **数量随任务走。** “全部款色”按当次清单逐一核对;单品摄影、情绪图或部分集合按本屏目的取舍,不额外加入历史动物凑数。普通靠垫、毯子等承托道具不自动成为在售 SKU。
|
|
12
|
-
- **裁切与清晰度随证明内容走。** 触感近景可以出画,尺度/全貌/款色比较应保留必要轮廓与区别;情绪图可以自然遮挡,但不能借裁切掩盖已知结构错误。需要验证商品细节时传入本次相应实拍,不用品牌成稿替代。
|
|
11
|
+
## 先判断整套详情的商品范围
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
先枚举并实际浏览整套旧图,再放大与商品范围、轮廓、结构、配色及材质判断有关的画面。总览缩略用于理解叙事,清楚原图用于辨认细节。图片无法读取时记录缺口并尝试允许的读取方式;未成功查看不能记为已查看,缺口影响关键判断时再向用户说明。
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
识别各页承担的作用:本品首屏、卖点/使用场景、商品特写、款色选择、店铺/系列总览、关联推荐、通用参数或价格说明。**整套图中出现过的商品,不自动等于本次需要升级的商品。** 综合主角、连续展示的使用关系、文字指向、明确的本品款色及用户说明,先得到本轮范围,再统计形态和配色。
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
- 用户明确清单优先;否则首屏与多个独立卖点/场景形成的商品叙事是重要依据,不能被一张商品总览直接覆盖。
|
|
18
|
+
- 店铺总览和关联推荐不自动扩充本轮范围;其中与本轮商品对应的格子仍可提供辅助外观依据。不得仅因某页是宫格就排除其全部内容,也不能把任何款色表都当店铺推荐。
|
|
19
|
+
- 不按出现次数投票。只出现一次但有明确本品款色/规格依据的颜色仍应纳入;同款重复摆放、道具和推荐商品不因此变为本轮款式。
|
|
20
|
+
- 证据充分时直接给出范围结论并准备卡片。真正无法判断某款是否属于本次、且影响卡片数量时,先完成已有分析,再通过原生问答工具问一个必要的范围问题。不要把可自行判断的整套商品清单交给用户重新筛选。
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
| --- | --- | --- | --- |
|
|
22
|
-
| 长条兔 | 粉色、奶白 | 俯卧细长圆柱身体,长软垂耳、小黑眼、小浅橙粉鼻、短爪、小圆尾;浅而不规则短绒块面 | `assets/products/rabbit.jpg` |
|
|
23
|
-
| 长条熊 | 浅杏、奶白 | 同款两色,短圆耳、略长且收窄口鼻、深色椭圆鼻、短爪;不能变兔耳或短身坐姿泰迪 | `assets/products/bear.jpg` |
|
|
24
|
-
| 长条河马 | 粉色 | 宽厚前突吻部、小圆耳、小黑眼;独立柔软布下颌、少量白布牙;细短平顺绒面、细短尾,不能球尾 | `assets/products/hippo.jpg` |
|
|
25
|
-
| 长条小狗 | 栗棕色 | 身体绒条沿长轴凸起延伸,垂耳纹理方向依实拍;黑鼻、小黑眼、少量嘴边绣点、短爪、短条尾 | `assets/products/dog.jpg` |
|
|
22
|
+
典型判断:首屏、连续卖点及后续产品展示均围绕白/棕小熊,只有一张混合总览出现兔、河马和狗;在无相反本品清单的情况下,应识别小熊一形态两配色,总览其余商品不自动纳入。这个结论来自全套内容,不能把“只做小熊”或某个页码写成其他任务的默认值。
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
## 建立简短识别记录
|
|
28
25
|
|
|
29
|
-
|
|
26
|
+
使用稳定的 `group_id` 和 `variant_id`,例如 `moon`、`moon-green`、`moon-purple`。简短记录:
|
|
27
|
+
|
|
28
|
+
| 记录 | 用途 |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| 各页作用、本轮纳入/排除的商品及理由 | 区分主推对象与店铺总览,说明本轮范围如何得到 |
|
|
31
|
+
| 形态名、配色名、来源图片 | 让用户能核对每个判断,避免漏款与重复 |
|
|
32
|
+
| 整体比例、轮廓、部件位置、纹理和标识 | 约束后续卡片与摄影 |
|
|
33
|
+
| 可见依据、合理补全部位、尚有歧义 | 区分直接观察与重建设定 |
|
|
34
|
+
| 卡片路径、版本、用户确认状态 | 后续只传入最终认可版本 |
|
|
35
|
+
| 卖点/使用场景及来源 | 提供 12 页讨论基础,不提前定四格动作 |
|
|
36
|
+
|
|
37
|
+
不要根据内置历史系列默认四款六色、法兰绒、PP 棉或旧尺寸。实际查看用户图片,不能只按文件名认款。若有明确商品清单或用户更正,以其为先并指出与旧图的冲突。
|
|
38
|
+
|
|
39
|
+
详细记录放入本轮项目文档,用户界面只给简短结论、必要图片和问答卡。用户纠正 Agent 的误识别时,记录为识别纠正,不把原本错误的范围描述成用户临时缩减需求。
|
|
40
|
+
|
|
41
|
+
## 为每个款色选择并实际传入多视角依据
|
|
42
|
+
|
|
43
|
+
从整套旧图选择能够互相补充的清楚画面,按可用资料覆盖:整体长宽与头身比例、头脸/五官、侧背/四肢尾部、面料纹理及本款颜色。**有多个有效视角时,必须选择并实际传入互补参考,不能只用一张总览裁图让模型补出全部角度。** 不硬凑固定数量,不把同一视角的重复图当互补信息,也不无差别传入整套页面。
|
|
44
|
+
|
|
45
|
+
可以裁取目标商品以减少人物、文字和其他商品干扰,但保留需要判断的整体轮廓、接触结构和有效清晰度;保留原图与裁图对应关系。不能因为某张是无人小图就优先于其他清楚的商品画面,不能把小图放大当作新增细节。原图里虽有人物但可辨的商品部位仍可用,明确人物/场景不迁移。
|
|
46
|
+
|
|
47
|
+
各来源不一致时,先对照清晰度、遮挡/透视、光线及与其他本品图的吻合程度。对商品外观优先采用清楚且互相支持的本品依据;单张低清总览、风格化示意或泛称材质文字不能覆盖多张清楚的纹理和结构。若仍有影响造型/面料/颜色的实质冲突,通过按钮展示候选解释或参考选择再确认,不能把冲突图片混合成平均外观。
|
|
48
|
+
|
|
49
|
+
每款色建立有序参考清单,注明各图负责约束什么、哪些内容不迁移。确需借同形态另一配色补结构时,明确只借结构,颜色仍以目标款色依据为准。卡片拼版样例只负责版式。提交时核对实际传入列表与清单一致:**Agent 自己看过图片,不等于生图模型已经收到图片。** 按当前工具参考上限保留有用的互补依据;若资料实际只有一个有效视角,记录这一限制,仍可按下文合理补全,不声称使用了多视角。
|
|
50
|
+
|
|
51
|
+
## 产品卡样式与生成
|
|
52
|
+
|
|
53
|
+
卡片样式原图已包含在本包:
|
|
54
|
+
|
|
55
|
+
- `assets/products/dog.jpg`:横向多图拼版,整体侧面、不同观察方向、头脸与纹理细节。
|
|
56
|
+
- `assets/products/rabbit.jpg`:整体轮廓、背面/尾部、头部和面料近摄。
|
|
57
|
+
|
|
58
|
+
它们对应本次用户提供的示例表达。只学习拼卡与观察方式;除非当前确为同款,不把示例动物的形态、颜色、鼻眼、长条比例或面料作为当前商品依据。
|
|
59
|
+
|
|
60
|
+
每张卡只展示一个款色,默认横向 6–7 个互补视图:至少有可识别整体形态的视图,再分配侧背、关键结构和材质近摄。格数按可辨识性适度调整,不为凑格数臆造配件。视图之间保持同一轮廓、比例、结构与颜色,用干净中性浅背景、柔和清楚的光线和细分隔;无需人物、标题、品牌标语、尺寸线或未经确认的数值。
|
|
61
|
+
|
|
62
|
+
通过当前 SealSeek 的参考图生成能力制作**一张完整拼卡图片**。实际输入上一步选定的有序商品参考,必要时附示例卡并明确其仅负责拼版;无需当前模特卡和整套品牌详情页。提示词保留参考可见的具体外观,不用“圆耳、长圆柱、羊羔绒”等泛化标签替代真实比例、五官形状与纹理。工具支持的比例、参考数量和实际分辨率以实时合同为准。卡片属于前置产品资料,不受每屏 A/B/C 数量规则影响:每款色先一张,不默认出三套卡。
|
|
63
|
+
|
|
64
|
+
已有用户制作的合格卡片时可直接采用并核对,避免无意义再生。来源提供多种颜色而确认清单只需部分颜色时,按明确范围制作。
|
|
65
|
+
|
|
66
|
+
## 无实物与生成补全
|
|
67
|
+
|
|
68
|
+
允许根据旧图生成未展示的角度、遮挡部分及合理结构;不以“没有实物”或“缺少背面照片”自动阻塞。**合理补全只用于资料缺失之处,已有清楚依据的部位应尽量保留。** 保护已知轮廓与部件,采用与现有证据相容的补全,简要标明例如“背部接缝依据可见结构补全,请确认”。无法区分到底几款/几色、或已有图存在未解决的实质矛盾时,通过原生问答工具先问最少的关键问题,避免为猜错款色付费。
|
|
69
|
+
|
|
70
|
+
卡片旁保留补全说明,不把它写成实拍已验证。用户确认后,该卡是本轮视觉制作认可的产品设定;以后用它约束造型、颜色和材质外观。用户也可自行修图、重画或上传替代卡,沿用同一 `variant_id` 更新版本。保留旧版本,不混用未确认的新卡。
|
|
71
|
+
|
|
72
|
+
视觉卡片不能自动确立材料成分、具体尺寸、载荷、认证或医疗功效。这里不生成参数页面,16–19 仍由同事处理。
|
|
73
|
+
|
|
74
|
+
## 生成后先对照原图验收
|
|
75
|
+
|
|
76
|
+
实际打开生成卡及选定的原始商品依据,检查:
|
|
77
|
+
|
|
78
|
+
- 整体轮廓与头身比例、头脸/眼鼻/耳朵的位置和形状、前后肢及尾部是否接近可见原图;不只检查“还是一只熊”等类别特征。
|
|
79
|
+
- 本款颜色、绒面颗粒/卷曲的大小和方向、起伏与光泽是否接近;不能把明显的大块卷曲统一改成细密颗粒绒,或把有依据的鼻部质感改成通用玩具造型。
|
|
80
|
+
- 同一卡各视角能否解释为同一件商品;同形态各配色是否保留应当一致的结构。没有依据时,不能一色有后脚、另一色没有;真实存在的结构差异则保留。
|
|
81
|
+
- 每个新角度中哪些部分来自原图、哪些属于补全;卡内能否清楚辨认整体和关键部件。
|
|
82
|
+
|
|
83
|
+
先排除透视、受力与光线造成的合理差异,再判断外观偏离。图像精美、拼版整齐或生成成功不代表产品接近。发现明显偏差时定位参考选取、传入缺失或提示词原因,在已授权范围内修正对应卡;需要额外付费且未获授权时用问答工具说明问题、范围和费用并等待选择,不无限重生。尚未修正的明显偏差不能标为合格,也不能交给后续详情页继续放大。
|
|
84
|
+
|
|
85
|
+
保留精简对照结论和补全部位,不承诺无实物条件下完全还原。卡片通过已知外观对照后,再进入用户统一确认;用户可直接修订或上传替代卡。用户确认不替代 Agent 的这次原图对照检查。
|
|
86
|
+
|
|
87
|
+
若用户明确指定采用与旧图不同的修订外观,记录其主动调整并以确认版本作为新设定;不要把用户指定的改动误判成未修正偏差。
|
|
88
|
+
|
|
89
|
+
## 卡片出齐后的统一确认(进入规划前)
|
|
90
|
+
|
|
91
|
+
展示所有卡片,并用一句可核对的结果开头,例如:
|
|
92
|
+
|
|
93
|
+
> 本次识别为 1 种月亮造型,绿色/紫色 2 个配色;产品卡 2 张,款式展示模块 1 页。
|
|
94
|
+
|
|
95
|
+
展示必要的补全部位和卖点候选,再调用原生问答工具集中收集确认与更正。例如产品问题选项可为“卡片与数量正确”“需要修改卡片或范围”“我会上传修订卡”;卖点问题按实际候选提供采用/调整的选择,并使用宿主自定义回答能力。不把全部问题写进大表格末尾让用户寻找。用户未回复不算确认;仅认可其中一张不能把其他卡自动标为已确认。确认可以一次覆盖全部事项,不要求用户逐张发送固定句式。
|
|
96
|
+
|
|
97
|
+
12 页始终规划四个图片位,但不强求四个互不重复的“功效”。根据用户确认的卖点及用途形成四个明确展示任务;卖点较少时可提出同一卖点的不同使用场景供确认,较多时请用户选重点或认可 Agent 的优先建议。不能自行填入原模板四种睡姿或编造功能凑数。
|
|
98
|
+
|
|
99
|
+
收到完整确认及本轮需求后再开始整套规划。记录用户确认的卡片版本、产品分组、配色、卖点和特殊要求,后续从该状态继续;只重问变化或尚未确认的部分。
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 每屏三案规划(兼容入口)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
同题三案的规则统一以该章为准:共同设计任务的提取、保留项/转换项/变化项的区分、品牌要求的落实方式、每案的完整调度、生成前的核对,以及生成后的单案与多案验收,均见 photography-method.md 的“摄影任务识别”“品牌正例的选择与使用”“工作流程”和“验收与定向修正”各节。
|
|
6
|
-
|
|
7
|
-
本文件不再维护独立的第二套三案方法。需要三案规则时直接阅读 photography-method.md,避免两处并存产生冲突。
|
|
3
|
+
当前流程按新版模板规划,不再逐张转换旧源页。页面职责与分组见 [页面映射](page-patterns.md),每屏 A/B/C、生成范围与交付数量见 [规划与清单](planning.md);摄影调度与同题差异见 [摄影方法](photography-method.md)。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import {comparisonRow} from './comparison-layout.mjs';
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
if (args.length !== 2 || args[0] !== '--input') throw new Error('Usage: node canvas-row.mjs --input ROW.json');
|
|
7
|
+
const input = JSON.parse(fs.readFileSync(args[1], 'utf8').replace(/^\uFEFF/, ''));
|
|
8
|
+
console.log(JSON.stringify(comparisonRow(input), null, 2));
|
|
9
|
+
} catch (error) {
|
|
10
|
+
console.error(JSON.stringify({ok:false, code:error.code || 'INVALID_INPUT', message:error.message}));
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {mediaDependency} from './media-dependency.mjs';
|
|
2
|
+
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
if (args.length && !(args.length === 2 && args[0] === '--package-root' && args[1])) {
|
|
5
|
+
console.error(JSON.stringify({ok:false, code:'INVALID_ARGUMENTS', message:'Usage: node check-media.mjs [--package-root VERIFIED_PACKAGE_ROOT]'}));
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
const result = mediaDependency({packageRoot: args[1]});
|
|
9
|
+
console.log(JSON.stringify(result, null, 2));
|
|
10
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { fail } from './runtime.mjs';
|
|
2
|
+
|
|
3
|
+
// Application layout only: service calls, uploads and verified writes belong
|
|
4
|
+
// to sealseek-media-cli. The original fits the result's row without cropping.
|
|
5
|
+
export function comparisonRow(input) {
|
|
6
|
+
if(!input || typeof input!=='object' || Array.isArray(input)) throw fail('INVALID_INPUT','Expected a comparison row object.');
|
|
7
|
+
const {run_id,row_index,top=0,source,result}=input;
|
|
8
|
+
if(typeof run_id!=='string'||!/^[a-zA-Z0-9_-]{1,80}$/.test(run_id)) throw fail('INVALID_INPUT','run_id must be a stable 1-80 character ASCII operation key.');
|
|
9
|
+
if(!Number.isSafeInteger(row_index)||row_index<1||row_index>10000||!Number.isFinite(top)||top<0||top>1e9) throw fail('INVALID_INPUT','Invalid row index or top coordinate.');
|
|
10
|
+
for(const asset of [source,result]) {
|
|
11
|
+
if(!asset||![asset.width,asset.height].every(v=>Number.isSafeInteger(v)&&v>0)) throw fail('INVALID_INPUT','Both assets need measured positive integer dimensions.');
|
|
12
|
+
let url;try{url=new URL(asset.url);}catch{throw fail('INVALID_INPUT','Both assets need uploaded HTTP(S) URLs.');}
|
|
13
|
+
if(!['https:','http:'].includes(url.protocol)||url.username||url.password) throw fail('INVALID_INPUT','Asset URLs must use HTTP(S) without embedded credentials.');
|
|
14
|
+
}
|
|
15
|
+
const width=480,horizontalGap=24,verticalGap=32;
|
|
16
|
+
const height=width*result.height/result.width;
|
|
17
|
+
const scale=Math.min(width/source.width,height/source.height);
|
|
18
|
+
const sourceWidth=source.width*scale,sourceHeight=source.height*scale;
|
|
19
|
+
if(![height,sourceWidth,sourceHeight].every(n=>Number.isFinite(n)&&n>0&&n<=100000)||top+height+verticalGap>1e9) throw fail('LAYOUT_UNSUPPORTED','Image geometry exceeds the canvas contract.');
|
|
20
|
+
const rects=[{x:(width-sourceWidth)/2,y:top,width:sourceWidth,height:sourceHeight},{x:width+horizontalGap,y:top,width,height}];
|
|
21
|
+
const media=[source,result].map((asset,index)=>({
|
|
22
|
+
id:`${run_id}-row-${row_index}-${index===0?'source':'result'}`,kind:'image',urls:[asset.url],
|
|
23
|
+
placement:{mode:'explicit',positions:[rects[index]]}
|
|
24
|
+
}));
|
|
25
|
+
if(result.generationParams!==undefined) {
|
|
26
|
+
if(!result.generationParams||typeof result.generationParams!=='object'||Array.isArray(result.generationParams)) throw fail('INVALID_INPUT','generationParams must be an object accepted by the media CLI.');
|
|
27
|
+
media[1].generationParams=result.generationParams;
|
|
28
|
+
}
|
|
29
|
+
return {ok:true,row_index,top,next_top:top+height+verticalGap,layout:{column_width:width,horizontal_gap:horizontalGap,vertical_gap:verticalGap},batch:{frames:[],media}};
|
|
30
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const name = '@petercjl/sealseek-media-cli';
|
|
7
|
+
const prompt = `请安装 npm 包 ${name}。`;
|
|
8
|
+
|
|
9
|
+
// Invoke the verified package entry with the active Node runtime, including on
|
|
10
|
+
// Windows where an npm .cmd shim is not a directly executable program.
|
|
11
|
+
export function mediaDependency({ env = process.env, packageRoot } = {}) {
|
|
12
|
+
const missing = (code, message) => ({ok:false,code,package:name,message,installPrompt:prompt,action:'pause'});
|
|
13
|
+
const roots=[];
|
|
14
|
+
if(packageRoot || env.CLOUD_HUG_MEDIA_ROOT) roots.push(packageRoot || env.CLOUD_HUG_MEDIA_ROOT);
|
|
15
|
+
else {
|
|
16
|
+
try { roots.push(path.dirname(createRequire(import.meta.url).resolve(`${name}/package.json`))); } catch {}
|
|
17
|
+
for(const dir of (env.PATH || '').split(path.delimiter).filter(Boolean)) {
|
|
18
|
+
roots.push(path.join(dir,'node_modules',...name.split('/')),path.join(dir,'..','lib','node_modules',...name.split('/')));
|
|
19
|
+
try {
|
|
20
|
+
const entry=fs.realpathSync(path.join(dir,'sealseek-media'));
|
|
21
|
+
roots.push(path.resolve(path.dirname(entry),'..'));
|
|
22
|
+
} catch {}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for(const candidate of [...new Set(roots)]) {
|
|
26
|
+
if(!fs.existsSync(path.join(candidate,'package.json'))) continue;
|
|
27
|
+
try {
|
|
28
|
+
const root=fs.realpathSync(candidate),pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));
|
|
29
|
+
if(pkg.name!==name) continue;
|
|
30
|
+
const parts=pkg.version?.split('.').map(Number);
|
|
31
|
+
if(!/^\d+\.\d+\.\d+$/.test(pkg.version)||!(parts[0]>0||parts[1]>6||(parts[1]===6&&parts[2]>=1))) return missing('DEPENDENCY_INCOMPATIBLE','sealseek-media-cli >=0.6.1 is required; update the package.');
|
|
32
|
+
if(typeof pkg.bin?.['sealseek-media']!=='string') return missing('DEPENDENCY_INVALID','Media CLI entry is missing.');
|
|
33
|
+
const entry=fs.realpathSync(path.resolve(root,pkg.bin['sealseek-media']));
|
|
34
|
+
if(!entry.startsWith(root+path.sep)) return missing('DEPENDENCY_INVALID','Media CLI entry must belong to its package.');
|
|
35
|
+
const result=spawnSync(process.execPath,[entry,'skill','source','--json'],{encoding:'utf8',timeout:15000,maxBuffer:1024*1024,env:{...env,SEALSEEK_MEDIA_AUTO_UPDATE:'0'}});
|
|
36
|
+
const data=JSON.parse(result.stdout||'null');
|
|
37
|
+
if(result.status!==0||data?.ok!==true||data.name!=='sealseek-media'||data.version!==pkg.version||typeof data.source!=='string') return missing('DEPENDENCY_INVALID','Media CLI canonical Skill discovery failed.');
|
|
38
|
+
const source=fs.realpathSync(data.source);
|
|
39
|
+
if(!source.startsWith(root+path.sep)||!/^name: sealseek-media\s*$/m.test(fs.readFileSync(path.join(source,'SKILL.md'),'utf8'))) return missing('DEPENDENCY_INVALID','Media companion Skill identity is invalid.');
|
|
40
|
+
const contract=JSON.parse(fs.readFileSync(path.join(source,'capabilities.json'),'utf8'));
|
|
41
|
+
const generate=contract.capabilities?.find(c=>c.id==='media.generate');
|
|
42
|
+
if(!['image','reference-image','dry-run','task-record','local-artifact'].every(f=>generate?.required_features?.includes(f))) return missing('DEPENDENCY_INCOMPATIBLE','Media generation contract is incomplete.');
|
|
43
|
+
const canvas=contract.canvas_contract;
|
|
44
|
+
if(canvas?.version!==1||!['explicit-placement','frame-batch','media-inspect'].every(f=>canvas.features?.includes(f))||!canvas.operations?.create||!canvas.operations?.upload?.some(c=>c.startsWith('image upload '))||!canvas.generation_options?.archive?.enum?.includes('none')) return missing('DEPENDENCY_INCOMPATIBLE','Media canvas creation, upload, inspection and explicit batch placement are required.');
|
|
45
|
+
return {ok:true,package:name,version:pkg.version,entry,source,invocation:{command:process.execPath,args:[entry]},serviceAccess:'requires-live-preflight',generationTested:false};
|
|
46
|
+
} catch { return missing('DEPENDENCY_INVALID','Media package, CLI or canonical Skill could not be verified.'); }
|
|
47
|
+
}
|
|
48
|
+
return missing('DEPENDENCY_MISSING','Required sealseek-media-cli package was not detected. Pause the Skill until it is installed and verified.');
|
|
49
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
4
|
+
|
|
5
|
+
const directions = ['A', 'B', 'C'];
|
|
6
|
+
const keyPattern = /^[a-z0-9][a-z0-9_-]{0,79}$/;
|
|
7
|
+
const fail = message => { throw new Error(message); };
|
|
8
|
+
|
|
9
|
+
// Enumerates files only. It neither infers products nor grants generation approval.
|
|
10
|
+
export function planInventory(input) {
|
|
11
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) fail('Expected an input object.');
|
|
12
|
+
const {groups, mode = 'assets', selected_pages} = input;
|
|
13
|
+
if (!['assets', 'pages'].includes(mode)) fail('mode must be assets or pages.');
|
|
14
|
+
if (!Array.isArray(groups) || !groups.length) fail('groups must contain at least one confirmed shape group.');
|
|
15
|
+
const seenGroups = new Set(), seenVariants = new Set();
|
|
16
|
+
for (const group of groups) {
|
|
17
|
+
if (!group || typeof group.id !== 'string' || !keyPattern.test(group.id) || seenGroups.has(group.id)) fail('Each group needs a unique ASCII id.');
|
|
18
|
+
seenGroups.add(group.id);
|
|
19
|
+
if (!Array.isArray(group.variants) || !group.variants.length) fail('Each group needs at least one variant id.');
|
|
20
|
+
for (const variant of group.variants) {
|
|
21
|
+
if (typeof variant !== 'string' || !keyPattern.test(variant) || seenVariants.has(variant)) fail('Variant ids must be unique ASCII keys.');
|
|
22
|
+
seenVariants.add(variant);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const single = id => ({page_id:id, template:id, group_id:null, slots:['main']});
|
|
26
|
+
const pages = [
|
|
27
|
+
...['03','04','05','06'].map(single),
|
|
28
|
+
...groups.map(group => ({page_id:`07_${group.id}`, template:'07-10', group_id:group.id, slots:['top','bottom-left','bottom-right']})),
|
|
29
|
+
single('11'),
|
|
30
|
+
{page_id:'12', template:'12', group_id:null, slots:['top-left','top-right','bottom-left','bottom-right']},
|
|
31
|
+
...['13','14','15'].map(single)
|
|
32
|
+
];
|
|
33
|
+
const allowed = new Set(pages.map(page => page.page_id));
|
|
34
|
+
let requested = allowed;
|
|
35
|
+
if (selected_pages !== undefined) {
|
|
36
|
+
if (!Array.isArray(selected_pages) || !selected_pages.length || new Set(selected_pages).size !== selected_pages.length) fail('selected_pages must be a nonempty list of unique page ids.');
|
|
37
|
+
for (const id of selected_pages) if (!allowed.has(id)) fail(`Unknown or excluded page: ${id}`);
|
|
38
|
+
requested = new Set(selected_pages);
|
|
39
|
+
}
|
|
40
|
+
const selected = pages.filter(page => requested.has(page.page_id));
|
|
41
|
+
const outputs = [];
|
|
42
|
+
for (const page of selected) {
|
|
43
|
+
for (const direction of directions) {
|
|
44
|
+
for (const slot of mode === 'pages' ? ['page'] : page.slots) {
|
|
45
|
+
outputs.push({page_id:page.page_id, template:page.template, group_id:page.group_id, direction, slot, file_key:`${page.page_id}_${direction}_${slot}`});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
ok:true,
|
|
51
|
+
generation_authorized:false,
|
|
52
|
+
note:'Inventory only; require confirmed products, approved plans and selected generation scope before execution.',
|
|
53
|
+
mode,
|
|
54
|
+
shape_groups:groups.length,
|
|
55
|
+
product_cards:seenVariants.size,
|
|
56
|
+
all_page_count:pages.length,
|
|
57
|
+
selected_page_count:selected.length,
|
|
58
|
+
selected_photo_slots:selected.reduce((sum, page) => sum + page.slots.length, 0),
|
|
59
|
+
output_count:outputs.length,
|
|
60
|
+
excluded_templates:['01','02','16','17','18','19'],
|
|
61
|
+
pages:selected,
|
|
62
|
+
outputs
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
67
|
+
try {
|
|
68
|
+
const args = process.argv.slice(2);
|
|
69
|
+
if (args.length !== 2 || args[0] !== '--input') fail('Usage: node plan-inventory.mjs --input INVENTORY.json');
|
|
70
|
+
const input = JSON.parse(fs.readFileSync(args[1], 'utf8').replace(/^\uFEFF/, ''));
|
|
71
|
+
console.log(JSON.stringify(planInventory(input), null, 2));
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error(JSON.stringify({ok:false, code:'INVALID_INPUT', message:error.message}));
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
}
|
|
76
|
+
}
|