@ohos-ports/slides-grab 1.5.0-beta.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/LICENSE +21 -0
- package/README-ko.md +292 -0
- package/README.md +301 -0
- package/bin/ohos-postinstall.cjs +118 -0
- package/bin/ppt-agent.js +620 -0
- package/convert.cjs +20 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/glib-2.0/include/glibconfig.h +220 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/index.js +1 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libexpat.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libffi.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libjpeg.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libpcre2-8.so.0 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips-cpp.so.42.20.3 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips.so.42.20.3 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/package.json +13 -0
- package/native/@img/sharp-openharmony-arm64/lib/sharp-openharmony-arm64.node +0 -0
- package/native/@img/sharp-openharmony-arm64/package.json +16 -0
- package/package.json +95 -0
- package/runtimes/claude-code/agents/design-critic-agent.md +23 -0
- package/runtimes/codex/agents/slides-grab-design-critic.md +22 -0
- package/scripts/build-viewer.js +444 -0
- package/scripts/design-gate.js +258 -0
- package/scripts/download-video.js +213 -0
- package/scripts/editor-server.js +1167 -0
- package/scripts/figma-export.js +169 -0
- package/scripts/generate-image.js +116 -0
- package/scripts/generate-images.js +164 -0
- package/scripts/html2pdf.js +822 -0
- package/scripts/html2png.js +246 -0
- package/scripts/html2pptx.js +162 -0
- package/scripts/import-template.js +86 -0
- package/scripts/install-runtime.js +216 -0
- package/scripts/render-tldraw.js +44 -0
- package/scripts/validate-slides.js +221 -0
- package/skills/slides-grab/SKILL.md +65 -0
- package/skills/slides-grab/references/presentation-workflow-reference.md +61 -0
- package/skills/slides-grab-card-news/SKILL.md +35 -0
- package/skills/slides-grab-design/SKILL.md +88 -0
- package/skills/slides-grab-design/references/beautiful-slide-defaults.md +88 -0
- package/skills/slides-grab-design/references/design-gate.md +349 -0
- package/skills/slides-grab-design/references/design-rules.md +76 -0
- package/skills/slides-grab-design/references/design-system-full.md +568 -0
- package/skills/slides-grab-design/references/detailed-design-rules.md +70 -0
- package/skills/slides-grab-export/SKILL.md +55 -0
- package/skills/slides-grab-export/references/export-rules.md +27 -0
- package/skills/slides-grab-export/references/html2pptx.md +627 -0
- package/skills/slides-grab-export/references/ooxml.md +427 -0
- package/skills/slides-grab-export/references/pptx-skill-reference.md +189 -0
- package/skills/slides-grab-html/SKILL.md +67 -0
- package/skills/slides-grab-image/SKILL.md +82 -0
- package/skills/slides-grab-plan/SKILL.md +68 -0
- package/skills/slides-grab-plan/references/design-md-to-slides-conversion.md +135 -0
- package/skills/slides-grab-plan/references/outline-format.md +47 -0
- package/skills/slides-grab-plan/references/plan-workflow-reference.md +140 -0
- package/src/codex-imagen.js +182 -0
- package/src/design-diversity-data.js +6932 -0
- package/src/design-gate-report.js +244 -0
- package/src/design-gate-state.js +329 -0
- package/src/design-import.js +164 -0
- package/src/design-md-parser.js +415 -0
- package/src/design-styles-data.js +1928 -0
- package/src/design-styles.js +209 -0
- package/src/editor/codex-edit.js +584 -0
- package/src/editor/edit-subprocess.js +170 -0
- package/src/editor/editor-codex-prompt.md +50 -0
- package/src/editor/editor.html +1785 -0
- package/src/editor/js/editor-bbox.js +332 -0
- package/src/editor/js/editor-chat.js +56 -0
- package/src/editor/js/editor-direct-edit.js +238 -0
- package/src/editor/js/editor-dom.js +59 -0
- package/src/editor/js/editor-init.js +405 -0
- package/src/editor/js/editor-navigation.js +54 -0
- package/src/editor/js/editor-select.js +558 -0
- package/src/editor/js/editor-send.js +175 -0
- package/src/editor/js/editor-sse.js +163 -0
- package/src/editor/js/editor-state.js +41 -0
- package/src/editor/js/editor-type.js +71 -0
- package/src/editor/js/editor-utils.js +167 -0
- package/src/editor/js/model-registry.js +37 -0
- package/src/editor/screenshot.js +82 -0
- package/src/export-resolution.cjs +68 -0
- package/src/figma.js +71 -0
- package/src/html2pptx-scale.cjs +120 -0
- package/src/html2pptx.cjs +1262 -0
- package/src/image-contract.js +329 -0
- package/src/image-native.js +468 -0
- package/src/nano-banana.js +841 -0
- package/src/ohos-browser-bridge.cjs +272 -0
- package/src/pptx-raster-export.cjs +299 -0
- package/src/resolve.js +110 -0
- package/src/slide-mode.cjs +72 -0
- package/src/template-fidelity.js +267 -0
- package/src/template-import.js +505 -0
- package/src/template-layout.js +261 -0
- package/src/template-pack.js +255 -0
- package/src/tldraw/render.js +473 -0
- package/src/validation/cli.js +120 -0
- package/src/validation/core.js +1023 -0
- package/templates/chart.html +121 -0
- package/templates/closing.html +54 -0
- package/templates/content.html +50 -0
- package/templates/contents.html +60 -0
- package/templates/cover.html +64 -0
- package/templates/custom/.gitkeep +0 -0
- package/templates/custom/README.md +7 -0
- package/templates/design-styles/README.md +20 -0
- package/templates/design-styles/preview.html +4438 -0
- package/templates/diagram-tldraw.html +56 -0
- package/templates/diagram.html +98 -0
- package/templates/quote.html +31 -0
- package/templates/section-divider.html +43 -0
- package/templates/split-layout.html +43 -0
- package/templates/statistics.html +55 -0
- package/templates/team.html +49 -0
- package/templates/timeline.html +59 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { basename, join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { loadTemplatePackFromSlidesDir, parseTemplatePack } from './template-pack.js';
|
|
6
|
+
import { selectTemplateLayout } from './template-layout.js';
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_OPENAI_IMAGE_MODEL,
|
|
9
|
+
DEFAULT_CODEX_MODEL,
|
|
10
|
+
DEFAULT_NANO_BANANA_ASPECT_RATIO,
|
|
11
|
+
DEFAULT_NANO_BANANA_MODEL,
|
|
12
|
+
IMAGE_PROVIDER_OPENAI,
|
|
13
|
+
IMAGE_PROVIDER_CODEX,
|
|
14
|
+
IMAGE_PROVIDER_NANO_BANANA,
|
|
15
|
+
generateOpenaiImage,
|
|
16
|
+
generateNanoBananaImage,
|
|
17
|
+
resolveOpenaiApiKey,
|
|
18
|
+
resolveOpenaiBaseUrl,
|
|
19
|
+
resolveNanoBananaApiKey,
|
|
20
|
+
} from './nano-banana.js';
|
|
21
|
+
import { generateCodexImage } from './codex-imagen.js';
|
|
22
|
+
|
|
23
|
+
export const IMAGE_NATIVE_METADATA_DIR = '.slides-grab/image-native';
|
|
24
|
+
|
|
25
|
+
function normalizeProvider(value) {
|
|
26
|
+
const provider = String(value || IMAGE_PROVIDER_CODEX).trim().toLowerCase();
|
|
27
|
+
if (!provider) return IMAGE_PROVIDER_CODEX;
|
|
28
|
+
if (provider === 'openai') return IMAGE_PROVIDER_OPENAI;
|
|
29
|
+
if (provider === 'gemini') return IMAGE_PROVIDER_NANO_BANANA;
|
|
30
|
+
if (provider === IMAGE_PROVIDER_OPENAI || provider === IMAGE_PROVIDER_CODEX || provider === IMAGE_PROVIDER_NANO_BANANA) return provider;
|
|
31
|
+
throw new Error(`Unknown image-native provider: ${value}. Expected codex, openai, or nano-banana.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stripSlidePrefix(title) {
|
|
35
|
+
return title.replace(/^slide\s+\d+\s*[:.)-]?\s*/i, '').trim() || title.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseSlideBlock(heading, lines, index) {
|
|
39
|
+
const bullets = [];
|
|
40
|
+
const bodyLines = [];
|
|
41
|
+
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
const bulletMatch = line.match(/^\s*[-*+]\s+(.+?)\s*$/);
|
|
44
|
+
if (bulletMatch) {
|
|
45
|
+
bullets.push(bulletMatch[1]);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (line.trim()) bodyLines.push(line.trim());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const sourceMarkdown = [`## ${heading}`, ...lines].join('\n').trim();
|
|
52
|
+
const body = bodyLines.join('\n').trim();
|
|
53
|
+
return {
|
|
54
|
+
index,
|
|
55
|
+
title: stripSlidePrefix(heading),
|
|
56
|
+
body,
|
|
57
|
+
bullets,
|
|
58
|
+
notes: body,
|
|
59
|
+
sourceMarkdown,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function parseImageNativeOutline(markdown) {
|
|
64
|
+
const text = String(markdown || '').replace(/\r\n/g, '\n');
|
|
65
|
+
const lines = text.split('\n');
|
|
66
|
+
const slides = [];
|
|
67
|
+
let currentHeading = null;
|
|
68
|
+
let currentLines = [];
|
|
69
|
+
|
|
70
|
+
function flush() {
|
|
71
|
+
if (!currentHeading) return;
|
|
72
|
+
slides.push(parseSlideBlock(currentHeading, currentLines, slides.length + 1));
|
|
73
|
+
currentHeading = null;
|
|
74
|
+
currentLines = [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
const headingMatch = line.match(/^##\s+(.+?)\s*$/);
|
|
79
|
+
if (headingMatch) {
|
|
80
|
+
flush();
|
|
81
|
+
currentHeading = headingMatch[1];
|
|
82
|
+
currentLines = [];
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (currentHeading) currentLines.push(line);
|
|
86
|
+
}
|
|
87
|
+
flush();
|
|
88
|
+
|
|
89
|
+
if (slides.length > 0) return slides;
|
|
90
|
+
|
|
91
|
+
const fallbackTitle = lines.find((line) => /^#\s+/.test(line))?.replace(/^#\s+/, '').trim() || 'Generated slide';
|
|
92
|
+
return [parseSlideBlock(fallbackTitle, lines.filter((line) => !/^#\s+/.test(line)), 1)];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function findLayoutById(pack, layoutId) {
|
|
96
|
+
return pack?.layouts?.find((layout) => (layout.layoutId ?? layout.layout_id ?? layout.id) === layoutId) ?? null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function layoutId(layout) {
|
|
100
|
+
return layout?.layoutId ?? layout?.layout_id ?? layout?.id ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function layoutKind(layout) {
|
|
104
|
+
return layout?.layoutKind ?? layout?.layout_kind ?? layout?.kind ?? null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function describeFields(layout) {
|
|
108
|
+
const fields = Array.isArray(layout?.fields) ? layout.fields : [];
|
|
109
|
+
return fields.map((field) => {
|
|
110
|
+
const parts = [field.role].filter(Boolean);
|
|
111
|
+
if (field.maxChars) parts.push(`max ${field.maxChars} chars`);
|
|
112
|
+
if (field.listLimits?.maxItems) parts.push(`max ${field.listLimits.maxItems} items`);
|
|
113
|
+
if (field.listLimits?.maxCharsPerItem) parts.push(`max ${field.listLimits.maxCharsPerItem} chars/item`);
|
|
114
|
+
return parts.join(', ');
|
|
115
|
+
}).filter(Boolean);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function referencePreviewDescription(layout) {
|
|
119
|
+
if (!layout?.preview) return null;
|
|
120
|
+
return `${layout.preview}: reference preview for layout ${layoutId(layout)}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function buildImageNativePrompt({
|
|
124
|
+
slide,
|
|
125
|
+
templatePack = null,
|
|
126
|
+
layout = null,
|
|
127
|
+
referencePreviewDescriptions = [],
|
|
128
|
+
} = {}) {
|
|
129
|
+
const safeSlide = slide ?? {};
|
|
130
|
+
const bullets = Array.isArray(safeSlide.bullets) ? safeSlide.bullets : [];
|
|
131
|
+
const fieldDescriptions = describeFields(layout);
|
|
132
|
+
const previews = [
|
|
133
|
+
...referencePreviewDescriptions,
|
|
134
|
+
referencePreviewDescription(layout),
|
|
135
|
+
].filter(Boolean);
|
|
136
|
+
|
|
137
|
+
return [
|
|
138
|
+
'Generate one full-slide 16:9 presentation image at 720pt × 405pt.',
|
|
139
|
+
'All text inside the image must be sharp, legible, correctly spelled, and faithful to the source outline. Do not invent facts.',
|
|
140
|
+
'',
|
|
141
|
+
`Slide ${safeSlide.index ?? ''}: ${safeSlide.title ?? 'Untitled slide'}`.trim(),
|
|
142
|
+
safeSlide.body ? `Body/notes: ${safeSlide.body}` : '',
|
|
143
|
+
bullets.length ? `Bullets:\n${bullets.map((item) => `- ${item}`).join('\n')}` : '',
|
|
144
|
+
'',
|
|
145
|
+
templatePack ? `Template pack: ${templatePack.name}` : 'Template pack: none',
|
|
146
|
+
layout ? `Selected template layout: ${layoutId(layout)} (${layoutKind(layout) || 'unknown kind'})` : 'Selected template layout: none',
|
|
147
|
+
layout?.description ? `Layout description: ${layout.description}` : '',
|
|
148
|
+
fieldDescriptions.length ? `Layout schema limits:\n${fieldDescriptions.map((item) => `- ${item}`).join('\n')}` : '',
|
|
149
|
+
previews.length ? `Reference preview descriptions:\n${previews.map((item) => `- ${item}`).join('\n')}` : 'Reference preview descriptions: none available; use distilled template description only.',
|
|
150
|
+
templatePack?.promptSafeStyleSummary ? `Template style summary: ${templatePack.promptSafeStyleSummary}` : '',
|
|
151
|
+
'',
|
|
152
|
+
'Composition requirements: fill the whole slide, no browser chrome, no crop marks, no extra margins outside the slide, preserve template colors/fonts/layout cues, and keep the slide immediately readable in a deck viewer.',
|
|
153
|
+
].filter((line) => line !== '').join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildWrapperHtml({ title, assetRef, metadataRef }) {
|
|
157
|
+
const safeTitle = String(title || 'Image-native slide')
|
|
158
|
+
.replace(/&/g, '&')
|
|
159
|
+
.replace(/</g, '<')
|
|
160
|
+
.replace(/>/g, '>')
|
|
161
|
+
.replace(/"/g, '"');
|
|
162
|
+
return `<!doctype html>
|
|
163
|
+
<html lang="en">
|
|
164
|
+
<head>
|
|
165
|
+
<meta charset="utf-8">
|
|
166
|
+
<meta name="slides-grab-image-native" content="true">
|
|
167
|
+
<meta name="slides-grab-image-native-metadata" content="${metadataRef}">
|
|
168
|
+
<title>${safeTitle}</title>
|
|
169
|
+
<style>
|
|
170
|
+
* { box-sizing: border-box; }
|
|
171
|
+
html, body {
|
|
172
|
+
margin: 0;
|
|
173
|
+
padding: 0;
|
|
174
|
+
width: 720pt;
|
|
175
|
+
height: 405pt;
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
background: #000000;
|
|
178
|
+
}
|
|
179
|
+
body { position: relative; }
|
|
180
|
+
img.slide-image {
|
|
181
|
+
position: absolute;
|
|
182
|
+
inset: 0;
|
|
183
|
+
width: 720pt;
|
|
184
|
+
height: 405pt;
|
|
185
|
+
display: block;
|
|
186
|
+
object-fit: cover;
|
|
187
|
+
}
|
|
188
|
+
</style>
|
|
189
|
+
</head>
|
|
190
|
+
<body>
|
|
191
|
+
<img class="slide-image" src="${assetRef}" width="960" height="540" alt="${safeTitle}">
|
|
192
|
+
</body>
|
|
193
|
+
</html>
|
|
194
|
+
`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function writeImageNativeSlideWrapper({
|
|
198
|
+
slidesDir,
|
|
199
|
+
slideName,
|
|
200
|
+
assetRef,
|
|
201
|
+
prompt,
|
|
202
|
+
provider,
|
|
203
|
+
model,
|
|
204
|
+
title = '',
|
|
205
|
+
} = {}) {
|
|
206
|
+
const normalizedSlideName = String(slideName || '').trim().replace(/\.html$/i, '');
|
|
207
|
+
if (!/^slide-\d{2}$/.test(normalizedSlideName)) {
|
|
208
|
+
throw new Error('--image-native requires --name in slide-XX format.');
|
|
209
|
+
}
|
|
210
|
+
if (typeof assetRef !== 'string' || !assetRef.startsWith('./assets/')) {
|
|
211
|
+
throw new Error('Image-native assets must use a ./assets/ reference.');
|
|
212
|
+
}
|
|
213
|
+
if (typeof prompt !== 'string' || prompt.trim() === '') {
|
|
214
|
+
throw new Error('Image-native slide metadata requires the generation prompt.');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const absoluteSlidesDir = resolve(slidesDir || 'slides');
|
|
218
|
+
const metadataDir = join(absoluteSlidesDir, IMAGE_NATIVE_METADATA_DIR);
|
|
219
|
+
const slideFile = `${normalizedSlideName}.html`;
|
|
220
|
+
const metadataFile = `${normalizedSlideName}.json`;
|
|
221
|
+
const metadataRef = `${IMAGE_NATIVE_METADATA_DIR}/${metadataFile}`;
|
|
222
|
+
const normalizedProvider = normalizeProvider(provider);
|
|
223
|
+
await mkdir(metadataDir, { recursive: true });
|
|
224
|
+
await writeFile(
|
|
225
|
+
join(absoluteSlidesDir, slideFile),
|
|
226
|
+
buildWrapperHtml({ title: title || normalizedSlideName, assetRef, metadataRef }),
|
|
227
|
+
'utf8',
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const metadata = {
|
|
231
|
+
schemaVersion: 1,
|
|
232
|
+
mode: 'image-native',
|
|
233
|
+
slideFile,
|
|
234
|
+
assetRef,
|
|
235
|
+
prompt: prompt.trim(),
|
|
236
|
+
provider: normalizedProvider,
|
|
237
|
+
model: String(model || '').trim(),
|
|
238
|
+
templateLayoutId: null,
|
|
239
|
+
templateLayoutKind: null,
|
|
240
|
+
regenerationHistory: [],
|
|
241
|
+
};
|
|
242
|
+
await writeFile(join(metadataDir, metadataFile), `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
|
|
243
|
+
return { slideFile, metadataFile: metadataRef, metadata };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function defaultGenerateImage({ prompt, provider, model, env = process.env, baseUrl = '', fetchImpl = globalThis.fetch, referenceImages = [] }) {
|
|
247
|
+
if (provider === IMAGE_PROVIDER_CODEX) {
|
|
248
|
+
return generateCodexImage({ prompt, model: model || DEFAULT_CODEX_MODEL, aspectRatio: DEFAULT_NANO_BANANA_ASPECT_RATIO, referenceImages });
|
|
249
|
+
}
|
|
250
|
+
if (provider === IMAGE_PROVIDER_OPENAI) {
|
|
251
|
+
const { apiKey } = resolveOpenaiApiKey(env);
|
|
252
|
+
if (!apiKey) throw new Error('OpenAI image-native generation requires OPENAI_API_KEY or OPENAI_IMAGE_API_KEY.');
|
|
253
|
+
return generateOpenaiImage({ prompt, apiKey, model: model || DEFAULT_OPENAI_IMAGE_MODEL, aspectRatio: DEFAULT_NANO_BANANA_ASPECT_RATIO, baseUrl: resolveOpenaiBaseUrl({ baseUrl, env }).baseUrl, fetchImpl });
|
|
254
|
+
}
|
|
255
|
+
if (provider === IMAGE_PROVIDER_NANO_BANANA) {
|
|
256
|
+
const { apiKey } = resolveNanoBananaApiKey(env);
|
|
257
|
+
if (!apiKey) throw new Error('Nano Banana image-native generation requires GOOGLE_API_KEY or GEMINI_API_KEY.');
|
|
258
|
+
return generateNanoBananaImage({ prompt, apiKey, model: model || DEFAULT_NANO_BANANA_MODEL, aspectRatio: DEFAULT_NANO_BANANA_ASPECT_RATIO, imageSize: '4K', fetchImpl });
|
|
259
|
+
}
|
|
260
|
+
throw new Error(`Unsupported image-native provider: ${provider}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function assetExtension(mimeType) {
|
|
264
|
+
if (mimeType === 'image/jpeg') return 'jpg';
|
|
265
|
+
if (mimeType === 'image/webp') return 'webp';
|
|
266
|
+
return 'png';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function loadOutline(outlinePath) {
|
|
270
|
+
return readFile(resolve(outlinePath), 'utf8');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function resolveTemplatePack({ slidesDir, templatePack, templatePackPath }) {
|
|
274
|
+
if (templatePack) return parseTemplatePack(templatePack);
|
|
275
|
+
if (templatePackPath) return parseTemplatePack(await readFile(resolve(templatePackPath), 'utf8'), { path: resolve(templatePackPath) });
|
|
276
|
+
return loadTemplatePackFromSlidesDir(slidesDir, { optional: true });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function generateImageNativeSlides({
|
|
280
|
+
outlinePath,
|
|
281
|
+
slidesDir = 'slides',
|
|
282
|
+
templatePack = null,
|
|
283
|
+
templatePackPath = '',
|
|
284
|
+
provider = IMAGE_PROVIDER_CODEX,
|
|
285
|
+
model = '',
|
|
286
|
+
baseUrl = '',
|
|
287
|
+
env = process.env,
|
|
288
|
+
fetchImpl = globalThis.fetch,
|
|
289
|
+
generateImageImpl = null,
|
|
290
|
+
} = {}) {
|
|
291
|
+
if (!outlinePath) throw new Error('generateImageNativeSlides requires outlinePath.');
|
|
292
|
+
const absoluteSlidesDir = resolve(slidesDir);
|
|
293
|
+
const assetsDir = join(absoluteSlidesDir, 'assets');
|
|
294
|
+
const metadataDir = join(absoluteSlidesDir, IMAGE_NATIVE_METADATA_DIR);
|
|
295
|
+
await mkdir(assetsDir, { recursive: true });
|
|
296
|
+
await mkdir(metadataDir, { recursive: true });
|
|
297
|
+
|
|
298
|
+
const outlineMarkdown = await loadOutline(outlinePath);
|
|
299
|
+
const slides = parseImageNativeOutline(outlineMarkdown);
|
|
300
|
+
const pack = await resolveTemplatePack({ slidesDir: absoluteSlidesDir, templatePack, templatePackPath });
|
|
301
|
+
const normalizedProvider = normalizeProvider(provider);
|
|
302
|
+
const resolvedModel = model || '';
|
|
303
|
+
const generatedSlides = [];
|
|
304
|
+
|
|
305
|
+
for (const slide of slides) {
|
|
306
|
+
const selected = pack ? selectTemplateLayout(slide, pack) : { layoutId: null, layoutKind: null };
|
|
307
|
+
const layout = selected.layoutId ? findLayoutById(pack, selected.layoutId) : null;
|
|
308
|
+
const prompt = buildImageNativePrompt({ slide, templatePack: pack, layout });
|
|
309
|
+
const generated = generateImageImpl
|
|
310
|
+
? await generateImageImpl({ prompt, slide, layout, provider: normalizedProvider, model: resolvedModel })
|
|
311
|
+
: await defaultGenerateImage({ prompt, provider: normalizedProvider, model: resolvedModel, env, baseUrl, fetchImpl });
|
|
312
|
+
const extension = assetExtension(generated.mimeType);
|
|
313
|
+
const slideStem = `slide-${String(slide.index).padStart(2, '0')}`;
|
|
314
|
+
const assetFile = `${slideStem}.${extension}`;
|
|
315
|
+
const slideFile = `${slideStem}.html`;
|
|
316
|
+
const metadataFile = `${slideStem}.json`;
|
|
317
|
+
const assetRef = `./assets/${assetFile}`;
|
|
318
|
+
const metadataRef = `${IMAGE_NATIVE_METADATA_DIR}/${metadataFile}`;
|
|
319
|
+
|
|
320
|
+
await writeFile(join(assetsDir, assetFile), generated.bytes);
|
|
321
|
+
await writeFile(join(absoluteSlidesDir, slideFile), buildWrapperHtml({ title: slide.title, assetRef, metadataRef }), 'utf8');
|
|
322
|
+
|
|
323
|
+
const metadata = {
|
|
324
|
+
schemaVersion: 1,
|
|
325
|
+
mode: 'image-native',
|
|
326
|
+
slideFile,
|
|
327
|
+
assetRef,
|
|
328
|
+
prompt,
|
|
329
|
+
provider: normalizedProvider,
|
|
330
|
+
model: resolvedModel || (normalizedProvider === IMAGE_PROVIDER_OPENAI ? DEFAULT_OPENAI_IMAGE_MODEL : normalizedProvider === IMAGE_PROVIDER_CODEX ? DEFAULT_CODEX_MODEL : DEFAULT_NANO_BANANA_MODEL),
|
|
331
|
+
templateLayoutId: selected.layoutId,
|
|
332
|
+
templateLayoutKind: selected.layoutKind,
|
|
333
|
+
sourceOutline: {
|
|
334
|
+
path: outlinePath,
|
|
335
|
+
slideIndex: slide.index,
|
|
336
|
+
title: slide.title,
|
|
337
|
+
markdown: slide.sourceMarkdown,
|
|
338
|
+
},
|
|
339
|
+
regenerationHistory: [],
|
|
340
|
+
};
|
|
341
|
+
await writeFile(join(metadataDir, metadataFile), `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
|
|
342
|
+
|
|
343
|
+
generatedSlides.push({
|
|
344
|
+
slideFile,
|
|
345
|
+
assetRef,
|
|
346
|
+
metadataFile: `${IMAGE_NATIVE_METADATA_DIR}/${metadataFile}`,
|
|
347
|
+
provider: metadata.provider,
|
|
348
|
+
model: metadata.model,
|
|
349
|
+
templateLayoutId: metadata.templateLayoutId,
|
|
350
|
+
templateLayoutKind: metadata.templateLayoutKind,
|
|
351
|
+
prompt,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
outlinePath,
|
|
357
|
+
slidesDir: absoluteSlidesDir,
|
|
358
|
+
templatePackName: pack?.name ?? null,
|
|
359
|
+
provider: normalizedProvider,
|
|
360
|
+
model: resolvedModel,
|
|
361
|
+
slides: generatedSlides,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function extractImageNativeMetadataRef(html) {
|
|
366
|
+
const match = html.match(/<meta\s+name=["']slides-grab-image-native-metadata["']\s+content=["']([^"']+)["']/i);
|
|
367
|
+
return match?.[1] ?? null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function ensureImageNativeHtml(html, slideFile) {
|
|
371
|
+
if (!/<meta\s+name=["']slides-grab-image-native["']\s+content=["']true["']/i.test(html)) {
|
|
372
|
+
throw new Error(`${slideFile} is not an image-native slide. Generate it with slides-grab image --image-native --name slide-XX or open HTML slides with slides-grab edit.`);
|
|
373
|
+
}
|
|
374
|
+
const metadataRef = extractImageNativeMetadataRef(html);
|
|
375
|
+
if (!metadataRef) {
|
|
376
|
+
throw new Error(`${slideFile} is missing image-native regeneration metadata.`);
|
|
377
|
+
}
|
|
378
|
+
return metadataRef;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function buildRegenerationPrompt({ metadata, prompt, selections = [] }) {
|
|
382
|
+
const boxes = selections
|
|
383
|
+
.map((selection, index) => `- Region ${index + 1}: x=${selection.bbox.x}, y=${selection.bbox.y}, width=${selection.bbox.width}, height=${selection.bbox.height}`)
|
|
384
|
+
.join('\n');
|
|
385
|
+
return [
|
|
386
|
+
'Regenerate this existing image-native slide as a complete replacement full-slide image.',
|
|
387
|
+
'The original image is provided as a reference input — preserve its overall layout, style, and content, but apply the user critique to the specified regions (or the whole slide if no regions are selected).',
|
|
388
|
+
'Keep the original slide intent and template constraints, but apply the user critique.',
|
|
389
|
+
'',
|
|
390
|
+
`User critique: ${prompt}`,
|
|
391
|
+
boxes ? `Selected bbox feedback regions (slide coordinate space):\n${boxes}` : 'Selected bbox feedback regions: none (apply the critique to the whole slide).',
|
|
392
|
+
'',
|
|
393
|
+
'Original generation prompt:',
|
|
394
|
+
metadata.prompt,
|
|
395
|
+
].join('\n');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function throwIfImageRegenerationAborted(signal) {
|
|
399
|
+
if (signal?.aborted) {
|
|
400
|
+
throw new Error('Image regeneration was aborted.');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export async function regenerateImageNativeSlide({
|
|
405
|
+
slidesDir,
|
|
406
|
+
slideFile,
|
|
407
|
+
prompt,
|
|
408
|
+
selections = [],
|
|
409
|
+
provider = IMAGE_PROVIDER_CODEX,
|
|
410
|
+
model = '',
|
|
411
|
+
baseUrl = '',
|
|
412
|
+
env = process.env,
|
|
413
|
+
fetchImpl = globalThis.fetch,
|
|
414
|
+
generateImageImpl = null,
|
|
415
|
+
signal = null,
|
|
416
|
+
} = {}) {
|
|
417
|
+
if (!slidesDir) throw new Error('regenerateImageNativeSlide requires slidesDir.');
|
|
418
|
+
if (!slideFile) throw new Error('regenerateImageNativeSlide requires slideFile.');
|
|
419
|
+
if (typeof prompt !== 'string' || prompt.trim() === '') throw new Error('regenerateImageNativeSlide requires a prompt.');
|
|
420
|
+
|
|
421
|
+
const absoluteSlidesDir = resolve(slidesDir);
|
|
422
|
+
const htmlPath = join(absoluteSlidesDir, slideFile);
|
|
423
|
+
const html = await readFile(htmlPath, 'utf8');
|
|
424
|
+
const metadataRef = ensureImageNativeHtml(html, slideFile);
|
|
425
|
+
const metadataPath = join(absoluteSlidesDir, metadataRef);
|
|
426
|
+
const metadata = JSON.parse(await readFile(metadataPath, 'utf8'));
|
|
427
|
+
const normalizedProvider = normalizeProvider(provider || metadata.provider || IMAGE_PROVIDER_CODEX);
|
|
428
|
+
const resolvedModel = model || metadata.model || '';
|
|
429
|
+
const regenerationPrompt = buildRegenerationPrompt({ metadata, prompt: prompt.trim(), selections });
|
|
430
|
+
throwIfImageRegenerationAborted(signal);
|
|
431
|
+
|
|
432
|
+
const assetPath = join(absoluteSlidesDir, metadata.assetRef.replace(/^\.\//, ''));
|
|
433
|
+
const referenceImages = existsSync(assetPath) ? [assetPath] : [];
|
|
434
|
+
|
|
435
|
+
const generated = generateImageImpl
|
|
436
|
+
? await generateImageImpl({ prompt: regenerationPrompt, metadata, selections, provider: normalizedProvider, model: resolvedModel })
|
|
437
|
+
: await defaultGenerateImage({ prompt: regenerationPrompt, provider: normalizedProvider, model: resolvedModel, env, baseUrl, fetchImpl, referenceImages });
|
|
438
|
+
throwIfImageRegenerationAborted(signal);
|
|
439
|
+
|
|
440
|
+
await writeFile(assetPath, generated.bytes);
|
|
441
|
+
|
|
442
|
+
const historyEntry = {
|
|
443
|
+
at: new Date().toISOString(),
|
|
444
|
+
prompt: prompt.trim(),
|
|
445
|
+
provider: normalizedProvider,
|
|
446
|
+
model: resolvedModel,
|
|
447
|
+
selections,
|
|
448
|
+
previousPrompt: metadata.prompt,
|
|
449
|
+
};
|
|
450
|
+
const nextMetadata = {
|
|
451
|
+
...metadata,
|
|
452
|
+
prompt: regenerationPrompt,
|
|
453
|
+
provider: normalizedProvider,
|
|
454
|
+
model: resolvedModel,
|
|
455
|
+
regenerationHistory: [...(Array.isArray(metadata.regenerationHistory) ? metadata.regenerationHistory : []), historyEntry],
|
|
456
|
+
};
|
|
457
|
+
await writeFile(metadataPath, `${JSON.stringify(nextMetadata, null, 2)}\n`, 'utf8');
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
slideFile,
|
|
461
|
+
assetRef: metadata.assetRef,
|
|
462
|
+
metadataFile: metadataRef,
|
|
463
|
+
provider: normalizedProvider,
|
|
464
|
+
model: resolvedModel,
|
|
465
|
+
prompt: regenerationPrompt,
|
|
466
|
+
regenerationHistoryLength: nextMetadata.regenerationHistory.length,
|
|
467
|
+
};
|
|
468
|
+
}
|