@taloswork/ose-shared 0.1.4 → 0.1.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taloswork/ose-shared",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Talos cross-OSE shared library — skills + MCP servers auto-loaded into every OSE (P93 2026-07-27).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -10,6 +10,6 @@
|
|
|
10
10
|
"publishConfig": {
|
|
11
11
|
"access": "public"
|
|
12
12
|
},
|
|
13
|
-
"_talosPublishHash": "sha256-
|
|
14
|
-
"_talosPublishedAt": "2026-09-
|
|
13
|
+
"_talosPublishHash": "sha256-CLI4WbNNvJRRbZYx7qniS0d-cVs5XiJlcU904jHxDww",
|
|
14
|
+
"_talosPublishedAt": "2026-09-06T18:53:54.545Z"
|
|
15
15
|
}
|
|
@@ -147660,6 +147660,7 @@ async function imagesToPptx(imagesBase64Png) {
|
|
|
147660
147660
|
}
|
|
147661
147661
|
|
|
147662
147662
|
// packages/core/dist/document/html-pptx-walker.js
|
|
147663
|
+
var DOCUMENT_CAPTURE_DENSITY = 2;
|
|
147663
147664
|
var LAYOUT_WALKER_JS = `
|
|
147664
147665
|
(() => {
|
|
147665
147666
|
const sections = [...document.querySelectorAll("section.slide")];
|
|
@@ -148118,13 +148119,22 @@ async function withHeadlessPage(html, fn) {
|
|
|
148118
148119
|
if (!chromium) return null;
|
|
148119
148120
|
const browser2 = await chromium.launch();
|
|
148120
148121
|
try {
|
|
148121
|
-
const page = await browser2.newPage({
|
|
148122
|
+
const page = await browser2.newPage({
|
|
148123
|
+
viewport: { width: 1280, height: 720 },
|
|
148124
|
+
deviceScaleFactor: DOCUMENT_CAPTURE_DENSITY
|
|
148125
|
+
});
|
|
148122
148126
|
await page.setContent(html, { waitUntil: "load" });
|
|
148123
148127
|
await page.evaluate("document.fonts && document.fonts.ready").catch(() => {
|
|
148124
148128
|
});
|
|
148125
148129
|
const adapter = {
|
|
148126
148130
|
evaluate: (script) => page.evaluate(script),
|
|
148127
|
-
screenshot: (clip) =>
|
|
148131
|
+
screenshot: async (clip) => {
|
|
148132
|
+
const png = await page.screenshot({ clip });
|
|
148133
|
+
const w = png.readUInt32BE(16), h = png.readUInt32BE(20);
|
|
148134
|
+
const want = [clip.width * DOCUMENT_CAPTURE_DENSITY, clip.height * DOCUMENT_CAPTURE_DENSITY];
|
|
148135
|
+
if (w !== want[0] || h !== want[1]) throw new Error(`captured ${w}x${h}, expected ${want[0]}x${want[1]}`);
|
|
148136
|
+
return png;
|
|
148137
|
+
}
|
|
148128
148138
|
};
|
|
148129
148139
|
return await fn(adapter);
|
|
148130
148140
|
} finally {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
© 2025 Anthropic, PBC. All rights reserved.
|
|
2
|
+
|
|
3
|
+
LICENSE: Use of these materials (including all code, prompts, assets, files,
|
|
4
|
+
and other components of this Skill) is governed by your agreement with
|
|
5
|
+
Anthropic regarding use of Anthropic's services. If no separate agreement
|
|
6
|
+
exists, use is governed by Anthropic's Consumer Terms of Service or
|
|
7
|
+
Commercial Terms of Service, as applicable:
|
|
8
|
+
https://www.anthropic.com/legal/consumer-terms
|
|
9
|
+
https://www.anthropic.com/legal/commercial-terms
|
|
10
|
+
Your applicable agreement is referred to as the "Agreement." "Services" are
|
|
11
|
+
as defined in the Agreement.
|
|
12
|
+
|
|
13
|
+
ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
|
|
14
|
+
contrary, users may not:
|
|
15
|
+
|
|
16
|
+
- Extract these materials from the Services or retain copies of these
|
|
17
|
+
materials outside the Services
|
|
18
|
+
- Reproduce or copy these materials, except for temporary copies created
|
|
19
|
+
automatically during authorized use of the Services
|
|
20
|
+
- Create derivative works based on these materials
|
|
21
|
+
- Distribute, sublicense, or transfer these materials to any third party
|
|
22
|
+
- Make, offer to sell, sell, or import any inventions embodied in these
|
|
23
|
+
materials
|
|
24
|
+
- Reverse engineer, decompile, or disassemble these materials
|
|
25
|
+
|
|
26
|
+
The receipt, viewing, or possession of these materials does not convey or
|
|
27
|
+
imply any license or right beyond those expressly granted above.
|
|
28
|
+
|
|
29
|
+
Anthropic retains all right, title, and interest in these materials,
|
|
30
|
+
including all copyrights, patents, and other intellectual property rights.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: image-layout
|
|
3
|
+
description: "Use this skill whenever the user wants EXISTING images combined or altered as pixels — 拼图 / 拼接 / 拼一张 / 宫格 / 九宫格 / 长图 / 对比图 / 前后对比 / 加水印 / 加标签 / 加边框 / 裁剪 / 圆角 / 缩放, or stitch / combine / merge images / collage / contact sheet / side-by-side / before-after / watermark / label / crop / round corners / resize. Examples: 'combine these four into one picture', '把生成的几张图拼成九宫格', 'add a caption under each'. Method: HTML+CSS rendered by this skill's bin/html2img.cjs. The mechanical test: if the images do not exist as files yet, this is NOT the skill. Do NOT trigger for: generating NEW images, INCLUDING when the request names a layout word — '生成一张九宫格的图' asks for ONE generated picture whose COMPOSITION is a grid, not for tiling existing files; documents / PDF / PowerPoint (document skill); Xiaohongshu cards (talos-xhs-ops); or upscaling / background removal / content-aware fill, which need a model, not CSS."
|
|
4
|
+
license: Proprietary. LICENSE.txt has complete terms
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Image layout
|
|
8
|
+
|
|
9
|
+
Combining or restyling images that **already exist** — as files on disk. You
|
|
10
|
+
write a layout in HTML+CSS; Chromium renders it to one PNG.
|
|
11
|
+
|
|
12
|
+
**Check first: do the image files exist?** If they do not, stop — the request is
|
|
13
|
+
for generation, not composition, and this skill cannot make imagery. A layout
|
|
14
|
+
word in the request does not change that: "生成一张九宫格的图" asks for ONE
|
|
15
|
+
generated picture whose composition happens to be a grid, the same way "three
|
|
16
|
+
cats" describes a scene rather than an output count. Compose only files you can
|
|
17
|
+
name a path for.
|
|
18
|
+
|
|
19
|
+
There is no ImageMagick / sharp / PIL / ffmpeg to call here, and no `npm i`.
|
|
20
|
+
Do not go looking for one, and do not ask a generation model to "draw a 2×2
|
|
21
|
+
grid" — it redraws the subjects instead of tiling the images you have.
|
|
22
|
+
|
|
23
|
+
## The one command
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
bin/html2img.cjs <in.html> <out.png> [--width N] [--max-height N]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- `--width` (default 1280) is the canvas width in CSS pixels. **You declare it**;
|
|
30
|
+
it must equal the sum of your columns plus gaps and padding.
|
|
31
|
+
- The **height is measured** from the rendered page — never compute it. Captions
|
|
32
|
+
of any length, wrapped titles, variable rows all just work.
|
|
33
|
+
- Local images referenced by `<img src>` and CSS `url()` are collected
|
|
34
|
+
automatically and allowlisted; relative paths resolve against the HTML file's
|
|
35
|
+
own directory, and any filename works, spaces and `~ % [ ]` included.
|
|
36
|
+
**Remote URLs are blocked**, and any image that fails to decode fails the whole
|
|
37
|
+
render — you never get a "finished" sheet with a blank cell in it.
|
|
38
|
+
- Needs the Talos app (Chromium loopback). Headless it exits 69 with a clear
|
|
39
|
+
message; do not try to work around that.
|
|
40
|
+
- Bounds: width 160–4096, rendered height ≤ `--max-height` (default 8192, refused
|
|
41
|
+
not cropped), ≤64 local images per render.
|
|
42
|
+
- Output is always PNG.
|
|
43
|
+
|
|
44
|
+
Then deliver the result with `present_file` — one call, the composed image.
|
|
45
|
+
|
|
46
|
+
## Where the images are
|
|
47
|
+
|
|
48
|
+
Pictures the user attached to the turn are **already on disk**: in `attachments/`
|
|
49
|
+
relative to your working directory (the session workspace), named `image_0.png`,
|
|
50
|
+
`image_1.png`, … — or by their original filename when they had one. The prompt
|
|
51
|
+
also carries an `[Attachment on disk: <path>]` marker for each.
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
ls attachments/
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Start there. **Do not scan the filesystem for them** (`find / -name "*.png"` and
|
|
58
|
+
friends): it is slow, it burns the turn, and the answer was one `ls` away.
|
|
59
|
+
Write your HTML in that same directory so the `<img src>` paths stay short.
|
|
60
|
+
|
|
61
|
+
## Rules that keep the output right
|
|
62
|
+
|
|
63
|
+
- **Give every cell an explicit pixel size.** The canvas width is arithmetic, and
|
|
64
|
+
arithmetic needs numbers: `cols × cell + (cols−1) × gap + 2 × padding`.
|
|
65
|
+
- **CJK text must lead its font stack with `TalosCJK`**, e.g.
|
|
66
|
+
`font-family: TalosCJK, system-ui, sans-serif`. The renderer injects that face
|
|
67
|
+
when the HTML contains CJK; without it Chinese labels come out blank.
|
|
68
|
+
- **Set an explicit `background`** on `body`. An unstyled page captures as white,
|
|
69
|
+
which is rarely what a dark sheet wants.
|
|
70
|
+
- Write the HTML **next to the images**, or reference them by absolute path.
|
|
71
|
+
- Source images keep their own resolution — a 1024px image in a 512px cell is
|
|
72
|
+
downscaled by the browser. Size cells for the output you want.
|
|
73
|
+
|
|
74
|
+
## Recipes
|
|
75
|
+
|
|
76
|
+
Each is a starting point; the layout is ordinary CSS, so change it freely.
|
|
77
|
+
|
|
78
|
+
### Contact sheet / 宫格
|
|
79
|
+
|
|
80
|
+
`cols × cell + (cols−1) × gap + 2 × pad`. Below: 2 × 512 + 20 + 48 = **1092**.
|
|
81
|
+
|
|
82
|
+
```html
|
|
83
|
+
<!doctype html><html><head><meta charset="utf-8"><style>
|
|
84
|
+
html,body{margin:0;background:#0d0d10}
|
|
85
|
+
body{font-family:TalosCJK,system-ui,-apple-system,sans-serif;color:#e8e8ea}
|
|
86
|
+
h1{margin:0;padding:24px 24px 8px;font-size:34px}
|
|
87
|
+
.sheet{display:grid;grid-template-columns:repeat(2,512px);gap:20px;padding:0 24px 24px}
|
|
88
|
+
figure{margin:0;position:relative}
|
|
89
|
+
figure img{width:512px;height:512px;object-fit:cover;display:block;border-radius:14px}
|
|
90
|
+
.tag{position:absolute;top:14px;left:14px;padding:6px 14px;border-radius:999px;
|
|
91
|
+
background:rgba(0,0,0,.55);font-size:20px;font-weight:600}
|
|
92
|
+
figcaption{padding-top:10px;font-size:20px;color:#c9c9d1;text-align:center}
|
|
93
|
+
</style></head><body>
|
|
94
|
+
<h1>四个方案对比</h1>
|
|
95
|
+
<div class="sheet">
|
|
96
|
+
<figure><img src="a.png"><span class="tag">1</span><figcaption>方案 A</figcaption></figure>
|
|
97
|
+
<figure><img src="b.png"><span class="tag">2</span><figcaption>方案 B</figcaption></figure>
|
|
98
|
+
</div>
|
|
99
|
+
</body></html>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Long strip / 长图
|
|
103
|
+
|
|
104
|
+
One column; the height takes care of itself.
|
|
105
|
+
|
|
106
|
+
```css
|
|
107
|
+
.strip{display:grid;grid-template-columns:1080px;gap:0;padding:0}
|
|
108
|
+
.strip img{width:1080px;height:auto;display:block}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Before / after
|
|
112
|
+
|
|
113
|
+
Two cells with labels, or one cell with the "after" clipped to half:
|
|
114
|
+
|
|
115
|
+
```css
|
|
116
|
+
.pair{display:grid;grid-template-columns:repeat(2,640px);gap:2px}
|
|
117
|
+
/* single-frame variant */
|
|
118
|
+
.overlay{position:relative;width:1280px;height:720px}
|
|
119
|
+
.overlay img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
120
|
+
.overlay .after{clip-path:inset(0 0 0 50%)}
|
|
121
|
+
.overlay .seam{position:absolute;left:50%;top:0;bottom:0;width:2px;background:#fff}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Watermark / label / border
|
|
125
|
+
|
|
126
|
+
```css
|
|
127
|
+
.mark{position:absolute;right:20px;bottom:20px;font-size:22px;opacity:.75}
|
|
128
|
+
.framed{border:8px solid #fff;box-shadow:0 8px 30px rgba(0,0,0,.35)}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Crop, scale, round, filter
|
|
132
|
+
|
|
133
|
+
Cropping and scaling are `object-fit` on a sized box; there is no separate crop
|
|
134
|
+
tool.
|
|
135
|
+
|
|
136
|
+
```css
|
|
137
|
+
.crop{width:512px;height:512px;object-fit:cover;object-position:center} /* fill + centre-crop */
|
|
138
|
+
.fit {width:512px;height:512px;object-fit:contain;background:#111} /* letterbox, no crop */
|
|
139
|
+
.round{border-radius:24px}
|
|
140
|
+
.grey{filter:grayscale(1) contrast(1.05)}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## When this is the wrong skill
|
|
144
|
+
|
|
145
|
+
- **Generating a new image** → the media-generation tools. This skill never
|
|
146
|
+
creates imagery, it only arranges what exists.
|
|
147
|
+
- **A PDF / Word / PowerPoint deliverable** → the document skill (`doc-gen`,
|
|
148
|
+
`html2pptx`). Do not build a "poster" here and call it a document.
|
|
149
|
+
- **Xiaohongshu campaign cards** → `talos-xhs-ops` (`render-cards`), which owns
|
|
150
|
+
the 1080×1440 card format and its QA.
|
|
151
|
+
- **Pixel-level algorithms** — background removal, super-resolution,
|
|
152
|
+
content-aware fill. CSS cannot do these; they need a model.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// apps/admin/data/skills.lib/image-layout/bin-src/html2img.mjs
|
|
5
|
+
var import_promises = require("node:fs/promises");
|
|
6
|
+
var import_node_path2 = require("node:path");
|
|
7
|
+
|
|
8
|
+
// packages/core/dist/document/html-local-assets.js
|
|
9
|
+
var import_node_fs = require("node:fs");
|
|
10
|
+
var import_node_path = require("node:path");
|
|
11
|
+
var import_node_url = require("node:url");
|
|
12
|
+
var IMG_SRC = /(<img\b[^>]*?\bsrc\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
13
|
+
var CSS_URL = /(url\(\s*)(?:"([^"]*)"|'([^']*)'|([^)\s]+))(\s*\))/gi;
|
|
14
|
+
function isNonLocal(ref) {
|
|
15
|
+
return ref.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(ref) && !ref.toLowerCase().startsWith("file:");
|
|
16
|
+
}
|
|
17
|
+
function resolveLocalAssets(html, baseDir) {
|
|
18
|
+
const found = /* @__PURE__ */ new Set();
|
|
19
|
+
const rewrite = (ref) => {
|
|
20
|
+
const url = ref === void 0 ? void 0 : localFileUrl(ref.trim(), baseDir);
|
|
21
|
+
if (url)
|
|
22
|
+
found.add(url);
|
|
23
|
+
return url;
|
|
24
|
+
};
|
|
25
|
+
const withCss = html.replace(CSS_URL, (match, prefix, dq, sq, bare, suffix) => {
|
|
26
|
+
const url = rewrite(dq ?? sq ?? bare);
|
|
27
|
+
return url ? `${prefix}"${url}"${suffix}` : match;
|
|
28
|
+
});
|
|
29
|
+
const rewritten = withCss.replace(IMG_SRC, (match, prefix, dq, sq, bare) => {
|
|
30
|
+
const url = rewrite(dq ?? sq ?? bare);
|
|
31
|
+
return url ? `${prefix}"${url}"` : match;
|
|
32
|
+
});
|
|
33
|
+
return { html: rewritten, assetFileUrls: [...found] };
|
|
34
|
+
}
|
|
35
|
+
function localFileUrl(ref, baseDir) {
|
|
36
|
+
if (!ref)
|
|
37
|
+
return void 0;
|
|
38
|
+
let path;
|
|
39
|
+
if (ref.toLowerCase().startsWith("file:")) {
|
|
40
|
+
try {
|
|
41
|
+
path = (0, import_node_url.fileURLToPath)(ref);
|
|
42
|
+
} catch {
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
} else if (isNonLocal(ref)) {
|
|
46
|
+
return void 0;
|
|
47
|
+
} else {
|
|
48
|
+
const bare = ref.split(/[?#]/)[0];
|
|
49
|
+
if (!bare)
|
|
50
|
+
return void 0;
|
|
51
|
+
path = (0, import_node_path.isAbsolute)(bare) ? bare : (0, import_node_path.resolve)(baseDir, bare);
|
|
52
|
+
}
|
|
53
|
+
return (0, import_node_fs.existsSync)(path) ? (0, import_node_url.pathToFileURL)(path).href : void 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// apps/admin/data/skills.lib/image-layout/bin-src/html2img.mjs
|
|
57
|
+
function die(msg, code = 1) {
|
|
58
|
+
process.stderr.write(`html2img: ${msg}
|
|
59
|
+
`);
|
|
60
|
+
process.exit(code);
|
|
61
|
+
}
|
|
62
|
+
function intArg(flag, fallback) {
|
|
63
|
+
const i = process.argv.indexOf(flag);
|
|
64
|
+
if (i < 0) return fallback;
|
|
65
|
+
const value = Number(process.argv[i + 1]);
|
|
66
|
+
if (!Number.isInteger(value)) die(`${flag} needs an integer`, 64);
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
async function main() {
|
|
70
|
+
const input = process.argv[2];
|
|
71
|
+
const output = process.argv[3];
|
|
72
|
+
if (!input || !output || input.startsWith("--") || output.startsWith("--")) {
|
|
73
|
+
die("usage: html2img <in.html> <out.png> [--width N] [--max-height N]", 64);
|
|
74
|
+
}
|
|
75
|
+
const width = intArg("--width", 1280);
|
|
76
|
+
const maxHeight = intArg("--max-height", void 0);
|
|
77
|
+
const url = process.env.TALOS_PDF_URL;
|
|
78
|
+
if (!url) {
|
|
79
|
+
die("rendering needs the Talos app's Chromium service (TALOS_PDF_URL is not set). Run inside the Talos app.", 69);
|
|
80
|
+
}
|
|
81
|
+
let source;
|
|
82
|
+
try {
|
|
83
|
+
source = await (0, import_promises.readFile)(input, "utf8");
|
|
84
|
+
} catch (e) {
|
|
85
|
+
die(`cannot read ${input}: ${e.message}`, 66);
|
|
86
|
+
}
|
|
87
|
+
const { html, assetFileUrls } = resolveLocalAssets(source, (0, import_node_path2.dirname)((0, import_node_path2.resolve)(input)));
|
|
88
|
+
let res;
|
|
89
|
+
try {
|
|
90
|
+
res = await fetch(url.replace(/\/pdf$/, "/img"), {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
"content-type": "application/json",
|
|
94
|
+
...process.env.TALOS_PDF_TOKEN ? { authorization: `Bearer ${process.env.TALOS_PDF_TOKEN}` } : {}
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify({ html, width, ...maxHeight === void 0 ? {} : { maxHeight }, assetFileUrls })
|
|
97
|
+
});
|
|
98
|
+
} catch (e) {
|
|
99
|
+
die(`render request failed: ${e.message}`, 69);
|
|
100
|
+
}
|
|
101
|
+
const text = await res.text();
|
|
102
|
+
if (!res.ok) {
|
|
103
|
+
let detail = text;
|
|
104
|
+
try {
|
|
105
|
+
detail = JSON.parse(text).error ?? text;
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
die(`render failed (${res.status}): ${detail}`);
|
|
109
|
+
}
|
|
110
|
+
let image;
|
|
111
|
+
try {
|
|
112
|
+
image = JSON.parse(text).image;
|
|
113
|
+
} catch {
|
|
114
|
+
die("render returned malformed JSON");
|
|
115
|
+
}
|
|
116
|
+
if (typeof image !== "string" || !image) die("render returned no image");
|
|
117
|
+
const dest = (0, import_node_path2.resolve)(output);
|
|
118
|
+
await (0, import_promises.writeFile)(dest, Buffer.from(image, "base64"));
|
|
119
|
+
process.stdout.write(`${JSON.stringify({ path: dest, width, assets: assetFileUrls.length })}
|
|
120
|
+
`);
|
|
121
|
+
}
|
|
122
|
+
main().catch((e) => die(e?.message ?? String(e)));
|