@gobing-ai/knowledge-kit 0.0.27 → 0.0.29
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/.claude-plugin/marketplace.json +1 -1
- package/package.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +27 -4
- package/plugins/generations/daily-article-gen/src/index.ts +34 -3
- package/plugins/generations/dailynews-gen/dist/index.js +42 -19
- package/plugins/generations/dailynews-gen/src/index.ts +29 -1
- package/plugins/generations/dailynews-gen/src/script-builder.ts +33 -21
- package/plugins/generations/episode-plan-gen/dist/index.js +7 -0
- package/plugins/generations/episode-plan-gen/src/index.ts +12 -0
- package/plugins/generations/image-gen/assets/fonts/NotoSansSC-Bold.otf +0 -0
- package/plugins/generations/image-gen/assets/fonts/NotoSansSC-Regular.otf +0 -0
- package/plugins/generations/image-gen/assets/fonts/OFL.txt +92 -0
- package/plugins/generations/image-gen/assets/fonts/SOURCE.md +15 -0
- package/plugins/generations/image-gen/dist/index.js +597 -16
- package/plugins/generations/image-gen/package.json +1 -0
- package/plugins/generations/image-gen/presets/formats/daily-news-cover.yaml +2 -2
- package/plugins/generations/image-gen/presets/formats/xhs-card.yaml +19 -11
- package/plugins/generations/image-gen/presets/styles/xhs-knowledge.yaml +2 -1
- package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +135 -0
- package/plugins/generations/image-gen/src/bytes.ts +28 -0
- package/plugins/generations/image-gen/src/compose/card.ts +294 -0
- package/plugins/generations/image-gen/src/compose/chromium.ts +147 -0
- package/plugins/generations/image-gen/src/compose/cmap.ts +177 -0
- package/plugins/generations/image-gen/src/index.ts +167 -9
- package/plugins/generations/image-gen/src/job.ts +6 -0
- package/plugins/generations/image-gen/src/presets.ts +53 -13
- package/plugins/generations/image-gen/src/providers/google.ts +9 -1
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/daily-voice-style.zh.md +25 -12
- package/plugins/kk/scripts/kk-workflow-stages.ts +344 -11
- package/plugins/kk/scripts/topic-boundary.json +105 -0
- package/plugins/kk/scripts/validate-voicescript.ts +15 -0
- package/plugins/kk/skills/content-safety/SKILL.md +163 -0
- package/plugins/kk/skills/content-safety/references/platforms/apple-podcasts.md +36 -0
- package/plugins/kk/skills/content-safety/references/platforms/netease-music.md +31 -0
- package/plugins/kk/skills/content-safety/references/platforms/wechatmp.md +32 -0
- package/plugins/kk/skills/content-safety/references/platforms/xiaoyuzhou.md +35 -0
- package/plugins/kk/skills/xhs-images/SKILL.md +11 -7
- package/plugins/kk/skills/xhs-images/references/card-layouts.md +4 -4
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +118 -16
- package/plugins/publishings/podcast-pub/dist/index.js +4 -0
- package/plugins/publishings/podcast-pub/src/index.ts +2 -0
- package/plugins/publishings/podcast-pub/src/show-notes.ts +6 -0
- package/plugins/publishings/wechatmp-pub/dist/index.js +272 -115
- package/plugins/publishings/wechatmp-pub/src/index.ts +336 -40
- package/plugins/publishings/wechatmp-pub/src/profile-check.ts +21 -9
- package/plugins/publishings/xhs-pub/dist/index.js +28 -22
- package/plugins/publishings/xhs-pub/src/index.ts +30 -22
|
@@ -21809,8 +21809,332 @@ function findProjectRoot(startDir) {
|
|
|
21809
21809
|
}
|
|
21810
21810
|
var init_file_system_node = () => {};
|
|
21811
21811
|
|
|
21812
|
+
// ../../plugins/generations/image-gen/src/bytes.ts
|
|
21813
|
+
import { readFile, writeFile } from "fs/promises";
|
|
21814
|
+
async function writeBytes(path2, bytes) {
|
|
21815
|
+
await writeFile(path2, bytes);
|
|
21816
|
+
}
|
|
21817
|
+
async function readBytes(path2) {
|
|
21818
|
+
return new Uint8Array(await readFile(path2));
|
|
21819
|
+
}
|
|
21820
|
+
function imageDimensions(bytes) {
|
|
21821
|
+
const PNG_SIG = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
21822
|
+
if (bytes.byteLength < 24) {
|
|
21823
|
+
throw new Error("imageDimensions: not a PNG \u2014 buffer too small");
|
|
21824
|
+
}
|
|
21825
|
+
for (let i = 0;i < 8; i++) {
|
|
21826
|
+
if (bytes[i] !== PNG_SIG[i]) {
|
|
21827
|
+
throw new Error("imageDimensions: not a PNG \u2014 invalid signature");
|
|
21828
|
+
}
|
|
21829
|
+
}
|
|
21830
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
21831
|
+
const ihdrLength = view.getUint32(8, false);
|
|
21832
|
+
if (ihdrLength !== 13 || bytes[12] !== 73 || bytes[13] !== 72 || bytes[14] !== 68 || bytes[15] !== 82) {
|
|
21833
|
+
throw new Error("imageDimensions: not a PNG \u2014 IHDR chunk not found at offset 8");
|
|
21834
|
+
}
|
|
21835
|
+
const width = view.getUint32(16, false);
|
|
21836
|
+
const height = view.getUint32(20, false);
|
|
21837
|
+
return { width, height };
|
|
21838
|
+
}
|
|
21839
|
+
var init_bytes = () => {};
|
|
21840
|
+
|
|
21841
|
+
// ../../plugins/generations/image-gen/src/compose/card.ts
|
|
21842
|
+
function assertCoverage(input, fonts) {
|
|
21843
|
+
const headlineCover = fonts.get(input.compose.fonts.headline);
|
|
21844
|
+
const bodyCover = fonts.get(input.compose.fonts.body);
|
|
21845
|
+
if (headlineCover === undefined) {
|
|
21846
|
+
throw new Error(`compose: headline font '${input.compose.fonts.headline}' not loaded`);
|
|
21847
|
+
}
|
|
21848
|
+
if (bodyCover === undefined) {
|
|
21849
|
+
throw new Error(`compose: body font '${input.compose.fonts.body}' not loaded`);
|
|
21850
|
+
}
|
|
21851
|
+
const checkField = (text, fieldName, covers) => {
|
|
21852
|
+
for (const ch of text) {
|
|
21853
|
+
const cp = ch.codePointAt(0);
|
|
21854
|
+
if (cp === undefined)
|
|
21855
|
+
continue;
|
|
21856
|
+
if (cp <= 32 || cp === 160 || cp >= 8192 && cp <= 8207 || cp === 12288) {
|
|
21857
|
+
continue;
|
|
21858
|
+
}
|
|
21859
|
+
if (!covers(cp)) {
|
|
21860
|
+
const hex3 = `U+${cp.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
21861
|
+
const fontName = fieldName.startsWith("points") ? input.compose.fonts.body : input.compose.fonts.headline;
|
|
21862
|
+
throw new Error(`compose: ${fieldName} has ${hex3} '${ch}' not covered by ${fontName}`);
|
|
21863
|
+
}
|
|
21864
|
+
}
|
|
21865
|
+
};
|
|
21866
|
+
checkField(input.headline, "headline", headlineCover);
|
|
21867
|
+
for (const [i, point] of input.points.entries()) {
|
|
21868
|
+
checkField(point, `points[${i}]`, bodyCover);
|
|
21869
|
+
}
|
|
21870
|
+
}
|
|
21871
|
+
function assertTextFreePrompt(prompt, copy, sources) {
|
|
21872
|
+
const hit = (text) => {
|
|
21873
|
+
const headline = copy.headline.trim();
|
|
21874
|
+
if (headline.length >= 2 && text.includes(headline))
|
|
21875
|
+
return `the headline '${headline}'`;
|
|
21876
|
+
for (const point of copy.points) {
|
|
21877
|
+
const trimmed = point.trim();
|
|
21878
|
+
if (trimmed.length >= 2 && text.includes(trimmed))
|
|
21879
|
+
return `point '${trimmed}'`;
|
|
21880
|
+
}
|
|
21881
|
+
const han = /\p{Script=Han}/u.exec(text);
|
|
21882
|
+
if (han !== null) {
|
|
21883
|
+
const cp = (han[0]?.codePointAt(0) ?? 0).toString(16).toUpperCase().padStart(4, "0");
|
|
21884
|
+
return `Han character '${han[0]}' (U+${cp})`;
|
|
21885
|
+
}
|
|
21886
|
+
return null;
|
|
21887
|
+
};
|
|
21888
|
+
const fail = (source, what2) => {
|
|
21889
|
+
throw new Error(`compose: the illustration prompt must stay text-free \u2014 ${source} contributes ${what2} (card copy is typeset, never prompted)`);
|
|
21890
|
+
};
|
|
21891
|
+
for (const { source, text } of sources) {
|
|
21892
|
+
const what2 = hit(text);
|
|
21893
|
+
if (what2 !== null)
|
|
21894
|
+
fail(source, what2);
|
|
21895
|
+
}
|
|
21896
|
+
const what = hit(prompt);
|
|
21897
|
+
if (what !== null)
|
|
21898
|
+
fail("the composed prompt", what);
|
|
21899
|
+
}
|
|
21900
|
+
function cardHtml(input, fontUrls) {
|
|
21901
|
+
const { compose: cfg, width, height, illustration } = input;
|
|
21902
|
+
const { safeArea, headlineMinPx, bodyMinPx, illustrationHeight, palette } = cfg;
|
|
21903
|
+
const headlineFont = fontUrls.get(cfg.fonts.headline) ?? "";
|
|
21904
|
+
const bodyFont = fontUrls.get(cfg.fonts.body) ?? "";
|
|
21905
|
+
const headlineStartPx = headlineMinPx * 1.5;
|
|
21906
|
+
const textPanelHeight = height - illustrationHeight;
|
|
21907
|
+
const safeWidth = width - safeArea * 2;
|
|
21908
|
+
const illustrationSrc = illustration ? `data:${illustration.mime};base64,${Buffer.from(illustration.bytes).toString("base64")}` : "";
|
|
21909
|
+
const pointsHtml = input.points.map((pt, i) => `<div class="point" data-field="points[${i}]" style="font-size:${bodyMinPx}px;">${escHtml(pt)}</div>`).join(`
|
|
21910
|
+
`);
|
|
21911
|
+
return `<!DOCTYPE html>
|
|
21912
|
+
<html lang="zh-Hans">
|
|
21913
|
+
<head>
|
|
21914
|
+
<meta charset="utf-8">
|
|
21915
|
+
<style>
|
|
21916
|
+
@font-face {
|
|
21917
|
+
font-family: 'NotoSansSC-Bold';
|
|
21918
|
+
src: url('${headlineFont}') format('opentype');
|
|
21919
|
+
font-weight: bold;
|
|
21920
|
+
font-style: normal;
|
|
21921
|
+
}
|
|
21922
|
+
@font-face {
|
|
21923
|
+
font-family: 'NotoSansSC-Regular';
|
|
21924
|
+
src: url('${bodyFont}') format('opentype');
|
|
21925
|
+
font-weight: normal;
|
|
21926
|
+
font-style: normal;
|
|
21927
|
+
}
|
|
21928
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
21929
|
+
html, body {
|
|
21930
|
+
width: ${width}px;
|
|
21931
|
+
height: ${height}px;
|
|
21932
|
+
overflow: hidden;
|
|
21933
|
+
background: ${palette.background};
|
|
21934
|
+
}
|
|
21935
|
+
.card {
|
|
21936
|
+
width: ${width}px;
|
|
21937
|
+
height: ${height}px;
|
|
21938
|
+
display: flex;
|
|
21939
|
+
flex-direction: column;
|
|
21940
|
+
background: ${palette.background};
|
|
21941
|
+
}
|
|
21942
|
+
.illustration-band {
|
|
21943
|
+
width: ${width}px;
|
|
21944
|
+
height: ${illustrationHeight}px;
|
|
21945
|
+
flex-shrink: 0;
|
|
21946
|
+
overflow: hidden;
|
|
21947
|
+
background: ${palette.background};
|
|
21948
|
+
}
|
|
21949
|
+
.illustration-band img {
|
|
21950
|
+
width: 100%;
|
|
21951
|
+
height: 100%;
|
|
21952
|
+
object-fit: cover;
|
|
21953
|
+
display: block;
|
|
21954
|
+
}
|
|
21955
|
+
.text-panel {
|
|
21956
|
+
width: ${width}px;
|
|
21957
|
+
height: ${textPanelHeight}px;
|
|
21958
|
+
flex-shrink: 0;
|
|
21959
|
+
padding: ${safeArea}px;
|
|
21960
|
+
display: flex;
|
|
21961
|
+
flex-direction: column;
|
|
21962
|
+
justify-content: flex-start;
|
|
21963
|
+
overflow: hidden;
|
|
21964
|
+
background: ${palette.background};
|
|
21965
|
+
}
|
|
21966
|
+
.headline {
|
|
21967
|
+
font-family: 'NotoSansSC-Bold', sans-serif;
|
|
21968
|
+
font-weight: bold;
|
|
21969
|
+
color: ${palette.text};
|
|
21970
|
+
line-height: 1.3;
|
|
21971
|
+
margin-bottom: ${Math.round(safeArea / 2)}px;
|
|
21972
|
+
word-break: break-all;
|
|
21973
|
+
white-space: normal;
|
|
21974
|
+
/* font-size is set inline with JS fit-step */
|
|
21975
|
+
font-size: ${headlineStartPx}px;
|
|
21976
|
+
}
|
|
21977
|
+
.points-container {
|
|
21978
|
+
display: flex;
|
|
21979
|
+
flex-direction: column;
|
|
21980
|
+
gap: ${Math.round(bodyMinPx * 0.5)}px;
|
|
21981
|
+
}
|
|
21982
|
+
.point {
|
|
21983
|
+
font-family: 'NotoSansSC-Regular', sans-serif;
|
|
21984
|
+
font-weight: normal;
|
|
21985
|
+
color: ${palette.text};
|
|
21986
|
+
line-height: 1.5;
|
|
21987
|
+
word-break: break-all;
|
|
21988
|
+
white-space: normal;
|
|
21989
|
+
}
|
|
21990
|
+
.safe-area-sentinel {
|
|
21991
|
+
position: absolute;
|
|
21992
|
+
top: ${illustrationHeight + safeArea}px;
|
|
21993
|
+
left: ${safeArea}px;
|
|
21994
|
+
width: ${safeWidth}px;
|
|
21995
|
+
height: ${textPanelHeight - safeArea * 2}px;
|
|
21996
|
+
pointer-events: none;
|
|
21997
|
+
}
|
|
21998
|
+
</style>
|
|
21999
|
+
</head>
|
|
22000
|
+
<body>
|
|
22001
|
+
<div class="card">
|
|
22002
|
+
<div class="illustration-band">
|
|
22003
|
+
${illustrationSrc ? `<img src="${illustrationSrc}" alt="">` : `<div style="width:100%;height:100%;background:${palette.background};"></div>`}
|
|
22004
|
+
</div>
|
|
22005
|
+
<div class="text-panel">
|
|
22006
|
+
<div class="headline" data-field="headline" id="headline">${escHtml(input.headline)}</div>
|
|
22007
|
+
<div class="points-container">
|
|
22008
|
+
${pointsHtml}
|
|
22009
|
+
</div>
|
|
22010
|
+
</div>
|
|
22011
|
+
</div>
|
|
22012
|
+
<div class="safe-area-sentinel" id="safe-area-sentinel"></div>
|
|
22013
|
+
<script>
|
|
22014
|
+
(function () {
|
|
22015
|
+
// Step headline font down from start until it fits, never below floor.
|
|
22016
|
+
var headline = document.getElementById('headline');
|
|
22017
|
+
var headlineMinPx = ${headlineMinPx};
|
|
22018
|
+
var headlineStartPx = ${headlineStartPx};
|
|
22019
|
+
var safeWidth = ${safeWidth};
|
|
22020
|
+
var panelHeight = ${textPanelHeight - safeArea * 2};
|
|
22021
|
+
var size = headlineStartPx;
|
|
22022
|
+
while (size > headlineMinPx) {
|
|
22023
|
+
headline.style.fontSize = size + 'px';
|
|
22024
|
+
if (headline.scrollWidth <= safeWidth && headline.scrollHeight <= panelHeight) break;
|
|
22025
|
+
size -= 4;
|
|
22026
|
+
}
|
|
22027
|
+
headline.style.fontSize = Math.max(size, headlineMinPx) + 'px';
|
|
22028
|
+
})();
|
|
22029
|
+
</script>
|
|
22030
|
+
</body>
|
|
22031
|
+
</html>`;
|
|
22032
|
+
}
|
|
22033
|
+
function escHtml(s) {
|
|
22034
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
22035
|
+
}
|
|
22036
|
+
|
|
22037
|
+
// ../../plugins/generations/image-gen/src/compose/chromium.ts
|
|
22038
|
+
var exports_chromium = {};
|
|
22039
|
+
__export(exports_chromium, {
|
|
22040
|
+
makeChromiumComposer: () => makeChromiumComposer,
|
|
22041
|
+
ChromiumComposer: () => ChromiumComposer
|
|
22042
|
+
});
|
|
22043
|
+
import { join as join3 } from "path";
|
|
22044
|
+
function fontRouteUrls(fontNames) {
|
|
22045
|
+
const map2 = new Map;
|
|
22046
|
+
for (const name of fontNames) {
|
|
22047
|
+
map2.set(name, `${FONT_ROUTE_BASE}${encodeURIComponent(name)}`);
|
|
22048
|
+
}
|
|
22049
|
+
return map2;
|
|
22050
|
+
}
|
|
22051
|
+
|
|
22052
|
+
class ChromiumComposer {
|
|
22053
|
+
async compose(input) {
|
|
22054
|
+
let chromium;
|
|
22055
|
+
try {
|
|
22056
|
+
const pw = await import("playwright");
|
|
22057
|
+
chromium = pw.chromium;
|
|
22058
|
+
} catch (err) {
|
|
22059
|
+
throw new Error(`compose: Playwright is not installed. Run: bunx playwright install chromium
|
|
22060
|
+
(${err instanceof Error ? err.message : String(err)})`);
|
|
22061
|
+
}
|
|
22062
|
+
const fontNames = [input.compose.fonts.headline, input.compose.fonts.body];
|
|
22063
|
+
const fontUrls = fontRouteUrls(fontNames);
|
|
22064
|
+
const html = cardHtml(input, fontUrls);
|
|
22065
|
+
let browser;
|
|
22066
|
+
try {
|
|
22067
|
+
try {
|
|
22068
|
+
browser = await chromium.launch({ headless: true });
|
|
22069
|
+
} catch (err) {
|
|
22070
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
22071
|
+
throw new Error(`compose: Chromium browser not found. Run: bunx playwright install chromium
|
|
22072
|
+
(${msg})`);
|
|
22073
|
+
}
|
|
22074
|
+
const page = await browser.newPage({
|
|
22075
|
+
viewport: { width: input.width, height: input.height },
|
|
22076
|
+
deviceScaleFactor: 1
|
|
22077
|
+
});
|
|
22078
|
+
await page.route(`${FONT_ROUTE_BASE}**`, async (route) => {
|
|
22079
|
+
const url2 = new URL(route.request().url());
|
|
22080
|
+
const fontName = decodeURIComponent(url2.pathname.slice(1));
|
|
22081
|
+
const fontPath = join3(FONTS_DIR, fontName);
|
|
22082
|
+
try {
|
|
22083
|
+
const bytes = await readBytes(fontPath);
|
|
22084
|
+
await route.fulfill({
|
|
22085
|
+
status: 200,
|
|
22086
|
+
contentType: "font/otf",
|
|
22087
|
+
body: Buffer.from(bytes)
|
|
22088
|
+
});
|
|
22089
|
+
} catch {
|
|
22090
|
+
await route.abort();
|
|
22091
|
+
}
|
|
22092
|
+
});
|
|
22093
|
+
await page.setContent(html, { waitUntil: "domcontentloaded" });
|
|
22094
|
+
await page.evaluate("document.fonts.ready");
|
|
22095
|
+
const { safeArea, illustrationHeight } = input.compose;
|
|
22096
|
+
const safeLeft = safeArea;
|
|
22097
|
+
const safeTop = illustrationHeight + safeArea;
|
|
22098
|
+
const safeRight = input.width - safeArea;
|
|
22099
|
+
const safeBottom = input.height - safeArea;
|
|
22100
|
+
const overflowExpr = `
|
|
22101
|
+
(function(safeLeft, safeTop, safeRight, safeBottom) {
|
|
22102
|
+
var results = [];
|
|
22103
|
+
var els = Array.from(document.querySelectorAll('[data-field]'));
|
|
22104
|
+
for (var i = 0; i < els.length; i++) {
|
|
22105
|
+
var el = els[i];
|
|
22106
|
+
var field = el.dataset.field || '';
|
|
22107
|
+
var rect = el.getBoundingClientRect();
|
|
22108
|
+
if (rect.left < safeLeft || rect.top < safeTop || rect.right > safeRight || rect.bottom > safeBottom) {
|
|
22109
|
+
results.push({ field: field, rect: { top: rect.top, left: rect.left, bottom: rect.bottom, right: rect.right } });
|
|
22110
|
+
}
|
|
22111
|
+
}
|
|
22112
|
+
return results;
|
|
22113
|
+
})(${safeLeft}, ${safeTop}, ${safeRight}, ${safeBottom})
|
|
22114
|
+
`;
|
|
22115
|
+
const overflows = await page.evaluate(overflowExpr);
|
|
22116
|
+
if (overflows.length > 0) {
|
|
22117
|
+
const fields = overflows.map((o) => o.field).join(", ");
|
|
22118
|
+
throw new Error(`compose: text overflows the safe area for field(s): ${fields} (safe area: ${safeArea}px inset)`);
|
|
22119
|
+
}
|
|
22120
|
+
const screenshot = await page.screenshot({ type: "png", animations: "disabled" });
|
|
22121
|
+
return new Uint8Array(screenshot);
|
|
22122
|
+
} finally {
|
|
22123
|
+
await browser?.close();
|
|
22124
|
+
}
|
|
22125
|
+
}
|
|
22126
|
+
}
|
|
22127
|
+
function makeChromiumComposer() {
|
|
22128
|
+
return new ChromiumComposer;
|
|
22129
|
+
}
|
|
22130
|
+
var FONTS_DIR, FONT_ROUTE_BASE = "http://kk-fonts.local/";
|
|
22131
|
+
var init_chromium = __esm(() => {
|
|
22132
|
+
init_bytes();
|
|
22133
|
+
FONTS_DIR = join3(import.meta.dir, "..", "..", "assets", "fonts");
|
|
22134
|
+
});
|
|
22135
|
+
|
|
21812
22136
|
// ../../plugins/generations/image-gen/src/index.ts
|
|
21813
|
-
import { dirname as dirname3 } from "path";
|
|
22137
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
21814
22138
|
import { parseArgs } from "util";
|
|
21815
22139
|
|
|
21816
22140
|
// ../../packages/kk-core/src/kinds.ts
|
|
@@ -21908,14 +22232,141 @@ init_config();
|
|
|
21908
22232
|
if (false) {}
|
|
21909
22233
|
// ../../plugins/generations/image-gen/src/index.ts
|
|
21910
22234
|
init_dist();
|
|
22235
|
+
init_bytes();
|
|
21911
22236
|
|
|
21912
|
-
// ../../plugins/generations/image-gen/src/
|
|
21913
|
-
|
|
21914
|
-
|
|
21915
|
-
|
|
22237
|
+
// ../../plugins/generations/image-gen/src/compose/cmap.ts
|
|
22238
|
+
function u16(view, offset) {
|
|
22239
|
+
return view.getUint16(offset, false);
|
|
22240
|
+
}
|
|
22241
|
+
function u32(view, offset) {
|
|
22242
|
+
return view.getUint32(offset, false);
|
|
22243
|
+
}
|
|
22244
|
+
function tag(view, offset) {
|
|
22245
|
+
return String.fromCharCode(view.getUint8(offset), view.getUint8(offset + 1), view.getUint8(offset + 2), view.getUint8(offset + 3));
|
|
22246
|
+
}
|
|
22247
|
+
function assertBytes(view, offset, needed, context2) {
|
|
22248
|
+
if (offset + needed > view.byteLength) {
|
|
22249
|
+
throw new Error(`compose: truncated font \u2014 expected ${needed} bytes at offset ${offset} (${context2})`);
|
|
22250
|
+
}
|
|
22251
|
+
}
|
|
22252
|
+
function findCmapTable(view) {
|
|
22253
|
+
assertBytes(view, 0, 12, "sfnt header");
|
|
22254
|
+
const magic = u32(view, 0);
|
|
22255
|
+
if (magic !== 65536 && magic !== 1330926671 && magic !== 1953658213) {
|
|
22256
|
+
throw new Error(`compose: not a valid sfnt font (magic 0x${magic.toString(16).padStart(8, "0")})`);
|
|
22257
|
+
}
|
|
22258
|
+
const numTables = u16(view, 4);
|
|
22259
|
+
assertBytes(view, 12, numTables * 16, "sfnt table directory");
|
|
22260
|
+
for (let i = 0;i < numTables; i++) {
|
|
22261
|
+
const base = 12 + i * 16;
|
|
22262
|
+
if (tag(view, base) === "cmap") {
|
|
22263
|
+
const offset = u32(view, base + 8);
|
|
22264
|
+
const length = u32(view, base + 12);
|
|
22265
|
+
return { offset, length };
|
|
22266
|
+
}
|
|
22267
|
+
}
|
|
22268
|
+
throw new Error("compose: truncated font \u2014 cmap table not found in sfnt directory");
|
|
22269
|
+
}
|
|
22270
|
+
function coverageFormat4(view, subtableOffset) {
|
|
22271
|
+
assertBytes(view, subtableOffset, 14, "cmap format 4 header");
|
|
22272
|
+
const length = u16(view, subtableOffset + 2);
|
|
22273
|
+
assertBytes(view, subtableOffset, length, "cmap format 4 body");
|
|
22274
|
+
const segCount = u16(view, subtableOffset + 6) / 2;
|
|
22275
|
+
const endCodesBase = subtableOffset + 14;
|
|
22276
|
+
assertBytes(view, endCodesBase, segCount * 2 + 2 + segCount * 2 + segCount * 2 + segCount * 2, "cmap format 4 segments");
|
|
22277
|
+
const startCodesBase = endCodesBase + segCount * 2 + 2;
|
|
22278
|
+
const idDeltaBase = startCodesBase + segCount * 2;
|
|
22279
|
+
const idRangeOffsetBase = idDeltaBase + segCount * 2;
|
|
22280
|
+
return (cp) => {
|
|
22281
|
+
if (cp > 65535)
|
|
22282
|
+
return false;
|
|
22283
|
+
let lo = 0;
|
|
22284
|
+
let hi = segCount - 1;
|
|
22285
|
+
while (lo <= hi) {
|
|
22286
|
+
const mid = lo + hi >>> 1;
|
|
22287
|
+
const endCode = u16(view, endCodesBase + mid * 2);
|
|
22288
|
+
if (endCode < cp) {
|
|
22289
|
+
lo = mid + 1;
|
|
22290
|
+
} else {
|
|
22291
|
+
hi = mid - 1;
|
|
22292
|
+
}
|
|
22293
|
+
}
|
|
22294
|
+
if (lo >= segCount)
|
|
22295
|
+
return false;
|
|
22296
|
+
const startCode = u16(view, startCodesBase + lo * 2);
|
|
22297
|
+
if (cp < startCode)
|
|
22298
|
+
return false;
|
|
22299
|
+
const idRangeOffset = u16(view, idRangeOffsetBase + lo * 2);
|
|
22300
|
+
if (idRangeOffset === 0) {
|
|
22301
|
+
const idDelta = u16(view, idDeltaBase + lo * 2);
|
|
22302
|
+
return (cp + idDelta & 65535) !== 0;
|
|
22303
|
+
}
|
|
22304
|
+
const glyphIdAddr = idRangeOffsetBase + lo * 2 + idRangeOffset + (cp - startCode) * 2;
|
|
22305
|
+
if (glyphIdAddr + 2 > view.byteLength)
|
|
22306
|
+
return false;
|
|
22307
|
+
const glyphId = u16(view, glyphIdAddr);
|
|
22308
|
+
return glyphId !== 0;
|
|
22309
|
+
};
|
|
22310
|
+
}
|
|
22311
|
+
function coverageFormat12(view, subtableOffset) {
|
|
22312
|
+
assertBytes(view, subtableOffset, 16, "cmap format 12 header");
|
|
22313
|
+
const numGroups = u32(view, subtableOffset + 12);
|
|
22314
|
+
assertBytes(view, subtableOffset + 16, numGroups * 12, "cmap format 12 groups");
|
|
22315
|
+
const groupsBase = subtableOffset + 16;
|
|
22316
|
+
return (cp) => {
|
|
22317
|
+
let lo = 0;
|
|
22318
|
+
let hi = numGroups - 1;
|
|
22319
|
+
while (lo <= hi) {
|
|
22320
|
+
const mid = lo + hi >>> 1;
|
|
22321
|
+
const base = groupsBase + mid * 12;
|
|
22322
|
+
const startChar = u32(view, base);
|
|
22323
|
+
const endChar = u32(view, base + 4);
|
|
22324
|
+
if (endChar < cp) {
|
|
22325
|
+
lo = mid + 1;
|
|
22326
|
+
} else if (startChar > cp) {
|
|
22327
|
+
hi = mid - 1;
|
|
22328
|
+
} else {
|
|
22329
|
+
const startGlyph = u32(view, base + 8);
|
|
22330
|
+
return startGlyph + (cp - startChar) !== 0;
|
|
22331
|
+
}
|
|
22332
|
+
}
|
|
22333
|
+
return false;
|
|
22334
|
+
};
|
|
21916
22335
|
}
|
|
21917
|
-
|
|
21918
|
-
|
|
22336
|
+
function glyphCoverage(fontBytes) {
|
|
22337
|
+
if (fontBytes.byteLength < 12) {
|
|
22338
|
+
throw new Error("compose: truncated font \u2014 too small for sfnt header");
|
|
22339
|
+
}
|
|
22340
|
+
const view = new DataView(fontBytes.buffer, fontBytes.byteOffset, fontBytes.byteLength);
|
|
22341
|
+
const { offset: cmapOffset } = findCmapTable(view);
|
|
22342
|
+
assertBytes(view, cmapOffset, 4, "cmap table header");
|
|
22343
|
+
const numSubtables = u16(view, cmapOffset + 2);
|
|
22344
|
+
assertBytes(view, cmapOffset + 4, numSubtables * 8, "cmap encoding records");
|
|
22345
|
+
let fmt4Offset;
|
|
22346
|
+
let fmt12Offset;
|
|
22347
|
+
for (let i = 0;i < numSubtables; i++) {
|
|
22348
|
+
const recBase = cmapOffset + 4 + i * 8;
|
|
22349
|
+
const platformId = u16(view, recBase);
|
|
22350
|
+
const encodingId = u16(view, recBase + 2);
|
|
22351
|
+
const subtableOffset = cmapOffset + u32(view, recBase + 4);
|
|
22352
|
+
if (subtableOffset + 2 > view.byteLength)
|
|
22353
|
+
continue;
|
|
22354
|
+
const fmt = u16(view, subtableOffset);
|
|
22355
|
+
if (fmt === 12 && (platformId === 3 || platformId === 0)) {
|
|
22356
|
+
fmt12Offset = subtableOffset;
|
|
22357
|
+
}
|
|
22358
|
+
if (fmt === 4 && (platformId === 3 || platformId === 0) && encodingId !== 10) {
|
|
22359
|
+
if (fmt4Offset === undefined)
|
|
22360
|
+
fmt4Offset = subtableOffset;
|
|
22361
|
+
}
|
|
22362
|
+
}
|
|
22363
|
+
if (fmt12Offset !== undefined) {
|
|
22364
|
+
return coverageFormat12(view, fmt12Offset);
|
|
22365
|
+
}
|
|
22366
|
+
if (fmt4Offset !== undefined) {
|
|
22367
|
+
return coverageFormat4(view, fmt4Offset);
|
|
22368
|
+
}
|
|
22369
|
+
throw new Error("compose: truncated font \u2014 no usable cmap subtable (format 4 or 12) found");
|
|
21919
22370
|
}
|
|
21920
22371
|
|
|
21921
22372
|
// ../../plugins/generations/image-gen/src/job.ts
|
|
@@ -21993,6 +22444,15 @@ var PresetSourceSchema = exports_external.object({
|
|
|
21993
22444
|
extractedAt: exports_external.string().optional(),
|
|
21994
22445
|
confidence: exports_external.enum(["high", "medium", "low"]).optional()
|
|
21995
22446
|
});
|
|
22447
|
+
var ComposeConfigSchema = exports_external.object({
|
|
22448
|
+
engine: exports_external.literal("text-card"),
|
|
22449
|
+
safeArea: exports_external.number(),
|
|
22450
|
+
headlineMinPx: exports_external.number(),
|
|
22451
|
+
bodyMinPx: exports_external.number(),
|
|
22452
|
+
illustrationHeight: exports_external.number(),
|
|
22453
|
+
palette: exports_external.object({ background: exports_external.string(), text: exports_external.string(), accent: exports_external.string() }),
|
|
22454
|
+
fonts: exports_external.object({ headline: exports_external.string(), body: exports_external.string() })
|
|
22455
|
+
});
|
|
21996
22456
|
var FormatPresetSchema = exports_external.object({
|
|
21997
22457
|
name: exports_external.string(),
|
|
21998
22458
|
description: exports_external.string(),
|
|
@@ -22004,7 +22464,10 @@ var FormatPresetSchema = exports_external.object({
|
|
|
22004
22464
|
keywords: exports_external.array(exports_external.string()),
|
|
22005
22465
|
outputFilename: exports_external.string(),
|
|
22006
22466
|
variables: exports_external.record(exports_external.string(), exports_external.object({ description: exports_external.string(), default: exports_external.string() })),
|
|
22007
|
-
template: exports_external.string()
|
|
22467
|
+
template: exports_external.string(),
|
|
22468
|
+
compose: ComposeConfigSchema.optional()
|
|
22469
|
+
}).refine((data) => data.compose === undefined || data.width !== undefined && data.height !== undefined, {
|
|
22470
|
+
message: "A format preset with a compose block requires both width and height to be declared"
|
|
22008
22471
|
});
|
|
22009
22472
|
var StylePresetSchema = exports_external.object({
|
|
22010
22473
|
name: exports_external.string(),
|
|
@@ -22157,6 +22620,7 @@ function loadPresets() {
|
|
|
22157
22620
|
import { Buffer as Buffer3 } from "buffer";
|
|
22158
22621
|
|
|
22159
22622
|
// ../../plugins/generations/image-gen/src/providers/refs.ts
|
|
22623
|
+
init_bytes();
|
|
22160
22624
|
import { extname as extname2 } from "path";
|
|
22161
22625
|
function mimeForPath(path2) {
|
|
22162
22626
|
const ext = extname2(path2).toLowerCase();
|
|
@@ -22178,6 +22642,7 @@ async function refToDataUri(path2) {
|
|
|
22178
22642
|
}
|
|
22179
22643
|
|
|
22180
22644
|
// ../../plugins/generations/image-gen/src/providers/seedream.ts
|
|
22645
|
+
init_bytes();
|
|
22181
22646
|
import { Buffer as Buffer2 } from "buffer";
|
|
22182
22647
|
|
|
22183
22648
|
// ../../plugins/generations/image-gen/src/providers/timeouts.ts
|
|
@@ -22376,9 +22841,11 @@ var createAgnes = (options) => {
|
|
|
22376
22841
|
};
|
|
22377
22842
|
|
|
22378
22843
|
// ../../plugins/generations/image-gen/src/providers/azure.ts
|
|
22844
|
+
init_bytes();
|
|
22379
22845
|
import { extname as extname3 } from "path";
|
|
22380
22846
|
|
|
22381
22847
|
// ../../plugins/generations/image-gen/src/providers/openai.ts
|
|
22848
|
+
init_bytes();
|
|
22382
22849
|
import { Buffer as Buffer4 } from "buffer";
|
|
22383
22850
|
import { basename as basename2 } from "path";
|
|
22384
22851
|
var DEFAULT_MODEL3 = "gpt-image-2";
|
|
@@ -22788,6 +23255,7 @@ var createAzure = (options) => {
|
|
|
22788
23255
|
};
|
|
22789
23256
|
|
|
22790
23257
|
// ../../plugins/generations/image-gen/src/providers/codex-cli.ts
|
|
23258
|
+
init_bytes();
|
|
22791
23259
|
import { spawn } from "child_process";
|
|
22792
23260
|
import { randomBytes } from "crypto";
|
|
22793
23261
|
import { access as access2, mkdir, rm, writeFile as writeFile2 } from "fs/promises";
|
|
@@ -23312,6 +23780,7 @@ import { execFileSync } from "child_process";
|
|
|
23312
23780
|
import { randomUUID } from "crypto";
|
|
23313
23781
|
import { tmpdir as tmpdir2 } from "os";
|
|
23314
23782
|
import { join as join2 } from "path";
|
|
23783
|
+
init_bytes();
|
|
23315
23784
|
var GOOGLE_MULTIMODAL_MODELS = [
|
|
23316
23785
|
"gemini-3-pro-image",
|
|
23317
23786
|
"gemini-3.1-flash-image",
|
|
@@ -23435,7 +23904,12 @@ header = ${curlQuote(`x-goog-api-key: ${apiKey}`)}
|
|
|
23435
23904
|
"-d",
|
|
23436
23905
|
"@-",
|
|
23437
23906
|
url2
|
|
23438
|
-
], {
|
|
23907
|
+
], {
|
|
23908
|
+
input: JSON.stringify(body),
|
|
23909
|
+
encoding: "utf8",
|
|
23910
|
+
timeout: 310000,
|
|
23911
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
23912
|
+
});
|
|
23439
23913
|
} catch (error51) {
|
|
23440
23914
|
const e = error51;
|
|
23441
23915
|
const details = (typeof e.stderr === "string" ? e.stderr.trim() : "") || e.message || "curl request failed";
|
|
@@ -23761,6 +24235,7 @@ var createJimeng = (options) => {
|
|
|
23761
24235
|
};
|
|
23762
24236
|
|
|
23763
24237
|
// ../../plugins/generations/image-gen/src/providers/minimax.ts
|
|
24238
|
+
init_bytes();
|
|
23764
24239
|
import { Buffer as Buffer7 } from "buffer";
|
|
23765
24240
|
var DEFAULT_MODEL9 = "image-01";
|
|
23766
24241
|
var MAX_REFERENCE_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
@@ -24891,6 +25366,65 @@ async function processGeneratorIO(inputPath, outputPath, options = {}) {
|
|
|
24891
25366
|
}
|
|
24892
25367
|
if (spec.job === "illustration")
|
|
24893
25368
|
assertAnchorPlan(doc2?.body ?? "", positions, prompts.length);
|
|
25369
|
+
let _chromiumComposer;
|
|
25370
|
+
const resolveComposer = async () => {
|
|
25371
|
+
if (options.composer !== undefined)
|
|
25372
|
+
return options.composer;
|
|
25373
|
+
if (_chromiumComposer !== undefined)
|
|
25374
|
+
return _chromiumComposer;
|
|
25375
|
+
const { makeChromiumComposer: makeChromiumComposer2 } = await Promise.resolve().then(() => (init_chromium(), exports_chromium));
|
|
25376
|
+
_chromiumComposer = makeChromiumComposer2();
|
|
25377
|
+
return _chromiumComposer;
|
|
25378
|
+
};
|
|
25379
|
+
if (format.compose !== undefined) {
|
|
25380
|
+
const cfg = format.compose;
|
|
25381
|
+
if (spec.job !== "free") {
|
|
25382
|
+
throw new Error(`compose: format '${format.name}' has a compose block but the job is '${spec.job}' \u2014 compose formats require job: free`);
|
|
25383
|
+
}
|
|
25384
|
+
const vars = spec.vars ?? {};
|
|
25385
|
+
const headline = vars.headline ?? format.variables.headline?.default ?? "";
|
|
25386
|
+
if (!headline.trim()) {
|
|
25387
|
+
throw new Error(`compose: format '${format.name}' requires a non-empty vars.headline`);
|
|
25388
|
+
}
|
|
25389
|
+
const pointsRaw = vars.points ?? format.variables.points?.default ?? "";
|
|
25390
|
+
const points = pointsRaw.split(`
|
|
25391
|
+
`).filter((l) => l.trim() !== "");
|
|
25392
|
+
const promptVars = { subject: doc2?.body ?? "", ...spec.vars ?? {} };
|
|
25393
|
+
const textFreeSources = [
|
|
25394
|
+
{ source: "the format template", text: format.template },
|
|
25395
|
+
{
|
|
25396
|
+
source: `the style preset '${style.name}'`,
|
|
25397
|
+
text: [style.modifiers, style.negative].filter((part) => part !== "").join(", ")
|
|
25398
|
+
},
|
|
25399
|
+
{ source: "spec.extra", text: spec.extra ?? "" },
|
|
25400
|
+
...Object.entries(promptVars).filter(([name]) => name !== "headline" && name !== "points" && name !== "index").map(([name, value]) => ({ source: `vars.${name}`, text: String(value) }))
|
|
25401
|
+
];
|
|
25402
|
+
assertTextFreePrompt(prompts[0] ?? "", { headline, points }, textFreeSources);
|
|
25403
|
+
const fontsDir = join4(import.meta.dir, "..", "assets", "fonts");
|
|
25404
|
+
const fontNames = [cfg.fonts.headline, cfg.fonts.body];
|
|
25405
|
+
const fontBytesMap = new Map;
|
|
25406
|
+
for (const name of fontNames) {
|
|
25407
|
+
fontBytesMap.set(name, new Uint8Array(await readBytes(join4(fontsDir, name))));
|
|
25408
|
+
}
|
|
25409
|
+
const coverageMap = new Map;
|
|
25410
|
+
for (const [name, bytes] of fontBytesMap) {
|
|
25411
|
+
coverageMap.set(name, glyphCoverage(bytes));
|
|
25412
|
+
}
|
|
25413
|
+
if (format.width == null || format.height == null) {
|
|
25414
|
+
throw new Error(`image-gen: format '${format.name}' declares no canvas size; compose requires width and height`);
|
|
25415
|
+
}
|
|
25416
|
+
const cardInput = {
|
|
25417
|
+
headline,
|
|
25418
|
+
points,
|
|
25419
|
+
role: vars.role ?? format.variables.role?.default ?? "content",
|
|
25420
|
+
compose: cfg,
|
|
25421
|
+
width: format.width,
|
|
25422
|
+
height: format.height
|
|
25423
|
+
};
|
|
25424
|
+
assertCoverage(cardInput, coverageMap);
|
|
25425
|
+
const composer2 = await resolveComposer();
|
|
25426
|
+
await composer2.compose(cardInput);
|
|
25427
|
+
}
|
|
24894
25428
|
const resolveProvider = options.providerResolver ?? defaultProviderResolver;
|
|
24895
25429
|
const primary = resolveProvider(spec);
|
|
24896
25430
|
const built = new Map;
|
|
@@ -24998,17 +25532,64 @@ async function processGeneratorIO(inputPath, outputPath, options = {}) {
|
|
|
24998
25532
|
throw new Error(`Illustration anchor must occur exactly once in the source body, found ${parts.length - 1}: '${anchor.slice(0, 50)}'`);
|
|
24999
25533
|
}
|
|
25000
25534
|
}
|
|
25001
|
-
|
|
25002
|
-
|
|
25003
|
-
|
|
25535
|
+
let artifactPathValue;
|
|
25536
|
+
let artifactBytes;
|
|
25537
|
+
let artifactWidth;
|
|
25538
|
+
let artifactHeight;
|
|
25539
|
+
let illustrationMeta;
|
|
25540
|
+
if (format.compose !== undefined) {
|
|
25541
|
+
const illustrationArtifactPath = artifactPath(outputPath, stem, `${format.name}-illustration`, index + 1, result.extension);
|
|
25542
|
+
await writeBytes(illustrationArtifactPath, result.bytes);
|
|
25543
|
+
written.push(illustrationArtifactPath);
|
|
25544
|
+
const vars = spec.vars ?? {};
|
|
25545
|
+
const headline = vars.headline ?? format.variables.headline?.default ?? "";
|
|
25546
|
+
const pointsRaw = vars.points ?? format.variables.points?.default ?? "";
|
|
25547
|
+
const points = pointsRaw.split(`
|
|
25548
|
+
`).filter((l) => l.trim() !== "");
|
|
25549
|
+
if (format.width == null || format.height == null) {
|
|
25550
|
+
throw new Error(`image-gen: format '${format.name}' declares no canvas size; compose requires width and height`);
|
|
25551
|
+
}
|
|
25552
|
+
const cardInput = {
|
|
25553
|
+
headline,
|
|
25554
|
+
points,
|
|
25555
|
+
role: vars.role ?? format.variables.role?.default ?? "content",
|
|
25556
|
+
compose: format.compose,
|
|
25557
|
+
width: format.width,
|
|
25558
|
+
height: format.height,
|
|
25559
|
+
illustration: { bytes: result.bytes, mime: `image/${result.extension}` }
|
|
25560
|
+
};
|
|
25561
|
+
const composer2 = await resolveComposer();
|
|
25562
|
+
const cardBytes = await composer2.compose(cardInput);
|
|
25563
|
+
const cardPath = artifactPath(outputPath, stem, format.name, index + 1, "png");
|
|
25564
|
+
await writeBytes(cardPath, cardBytes);
|
|
25565
|
+
written.push(cardPath);
|
|
25566
|
+
const dims = imageDimensions(cardBytes);
|
|
25567
|
+
if (dims.width !== format.width || dims.height !== format.height) {
|
|
25568
|
+
throw new Error(`compose: card dimensions ${dims.width}x${dims.height} do not match format canvas ${format.width}x${format.height}`);
|
|
25569
|
+
}
|
|
25570
|
+
artifactPathValue = cardPath;
|
|
25571
|
+
artifactBytes = cardBytes.byteLength;
|
|
25572
|
+
artifactWidth = dims.width;
|
|
25573
|
+
artifactHeight = dims.height;
|
|
25574
|
+
illustrationMeta = { path: illustrationArtifactPath, bytes: result.bytes.byteLength };
|
|
25575
|
+
} else {
|
|
25576
|
+
const path2 = artifactPath(outputPath, stem, format.name, index + 1, result.extension);
|
|
25577
|
+
await writeBytes(path2, result.bytes);
|
|
25578
|
+
written.push(path2);
|
|
25579
|
+
artifactPathValue = path2;
|
|
25580
|
+
artifactBytes = result.bytes.byteLength;
|
|
25581
|
+
}
|
|
25004
25582
|
const artifact = {
|
|
25005
|
-
path:
|
|
25583
|
+
path: artifactPathValue,
|
|
25006
25584
|
format: format.name,
|
|
25007
25585
|
style: style.name,
|
|
25008
25586
|
provider: result.provider,
|
|
25009
25587
|
model: result.model,
|
|
25010
25588
|
prompt,
|
|
25011
|
-
bytes:
|
|
25589
|
+
bytes: artifactBytes,
|
|
25590
|
+
...artifactWidth !== undefined ? { width: artifactWidth } : {},
|
|
25591
|
+
...artifactHeight !== undefined ? { height: artifactHeight } : {},
|
|
25592
|
+
...illustrationMeta !== undefined ? { illustration: illustrationMeta } : {}
|
|
25012
25593
|
};
|
|
25013
25594
|
if (spec.seed !== undefined) {
|
|
25014
25595
|
artifact.seed = spec.seed;
|
|
@@ -25019,10 +25600,10 @@ async function processGeneratorIO(inputPath, outputPath, options = {}) {
|
|
|
25019
25600
|
const altText = `Illustration ${index + 1}`;
|
|
25020
25601
|
const markdown = `${anchor}
|
|
25021
25602
|
|
|
25022
|
-
`;
|
|
25023
25604
|
captions.push({
|
|
25024
25605
|
index: index + 1,
|
|
25025
|
-
path:
|
|
25606
|
+
path: artifactPathValue,
|
|
25026
25607
|
caption: anchor,
|
|
25027
25608
|
altText,
|
|
25028
25609
|
anchor
|