slides-grab 1.2.5 → 1.2.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/README.md +21 -5
- package/bin/ppt-agent.js +16 -4
- package/package.json +6 -3
- package/scripts/editor-server.js +169 -14
- package/scripts/generate-image.js +44 -0
- package/skills/slides-grab/SKILL.md +3 -3
- package/skills/slides-grab/references/presentation-workflow-reference.md +3 -3
- package/skills/slides-grab-design/SKILL.md +3 -3
- package/skills/slides-grab-design/references/design-rules.md +3 -3
- package/skills/slides-grab-design/references/detailed-design-rules.md +2 -2
- package/src/editor/codex-edit.js +8 -12
- package/src/editor/edit-subprocess.js +78 -5
- package/src/editor/editor-codex-prompt.md +2 -2
- package/src/editor/editor.html +3 -0
- package/src/editor/js/editor-state.js +2 -2
- package/src/editor/js/model-registry.js +38 -0
- package/src/god-tibo-imagen.js +135 -0
- package/src/nano-banana.js +332 -27
package/src/editor/codex-edit.js
CHANGED
|
@@ -52,7 +52,7 @@ const EDITOR_PPT_DESIGN_DUPLICATE_PATTERNS = [
|
|
|
52
52
|
const EDITOR_PPT_DESIGN_SKILL_FALLBACK = [
|
|
53
53
|
'## Workflow',
|
|
54
54
|
'1. Read approved `slide-outline.md` or the existing slide before editing.',
|
|
55
|
-
'2. When a slide needs bespoke imagery, prefer `slides-grab image --prompt "<prompt>" --slides-dir <path
|
|
55
|
+
'2. When a slide needs bespoke imagery, prefer `slides-grab image --prompt "<prompt>" --slides-dir <path>`. The default provider is god-tibo-imagen, which reuses your local Codex ChatGPT login (`codex login`) and saves a local asset under `<slides-dir>/assets/` without requiring an API key.',
|
|
56
56
|
'3. Run `slides-grab validate --slides-dir <path>` after generation or edits.',
|
|
57
57
|
'4. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.',
|
|
58
58
|
'5. Run `slides-grab build-viewer --slides-dir <path>` only after validation passes.',
|
|
@@ -70,8 +70,8 @@ const EDITOR_PPT_DESIGN_SKILL_FALLBACK = [
|
|
|
70
70
|
'- Do not leave remote `http(s)://` image URLs in saved slide HTML; download source images into `<slides-dir>/assets/` and reference them as `./assets/<file>`.',
|
|
71
71
|
'- For local videos, use `<video src="./assets/<file>">` and prefer `poster="./assets/<file>"` so PDF export can use a thumbnail.',
|
|
72
72
|
'- If a video starts on YouTube or another supported page, use `slides-grab fetch-video --url <youtube-url> --slides-dir <path>` (or `yt-dlp` directly if needed) to download it into `<slides-dir>/assets/` before saving the slide HTML.',
|
|
73
|
-
'- Prefer `slides-grab image` with
|
|
74
|
-
'-
|
|
73
|
+
'- Prefer `slides-grab image` with god-tibo-imagen (the default) for bespoke imagery when it improves the slide.',
|
|
74
|
+
'- The default provider, god-tibo-imagen, reuses your local Codex ChatGPT login (`~/.codex/auth.json`) — no API key required. Run `codex login` once to enable it. WARNING: god-tibo-imagen calls an unsupported private Codex backend that may break without notice. Optional fallbacks: set `OPENAI_API_KEY` (Codex/OpenAI gpt-image-2; maps `--aspect-ratio` to the nearest supported OpenAI image size) or `GOOGLE_API_KEY`/`GEMINI_API_KEY` (Nano Banana; supports `--image-size 2K|4K`). If image generation credentials are unavailable, fall back to web search + download into `<slides-dir>/assets/`.',
|
|
75
75
|
'- Prefer `<img>` for slide imagery and `data-image-placeholder` when no final asset exists.',
|
|
76
76
|
'- Do not present slides for review until `slides-grab validate --slides-dir <path>` passes.',
|
|
77
77
|
'- Do not start conversion before approval.',
|
|
@@ -89,12 +89,12 @@ const DETAILED_DESIGN_SKILL_FALLBACK = [
|
|
|
89
89
|
'- Always include alt on img tags.',
|
|
90
90
|
'- Use ./assets/<file> as the default image and video contract for slide HTML.',
|
|
91
91
|
'- Keep slide assets in <slides-dir>/assets/.',
|
|
92
|
-
'- Use `slides-grab image --prompt "<prompt>" --slides-dir <path>`
|
|
92
|
+
'- Use `slides-grab image --prompt "<prompt>" --slides-dir <path>` (default provider: god-tibo-imagen via `codex login`) when the slide needs bespoke imagery.',
|
|
93
93
|
'- data: URLs are allowed for fully self-contained slides.',
|
|
94
94
|
'- Do not leave remote http(s):// image URLs in saved slide HTML; download source images into <slides-dir>/assets/ and reference them as ./assets/<file>.',
|
|
95
95
|
'- Store local videos under <slides-dir>/assets/, reference them as ./assets/<file>, and prefer poster images under ./assets/ for PDF export.',
|
|
96
96
|
'- If a video starts on YouTube or another supported page, use slides-grab fetch-video --url <youtube-url> --slides-dir <path> (or yt-dlp directly if needed) before saving slide HTML.',
|
|
97
|
-
'-
|
|
97
|
+
'- Default provider god-tibo-imagen reuses your local Codex ChatGPT login (~/.codex/auth.json) — no API key required (run `codex login` once). WARNING: god-tibo calls an unsupported private Codex backend that may break without notice. Optional fallbacks: OPENAI_API_KEY (Codex/OpenAI gpt-image-2; maps --aspect-ratio to nearest OpenAI size) or GOOGLE_API_KEY/GEMINI_API_KEY (Nano Banana; --image-size 2K|4K). If credentials are unavailable, fall back to web search + download into <slides-dir>/assets/.',
|
|
98
98
|
'- Do not use absolute filesystem paths in slide HTML.',
|
|
99
99
|
'- Do not use non-body background-image for content imagery; use <img> instead.',
|
|
100
100
|
'- Use data-image-placeholder to reserve space when no image is available yet.',
|
|
@@ -419,8 +419,8 @@ export function buildCodexEditPrompt({ slideFile, slidePath, userPrompt, slideMo
|
|
|
419
419
|
`- Keep slide dimensions at ${sizeLabel}.`,
|
|
420
420
|
'- Keep text in semantic tags (<p>, <h1>-<h6>, <ul>, <ol>, <li>).',
|
|
421
421
|
'- You may add or update supporting files required for the requested slide, including local images and videos under <slides-dir>/assets/ and tldraw source/export files used to generate those assets.',
|
|
422
|
-
'- When the request needs bespoke imagery, prefer `slides-grab image --prompt "<prompt>" --slides-dir <path>` so
|
|
423
|
-
'-
|
|
422
|
+
'- When the request needs bespoke imagery, prefer `slides-grab image --prompt "<prompt>" --slides-dir <path>` so the default god-tibo-imagen provider saves the asset under <slides-dir>/assets/ via your Codex ChatGPT login (run `codex login` once if needed).',
|
|
423
|
+
'- Default provider god-tibo-imagen reuses ~/.codex/auth.json — no API key required. WARNING: god-tibo calls an unsupported private Codex backend that may break without notice. Optional fallbacks: OPENAI_API_KEY (Codex/OpenAI gpt-image-2; maps --aspect-ratio to nearest OpenAI size) or GOOGLE_API_KEY/GEMINI_API_KEY (Nano Banana; --image-size 2K|4K). If credentials are unavailable, fall back to web search + download into <slides-dir>/assets/.',
|
|
424
424
|
'- If you create or update a supporting asset, store it under <slides-dir>/assets/ and reference it from the requested slide as ./assets/<file>.',
|
|
425
425
|
'- If you need a web-hosted video, download it into <slides-dir>/assets/ first with slides-grab fetch-video --url <youtube-url> --slides-dir <path> (or yt-dlp directly if needed), then reference only the local file.',
|
|
426
426
|
'- Keep local assets under ./assets/ and preserve portable relative paths.',
|
|
@@ -450,11 +450,7 @@ export function buildCodexExecArgs({ prompt, imagePath, model }) {
|
|
|
450
450
|
return args;
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
-
export
|
|
454
|
-
|
|
455
|
-
export function isClaudeModel(model) {
|
|
456
|
-
return typeof model === 'string' && CLAUDE_MODELS.includes(model.trim());
|
|
457
|
-
}
|
|
453
|
+
export { CLAUDE_MODELS, isClaudeModel } from './js/model-registry.js';
|
|
458
454
|
|
|
459
455
|
export function buildClaudeExecArgs({ prompt, imagePath, model }) {
|
|
460
456
|
const args = [
|
|
@@ -23,6 +23,14 @@ export function buildEditTimeoutMessage({ engineLabel = 'Editor process', timeou
|
|
|
23
23
|
return `${engineLabel} edit timed out after ${timeoutMs}ms and was terminated.`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export const EDIT_ABORT_EXIT_CODE = 130;
|
|
27
|
+
export const EDIT_ABORT_KILL_SIGNAL = 'SIGTERM';
|
|
28
|
+
export const EDIT_ABORT_FORCE_KILL_AFTER_MS = 5_000;
|
|
29
|
+
|
|
30
|
+
export function buildEditAbortMessage({ engineLabel = 'Editor process' } = {}) {
|
|
31
|
+
return `${engineLabel} edit was aborted and the child process was terminated.`;
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
export function runEditSubprocess({
|
|
27
35
|
bin,
|
|
28
36
|
args,
|
|
@@ -32,32 +40,79 @@ export function runEditSubprocess({
|
|
|
32
40
|
timeoutMs = DEFAULT_EDIT_TIMEOUT_MS,
|
|
33
41
|
engineLabel,
|
|
34
42
|
onLog = () => {},
|
|
43
|
+
onChild = () => {},
|
|
44
|
+
signal,
|
|
35
45
|
spawnImpl = spawn,
|
|
36
46
|
}) {
|
|
37
47
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
38
48
|
const child = spawnImpl(bin, args, { cwd, env, stdio });
|
|
39
49
|
|
|
50
|
+
try {
|
|
51
|
+
onChild(child);
|
|
52
|
+
} catch {
|
|
53
|
+
// Never let a faulty observer crash the spawn lifecycle.
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
let stdout = '';
|
|
41
57
|
let stderr = '';
|
|
42
58
|
let timedOut = false;
|
|
59
|
+
let aborted = false;
|
|
43
60
|
let settled = false;
|
|
44
61
|
let forceKillTimer = null;
|
|
62
|
+
let abortForceKillTimer = null;
|
|
45
63
|
|
|
46
64
|
const timeoutMessage = buildEditTimeoutMessage({ engineLabel, timeoutMs });
|
|
65
|
+
const abortMessage = buildEditAbortMessage({ engineLabel });
|
|
47
66
|
|
|
48
67
|
const timeoutTimer = setTimeout(() => {
|
|
68
|
+
if (settled || aborted) return;
|
|
49
69
|
timedOut = true;
|
|
50
70
|
const messageLine = `${timeoutMessage}\n`;
|
|
51
71
|
stderr += messageLine;
|
|
52
72
|
onLog('stderr', messageLine);
|
|
53
|
-
|
|
73
|
+
try {
|
|
74
|
+
child.kill(EDIT_TIMEOUT_KILL_SIGNAL);
|
|
75
|
+
} catch {}
|
|
54
76
|
forceKillTimer = setTimeout(() => {
|
|
55
|
-
|
|
77
|
+
try {
|
|
78
|
+
child.kill('SIGKILL');
|
|
79
|
+
} catch {}
|
|
56
80
|
}, EDIT_TIMEOUT_FORCE_KILL_AFTER_MS);
|
|
57
81
|
forceKillTimer.unref?.();
|
|
58
82
|
}, timeoutMs);
|
|
59
83
|
timeoutTimer.unref?.();
|
|
60
84
|
|
|
85
|
+
function abortChild() {
|
|
86
|
+
if (settled || aborted) return;
|
|
87
|
+
aborted = true;
|
|
88
|
+
const messageLine = `${abortMessage}\n`;
|
|
89
|
+
stderr += messageLine;
|
|
90
|
+
try {
|
|
91
|
+
onLog('stderr', messageLine);
|
|
92
|
+
} catch {}
|
|
93
|
+
try {
|
|
94
|
+
child.kill(EDIT_ABORT_KILL_SIGNAL);
|
|
95
|
+
} catch {}
|
|
96
|
+
abortForceKillTimer = setTimeout(() => {
|
|
97
|
+
try {
|
|
98
|
+
child.kill('SIGKILL');
|
|
99
|
+
} catch {}
|
|
100
|
+
}, EDIT_ABORT_FORCE_KILL_AFTER_MS);
|
|
101
|
+
abortForceKillTimer.unref?.();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (signal) {
|
|
105
|
+
if (signal.aborted) {
|
|
106
|
+
setImmediate(abortChild);
|
|
107
|
+
} else {
|
|
108
|
+
const onAbort = () => abortChild();
|
|
109
|
+
signal.addEventListener?.('abort', onAbort, { once: true });
|
|
110
|
+
child.once('close', () => {
|
|
111
|
+
signal.removeEventListener?.('abort', onAbort);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
61
116
|
child.stdout?.on('data', (chunk) => {
|
|
62
117
|
const text = chunk.toString();
|
|
63
118
|
stdout += text;
|
|
@@ -70,19 +125,36 @@ export function runEditSubprocess({
|
|
|
70
125
|
onLog('stderr', text);
|
|
71
126
|
});
|
|
72
127
|
|
|
73
|
-
child.on('close', (code,
|
|
128
|
+
child.on('close', (code, exitSignal) => {
|
|
74
129
|
if (settled) return;
|
|
75
130
|
settled = true;
|
|
76
131
|
clearTimeout(timeoutTimer);
|
|
77
132
|
clearTimeout(forceKillTimer);
|
|
133
|
+
clearTimeout(abortForceKillTimer);
|
|
134
|
+
|
|
135
|
+
let resolvedCode;
|
|
136
|
+
if (timedOut) {
|
|
137
|
+
resolvedCode = EDIT_TIMEOUT_EXIT_CODE;
|
|
138
|
+
} else if (aborted) {
|
|
139
|
+
resolvedCode = EDIT_ABORT_EXIT_CODE;
|
|
140
|
+
} else {
|
|
141
|
+
resolvedCode = code ?? 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let resolvedSignal = exitSignal;
|
|
145
|
+
if (timedOut && !exitSignal) resolvedSignal = EDIT_TIMEOUT_KILL_SIGNAL;
|
|
146
|
+
else if (aborted && !exitSignal) resolvedSignal = EDIT_ABORT_KILL_SIGNAL;
|
|
147
|
+
|
|
78
148
|
resolvePromise({
|
|
79
|
-
code:
|
|
149
|
+
code: resolvedCode,
|
|
80
150
|
stdout,
|
|
81
151
|
stderr,
|
|
82
|
-
signal:
|
|
152
|
+
signal: resolvedSignal,
|
|
83
153
|
timedOut,
|
|
154
|
+
aborted,
|
|
84
155
|
timeoutMs: timedOut ? timeoutMs : null,
|
|
85
156
|
timeoutMessage: timedOut ? timeoutMessage : null,
|
|
157
|
+
abortMessage: aborted ? abortMessage : null,
|
|
86
158
|
});
|
|
87
159
|
});
|
|
88
160
|
|
|
@@ -91,6 +163,7 @@ export function runEditSubprocess({
|
|
|
91
163
|
settled = true;
|
|
92
164
|
clearTimeout(timeoutTimer);
|
|
93
165
|
clearTimeout(forceKillTimer);
|
|
166
|
+
clearTimeout(abortForceKillTimer);
|
|
94
167
|
rejectPromise(error);
|
|
95
168
|
});
|
|
96
169
|
});
|
|
@@ -29,8 +29,8 @@ The user's edit request is the primary objective. All rules below exist to suppo
|
|
|
29
29
|
- Do not use absolute filesystem paths in slide HTML.
|
|
30
30
|
- Do not use non-body `background-image` for content imagery; use `<img>` instead.
|
|
31
31
|
- Use `data-image-placeholder` to reserve space when no image is available yet.
|
|
32
|
-
- When the request needs bespoke imagery, prefer `slides-grab image --prompt "<prompt>" --slides-dir <path>` so
|
|
33
|
-
-
|
|
32
|
+
- When the request needs bespoke imagery, prefer `slides-grab image --prompt "<prompt>" --slides-dir <path>` so the default god-tibo-imagen provider saves the asset under `<slides-dir>/assets/` via your local Codex ChatGPT login (run `codex login` once if needed).
|
|
33
|
+
- Default provider god-tibo-imagen reuses `~/.codex/auth.json` — no OpenAI/Google API key required; requires a Codex/ChatGPT account entitled to image generation. ⚠️ god-tibo calls an unsupported private Codex backend that may break without notice. Optional fallbacks: `--provider codex` (Codex/OpenAI gpt-image-2 via `OPENAI_API_KEY`; maps `--aspect-ratio` to the nearest supported OpenAI image size; `--image-size 2K|4K` is Nano Banana-only) or `--provider nano-banana` (Google `gemini-3-pro-image-preview` via `GOOGLE_API_KEY` / `GEMINI_API_KEY`; supports `--image-size 2K|4K`). If credentials are unavailable, fall back to web search + download into `./assets/`.
|
|
34
34
|
- For local videos, use `<video src="./assets/<file>">` with `poster="./assets/<file>"`.
|
|
35
35
|
- If a video starts on YouTube or a supported page, use `slides-grab fetch-video --url <url> --slides-dir <path>` to download first.
|
|
36
36
|
|
package/src/editor/editor.html
CHANGED
|
@@ -1630,9 +1630,12 @@
|
|
|
1630
1630
|
<div class="sidebar-section">
|
|
1631
1631
|
<span class="sidebar-label">Model</span>
|
|
1632
1632
|
<select id="model-select" class="model-select">
|
|
1633
|
+
<option value="gpt-5.5">gpt-5.5</option>
|
|
1633
1634
|
<option value="gpt-5.4">gpt-5.4</option>
|
|
1634
1635
|
<option value="gpt-5.3-codex">gpt-5.3-codex</option>
|
|
1635
1636
|
<option value="gpt-5.3-codex-spark">gpt-5.3-codex-spark</option>
|
|
1637
|
+
<option value="claude-opus-4-7">claude-opus-4-7</option>
|
|
1638
|
+
<option value="claude-sonnet-4-6">claude-sonnet-4-6</option>
|
|
1636
1639
|
</select>
|
|
1637
1640
|
</div>
|
|
1638
1641
|
<button class="sidebar-btn" id="btn-send" title="Run Codex">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { ALL_MODELS } from './model-registry.js';
|
|
2
2
|
|
|
3
3
|
export let SLIDE_W = 960;
|
|
4
4
|
export let SLIDE_H = 540;
|
|
@@ -15,7 +15,7 @@ export const POPOVER_TEXT = 'text';
|
|
|
15
15
|
export const POPOVER_TEXT_COLOR = 'text-color';
|
|
16
16
|
export const POPOVER_BG_COLOR = 'bg-color';
|
|
17
17
|
export const POPOVER_SIZE = 'size';
|
|
18
|
-
export const DEFAULT_MODELS =
|
|
18
|
+
export const DEFAULT_MODELS = ALL_MODELS.slice();
|
|
19
19
|
export const DIRECT_TEXT_TAGS = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li']);
|
|
20
20
|
export const NON_SELECTABLE_TAGS = new Set(['html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript']);
|
|
21
21
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Canonical editor model registry — single source of truth for the slide
|
|
2
|
+
// editor's model dropdown and dispatch routing. Imported by both Node code
|
|
3
|
+
// (scripts/editor-server.js, src/editor/codex-edit.js) and browser code
|
|
4
|
+
// (src/editor/js/editor-state.js), so this module MUST stay browser-safe:
|
|
5
|
+
// no Node imports, no Node-only globals.
|
|
6
|
+
//
|
|
7
|
+
// To add a new model:
|
|
8
|
+
// 1. Append to CODEX_MODELS or CLAUDE_MODELS below.
|
|
9
|
+
// 2. Add a matching <option> to src/editor/editor.html (also a fallback).
|
|
10
|
+
// 3. Existing tests/editor/editor-model-dispatch.test.js will automatically
|
|
11
|
+
// verify the new model dispatches correctly through the editor pipeline.
|
|
12
|
+
// No per-model test additions needed.
|
|
13
|
+
|
|
14
|
+
export const CODEX_MODELS = [
|
|
15
|
+
'gpt-5.5',
|
|
16
|
+
'gpt-5.4',
|
|
17
|
+
'gpt-5.3-codex',
|
|
18
|
+
'gpt-5.3-codex-spark',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const CLAUDE_MODELS = ['claude-opus-4-7', 'claude-sonnet-4-6'];
|
|
22
|
+
|
|
23
|
+
export const ALL_MODELS = [...CODEX_MODELS, ...CLAUDE_MODELS];
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_CODEX_MODEL = CODEX_MODELS[0];
|
|
26
|
+
export const DEFAULT_MODEL = DEFAULT_CODEX_MODEL;
|
|
27
|
+
|
|
28
|
+
export function isClaudeModel(model) {
|
|
29
|
+
return typeof model === 'string' && CLAUDE_MODELS.includes(model.trim());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isCodexModel(model) {
|
|
33
|
+
return typeof model === 'string' && CODEX_MODELS.includes(model.trim());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isKnownEditorModel(model) {
|
|
37
|
+
return isCodexModel(model) || isClaudeModel(model);
|
|
38
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { mkdir, readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createProvider as defaultCreateProvider,
|
|
8
|
+
resolveConfig as defaultResolveConfig,
|
|
9
|
+
} from 'god-tibo-imagen';
|
|
10
|
+
|
|
11
|
+
export const GOD_TIBO_DEFAULT_MODEL = 'gpt-5.4';
|
|
12
|
+
export const GOD_TIBO_PROVIDER_AUTO = 'auto';
|
|
13
|
+
export const GOD_TIBO_PROVIDER_PRIVATE_CODEX = 'private-codex';
|
|
14
|
+
export const GOD_TIBO_PROVIDER_CODEX_CLI = 'codex-cli';
|
|
15
|
+
|
|
16
|
+
const ASPECT_RATIO_HINTS = new Map([
|
|
17
|
+
['16:9', 'wide landscape 16:9 aspect ratio'],
|
|
18
|
+
['9:16', 'tall portrait 9:16 aspect ratio'],
|
|
19
|
+
['1:1', 'square 1:1 aspect ratio'],
|
|
20
|
+
['4:3', '4:3 aspect ratio'],
|
|
21
|
+
['3:4', 'portrait 3:4 aspect ratio'],
|
|
22
|
+
['3:2', 'landscape 3:2 aspect ratio'],
|
|
23
|
+
['2:3', 'portrait 2:3 aspect ratio'],
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export function injectAspectRatioHint(prompt, aspectRatio) {
|
|
27
|
+
if (typeof prompt !== 'string' || prompt.trim() === '') {
|
|
28
|
+
throw new Error('injectAspectRatioHint: prompt must be a non-empty string.');
|
|
29
|
+
}
|
|
30
|
+
if (!aspectRatio || typeof aspectRatio !== 'string') {
|
|
31
|
+
return prompt;
|
|
32
|
+
}
|
|
33
|
+
const trimmed = aspectRatio.trim();
|
|
34
|
+
const explicit = ASPECT_RATIO_HINTS.get(trimmed);
|
|
35
|
+
if (explicit) {
|
|
36
|
+
return `${prompt} (${explicit})`;
|
|
37
|
+
}
|
|
38
|
+
if (/^\d+(?:\.\d+)?:\d+(?:\.\d+)?$/.test(trimmed)) {
|
|
39
|
+
return `${prompt} (${trimmed} aspect ratio)`;
|
|
40
|
+
}
|
|
41
|
+
return prompt;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveGodTiboConfig({ providerMode = GOD_TIBO_PROVIDER_AUTO, resolveConfigImpl = defaultResolveConfig } = {}) {
|
|
45
|
+
return resolveConfigImpl({ provider: providerMode });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isCodexAuthError(error) {
|
|
49
|
+
if (!error || typeof error !== 'object') return false;
|
|
50
|
+
const code = String(error.code || '').toUpperCase();
|
|
51
|
+
if (code === 'UNAUTHORIZED' || code === 'ENOENT') return true;
|
|
52
|
+
const message = String(error.message || '').toLowerCase();
|
|
53
|
+
return (
|
|
54
|
+
message.includes('auth.json') ||
|
|
55
|
+
message.includes('unauthorized') ||
|
|
56
|
+
message.includes('chatgpt auth') ||
|
|
57
|
+
message.includes('codex login')
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getGodTiboFallbackMessage(reason) {
|
|
62
|
+
const summary = typeof reason === 'string' && reason.trim()
|
|
63
|
+
? reason.trim()
|
|
64
|
+
: 'god-tibo-imagen image generation failed.';
|
|
65
|
+
return `${summary} god-tibo-imagen is the default image provider and reuses your local Codex ChatGPT login (~/.codex/auth.json). Run \`codex login\` once to enable it. Optional fallbacks: set OPENAI_API_KEY (Codex/OpenAI gpt-image-2) or GOOGLE_API_KEY/GEMINI_API_KEY (Nano Banana). If image generation credentials are unavailable, use web search and download the chosen image into ./assets/<file>.`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function generateGodTiboImage({
|
|
69
|
+
prompt,
|
|
70
|
+
model = GOD_TIBO_DEFAULT_MODEL,
|
|
71
|
+
aspectRatio,
|
|
72
|
+
providerMode = GOD_TIBO_PROVIDER_AUTO,
|
|
73
|
+
dryRun = false,
|
|
74
|
+
deps = {},
|
|
75
|
+
} = {}) {
|
|
76
|
+
if (typeof prompt !== 'string' || prompt.trim() === '') {
|
|
77
|
+
throw new Error('generateGodTiboImage: prompt must be a non-empty string.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const createProviderImpl = deps.createProvider || defaultCreateProvider;
|
|
81
|
+
const resolveConfigImpl = deps.resolveConfig || defaultResolveConfig;
|
|
82
|
+
|
|
83
|
+
const enrichedPrompt = injectAspectRatioHint(prompt, aspectRatio);
|
|
84
|
+
const config = resolveGodTiboConfig({ providerMode, resolveConfigImpl });
|
|
85
|
+
const provider = createProviderImpl(config);
|
|
86
|
+
|
|
87
|
+
// god-tibo's provider.generateImage writes a PNG to outputPath as a side
|
|
88
|
+
// effect. slides-grab centralizes asset path resolution in
|
|
89
|
+
// saveNanoBananaImage (src/nano-banana.js) - so we route god-tibo's write
|
|
90
|
+
// to a tmp file and read the bytes back, letting the caller persist via
|
|
91
|
+
// the existing asset contract. This keeps the asset path policy single-sourced.
|
|
92
|
+
const tempDir = join(tmpdir(), `slides-grab-godtibo-${randomUUID()}`);
|
|
93
|
+
await mkdir(tempDir, { recursive: true });
|
|
94
|
+
const tempPath = join(tempDir, 'image.png');
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const result = await provider.generateImage({
|
|
98
|
+
prompt: enrichedPrompt,
|
|
99
|
+
model,
|
|
100
|
+
outputPath: tempPath,
|
|
101
|
+
dryRun: Boolean(dryRun),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (dryRun) {
|
|
105
|
+
return {
|
|
106
|
+
mimeType: 'image/png',
|
|
107
|
+
bytes: Buffer.alloc(0),
|
|
108
|
+
mode: result?.mode || 'dry-run',
|
|
109
|
+
warnings: Array.isArray(result?.warnings) ? result.warnings : [],
|
|
110
|
+
revisedPrompt: result?.revisedPrompt ?? null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const bytes = await readFile(tempPath);
|
|
115
|
+
return {
|
|
116
|
+
mimeType: 'image/png',
|
|
117
|
+
bytes,
|
|
118
|
+
mode: result?.mode || 'live',
|
|
119
|
+
warnings: Array.isArray(result?.warnings) ? result.warnings : [],
|
|
120
|
+
revisedPrompt: result?.revisedPrompt ?? null,
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const wrapped = new Error(getGodTiboFallbackMessage(error?.message || String(error)));
|
|
124
|
+
wrapped.cause = error;
|
|
125
|
+
wrapped.isAuthError = isCodexAuthError(error);
|
|
126
|
+
throw wrapped;
|
|
127
|
+
} finally {
|
|
128
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const __test_only__ = {
|
|
133
|
+
ASPECT_RATIO_HINTS,
|
|
134
|
+
isCodexAuthError,
|
|
135
|
+
};
|