@supportpages.io/wtfm 0.0.0-stage → 0.3.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 +201 -0
- package/NOTICE +23 -0
- package/README.md +392 -2
- package/dist/actions.d.ts +98 -0
- package/dist/actions.js +86 -0
- package/dist/actions.js.map +1 -0
- package/dist/agent-settings.d.ts +22 -0
- package/dist/agent-settings.js +35 -0
- package/dist/agent-settings.js.map +1 -0
- package/dist/api.d.ts +14 -0
- package/dist/api.js +159 -0
- package/dist/api.js.map +1 -0
- package/dist/article-link.d.ts +16 -0
- package/dist/article-link.js +25 -0
- package/dist/article-link.js.map +1 -0
- package/dist/artifacts.d.ts +45 -0
- package/dist/artifacts.js +144 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/brand.d.ts +4 -0
- package/dist/brand.js +9 -0
- package/dist/brand.js.map +1 -0
- package/dist/bridge.d.ts +2964 -0
- package/dist/bridge.js +1036 -0
- package/dist/bridge.js.map +1 -0
- package/dist/capacity.d.ts +5 -0
- package/dist/capacity.js +11 -0
- package/dist/capacity.js.map +1 -0
- package/dist/credentials.d.ts +13 -0
- package/dist/credentials.js +88 -0
- package/dist/credentials.js.map +1 -0
- package/dist/development-tls.d.ts +8 -0
- package/dist/development-tls.js +38 -0
- package/dist/development-tls.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/export.d.ts +11 -0
- package/dist/export.js +91 -0
- package/dist/export.js.map +1 -0
- package/dist/hosted-operations.d.ts +108 -0
- package/dist/hosted-operations.js +120 -0
- package/dist/hosted-operations.js.map +1 -0
- package/dist/hosting-benefits.d.ts +66 -0
- package/dist/hosting-benefits.js +67 -0
- package/dist/hosting-benefits.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/local-inventory.d.ts +22 -0
- package/dist/local-inventory.js +59 -0
- package/dist/local-inventory.js.map +1 -0
- package/dist/local-setup.d.ts +208 -0
- package/dist/local-setup.js +140 -0
- package/dist/local-setup.js.map +1 -0
- package/dist/pairing.d.ts +71 -0
- package/dist/pairing.js +235 -0
- package/dist/pairing.js.map +1 -0
- package/dist/preferences.d.ts +18 -0
- package/dist/preferences.js +44 -0
- package/dist/preferences.js.map +1 -0
- package/dist/progress.d.ts +205 -0
- package/dist/progress.js +224 -0
- package/dist/progress.js.map +1 -0
- package/dist/reminders.d.ts +31 -0
- package/dist/reminders.js +73 -0
- package/dist/reminders.js.map +1 -0
- package/dist/replace-connection.d.ts +6 -0
- package/dist/replace-connection.js +77 -0
- package/dist/replace-connection.js.map +1 -0
- package/dist/repository-actions.d.ts +18 -0
- package/dist/repository-actions.js +9 -0
- package/dist/repository-actions.js.map +1 -0
- package/dist/repository-benefits.d.ts +96 -0
- package/dist/repository-benefits.js +64 -0
- package/dist/repository-benefits.js.map +1 -0
- package/dist/run-update.d.ts +17 -0
- package/dist/run-update.js +27 -0
- package/dist/run-update.js.map +1 -0
- package/dist/runs.d.ts +541 -0
- package/dist/runs.js +146 -0
- package/dist/runs.js.map +1 -0
- package/dist/runtime.d.ts +12 -0
- package/dist/runtime.js +58 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +277 -0
- package/dist/schema.js +66 -0
- package/dist/schema.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +298 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +1714 -0
- package/dist/session.js +619 -0
- package/dist/session.js.map +1 -0
- package/dist/settings.d.ts +9 -0
- package/dist/settings.js +21 -0
- package/dist/settings.js.map +1 -0
- package/dist/sync.d.ts +495 -0
- package/dist/sync.js +191 -0
- package/dist/sync.js.map +1 -0
- package/dist/telemetry-scrub.d.ts +13 -0
- package/dist/telemetry-scrub.js +51 -0
- package/dist/telemetry-scrub.js.map +1 -0
- package/dist/telemetry.d.ts +73 -0
- package/dist/telemetry.js +173 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/walkthroughs.d.ts +224 -0
- package/dist/walkthroughs.js +109 -0
- package/dist/walkthroughs.js.map +1 -0
- package/dist/workspace.d.ts +14 -0
- package/dist/workspace.js +127 -0
- package/dist/workspace.js.map +1 -0
- package/dist/writer-agent.d.ts +21 -0
- package/dist/writer-agent.js +27 -0
- package/dist/writer-agent.js.map +1 -0
- package/dist/writer-entry.d.ts +171 -0
- package/dist/writer-entry.js +233 -0
- package/dist/writer-entry.js.map +1 -0
- package/dist/writing-style.d.ts +7 -0
- package/dist/writing-style.js +52 -0
- package/dist/writing-style.js.map +1 -0
- package/engine/SYNC.json +4 -0
- package/engine/VERSION +1 -0
- package/engine/detect-project/README.md +141 -0
- package/engine/detect-project/SKILL.md +1421 -0
- package/engine/detect-project/assets/desktop/desktop-frame.css +428 -0
- package/engine/detect-project/assets/game/game-frame.css +132 -0
- package/engine/detect-project/assets/macosui/LICENSE-puppertino.txt +21 -0
- package/engine/detect-project/assets/macosui/VERSIONS.txt +1 -0
- package/engine/detect-project/assets/macosui/fonts.css +15 -0
- package/engine/detect-project/assets/macosui/macos-frame.css +481 -0
- package/engine/detect-project/assets/macosui/puppertino.css +2153 -0
- package/engine/detect-project/assets/mobileui/LICENSE-fonts.txt +13 -0
- package/engine/detect-project/assets/mobileui/LICENSE-framework7.txt +52 -0
- package/engine/detect-project/assets/mobileui/VERSIONS.txt +6 -0
- package/engine/detect-project/assets/mobileui/device-frame.css +316 -0
- package/engine/detect-project/assets/mobileui/f7-color-theme.mjs +1345 -0
- package/engine/detect-project/assets/mobileui/f7-icons-names.json +1254 -0
- package/engine/detect-project/assets/mobileui/fonts.css +16 -0
- package/engine/detect-project/assets/mobileui/framework7-components.css +39 -0
- package/engine/detect-project/assets/mobileui/framework7-core.css +5245 -0
- package/engine/detect-project/assets/mobileui/icons.css +31 -0
- package/engine/detect-project/assets/mobileui/md3-defaults.css +89 -0
- package/engine/detect-project/assets/mobileui/platforms.json +46 -0
- package/engine/detect-project/assets/tailwind-fallback.css +1729 -0
- package/engine/detect-project/assets/webtui/LICENSE-webtui.txt +28 -0
- package/engine/detect-project/assets/webtui/VERSIONS.txt +7 -0
- package/engine/detect-project/assets/webtui/terminal-frame.css +195 -0
- package/engine/detect-project/assets/webtui/theme-catppuccin.css +1 -0
- package/engine/detect-project/assets/webtui/theme-everforest.css +1 -0
- package/engine/detect-project/assets/webtui/theme-gruvbox.css +1 -0
- package/engine/detect-project/assets/webtui/theme-nord.css +1 -0
- package/engine/detect-project/assets/webtui/theme-vitesse.css +1 -0
- package/engine/detect-project/assets/webtui/themes.json +37 -0
- package/engine/detect-project/assets/webtui/webtui-core.css +1 -0
- package/engine/detect-project/assets/win32ui/7css.css +2 -0
- package/engine/detect-project/assets/win32ui/LICENSE-7css.txt +21 -0
- package/engine/detect-project/assets/win32ui/VERSIONS.txt +1 -0
- package/engine/detect-project/assets/win32ui/win32-frame.css +278 -0
- package/engine/detect-project/package-lock.json +12 -0
- package/engine/detect-project/package.json +10 -0
- package/engine/detect-project/scripts/apply_runtime_profiles.js +313 -0
- package/engine/detect-project/scripts/check_css_health.js +412 -0
- package/engine/detect-project/scripts/check_project_map.js +150 -0
- package/engine/detect-project/scripts/check_runtime_coverage.js +311 -0
- package/engine/detect-project/scripts/check_runtime_recipe_quality.js +184 -0
- package/engine/detect-project/scripts/classify_app_type.sh +246 -0
- package/engine/detect-project/scripts/classify_surface.sh +95 -0
- package/engine/detect-project/scripts/classify_workspace.js +39 -0
- package/engine/detect-project/scripts/compile_css.sh +447 -0
- package/engine/detect-project/scripts/detect_static.js +645 -0
- package/engine/detect-project/scripts/detect_structure.js +187 -0
- package/engine/detect-project/scripts/include_census.js +451 -0
- package/engine/detect-project/scripts/json_get.js +142 -0
- package/engine/detect-project/scripts/merge_json.js +52 -0
- package/engine/detect-project/scripts/recommend_model_tier.js +252 -0
- package/engine/detect-project/scripts/resolve_route_chains.js +135 -0
- package/engine/detect-project/scripts/run_css_build.sh +40 -0
- package/engine/detect-project/scripts/sanitize_css.js +83 -0
- package/engine/detect-project/scripts/test_classify_surface.js +108 -0
- package/engine/detect-project/scripts/test_node_helpers.js +142 -0
- package/engine/detect-project/scripts/test_recommend_model_tier.js +119 -0
- package/engine/detect-project/scripts/test_resolve_route_chains.js +182 -0
- package/engine/detect-project/scripts/test_runtime_coverage.js +323 -0
- package/engine/detect-project/scripts/test_runtime_recipe_quality.js +187 -0
- package/engine/detect-project/scripts/theme_overrides.js +169 -0
- package/engine/detect-project/scripts/write_branding.js +129 -0
- package/engine/generate-illustrated-article/SKILL.md +461 -0
- package/engine/generate-illustrated-article/contracts/desktop.md +90 -0
- package/engine/generate-illustrated-article/contracts/game.md +38 -0
- package/engine/generate-illustrated-article/contracts/label-evidence.md +38 -0
- package/engine/generate-illustrated-article/contracts/macos.md +79 -0
- package/engine/generate-illustrated-article/contracts/mobile.md +40 -0
- package/engine/generate-illustrated-article/contracts/terminal.md +29 -0
- package/engine/generate-illustrated-article/contracts/win32.md +43 -0
- package/engine/generate-illustrated-article/package-lock.json +366 -0
- package/engine/generate-illustrated-article/package.json +15 -0
- package/engine/generate-illustrated-article/scripts/article_blocks.js +34 -0
- package/engine/generate-illustrated-article/scripts/check_article_json.js +105 -0
- package/engine/generate-illustrated-article/scripts/emit_walkthrough_signals.js +100 -0
- package/engine/generate-illustrated-article/scripts/extract_images.js +187 -0
- package/engine/generate-illustrated-article/scripts/generate_content_images.js +376 -0
- package/engine/generate-illustrated-article/scripts/include_census.js +451 -0
- package/engine/generate-illustrated-article/scripts/inject_assets.js +1691 -0
- package/engine/generate-illustrated-article/scripts/jit_mockup_css.js +191 -0
- package/engine/generate-illustrated-article/scripts/label_evidence.js +87 -0
- package/engine/generate-illustrated-article/scripts/lint_article_copy.js +252 -0
- package/engine/generate-illustrated-article/scripts/lint_mockup_fidelity.js +2403 -0
- package/engine/generate-illustrated-article/scripts/polish_tickets.js +1148 -0
- package/engine/generate-illustrated-article/scripts/related_repos.sh +52 -0
- package/engine/generate-illustrated-article/scripts/render_all.js +177 -0
- package/engine/generate-illustrated-article/scripts/render_mockup.js +665 -0
- package/engine/generate-illustrated-article/scripts/render_ready.js +164 -0
- package/engine/generate-illustrated-article/scripts/resolve_workspace.sh +86 -0
- package/engine/generate-illustrated-article/scripts/runtime_region_geometry.js +55 -0
- package/engine/generate-illustrated-article/scripts/source_paths.js +49 -0
- package/engine/generate-illustrated-article/scripts/test_control_visibility.js +30 -0
- package/engine/generate-illustrated-article/scripts/test_emit_walkthrough_signals.js +157 -0
- package/engine/generate-illustrated-article/scripts/test_generate_content_images.js +168 -0
- package/engine/generate-illustrated-article/scripts/test_include_census.js +126 -0
- package/engine/generate-illustrated-article/scripts/test_label_evidence.js +49 -0
- package/engine/generate-illustrated-article/scripts/test_lint_article_copy.js +138 -0
- package/engine/generate-illustrated-article/scripts/test_lint_mockup_fidelity.js +653 -0
- package/engine/generate-illustrated-article/scripts/test_node_ports.js +152 -0
- package/engine/generate-illustrated-article/scripts/test_polish_tickets.js +481 -0
- package/engine/generate-illustrated-article/scripts/test_related_repos.js +64 -0
- package/engine/generate-illustrated-article/scripts/test_render_ready.js +85 -0
- package/engine/generate-illustrated-article/scripts/test_source_paths.js +68 -0
- package/engine/generate-illustrated-article/scripts/trace_hook.js +79 -0
- package/engine/generate-illustrated-article/scripts/trace_hook.sh +4 -0
- package/engine/generate-illustrated-article/scripts/validate_html.js +114 -0
- package/engine/generate-illustrated-article/scripts/watermark.js +69 -0
- package/install.sh +14 -0
- package/package.json +57 -4
- package/scripts/auth.mjs +33 -0
- package/scripts/auto-update.mjs +7 -0
- package/scripts/build-plugin.mjs +58 -0
- package/scripts/build-release.mjs +94 -0
- package/scripts/check-release.mjs +40 -0
- package/scripts/check-runtime.mjs +10 -0
- package/scripts/cli.mjs +8 -0
- package/scripts/install.mjs +64 -0
- package/scripts/lib/agent-runner.mjs +181 -0
- package/scripts/lib/agent-settings.mjs +215 -0
- package/scripts/lib/article-skills.mjs +82 -0
- package/scripts/lib/auto-update.mjs +50 -0
- package/scripts/lib/brand.mjs +9 -0
- package/scripts/lib/browser.mjs +12 -0
- package/scripts/lib/claude-connection.mjs +33 -0
- package/scripts/lib/claude-permissions.mjs +47 -0
- package/scripts/lib/claude-plugin.mjs +15 -0
- package/scripts/lib/claude-writer.mjs +10 -0
- package/scripts/lib/cli-main.mjs +85 -0
- package/scripts/lib/cli.mjs +888 -0
- package/scripts/lib/codex-config.mjs +51 -0
- package/scripts/lib/codex-integration.mjs +61 -0
- package/scripts/lib/codex-skill.mjs +34 -0
- package/scripts/lib/harness-models.mjs +109 -0
- package/scripts/lib/install.mjs +269 -0
- package/scripts/lib/managed-writer.mjs +38 -0
- package/scripts/lib/planning.mjs +263 -0
- package/scripts/lib/prepare-update.mjs +85 -0
- package/scripts/lib/refresh-writers.mjs +12 -0
- package/scripts/lib/remove.mjs +167 -0
- package/scripts/lib/renderer.mjs +34 -0
- package/scripts/lib/terminal.mjs +252 -0
- package/scripts/lib/uninit.mjs +72 -0
- package/scripts/lib/update.mjs +55 -0
- package/scripts/lib/writer-recovery.mjs +35 -0
- package/scripts/lib/yolo.mjs +362 -0
- package/scripts/plugin-session.mjs +65 -0
- package/scripts/prepare-update.mjs +12 -0
- package/scripts/publish-release.mjs +92 -0
- package/scripts/skills/supportpages/SKILL.md +120 -0
- package/scripts/smoke-release.mjs +101 -0
- package/server.json +27 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* extract_images.js — Walk a directory, find brand/UI images, output as base64 data URIs.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node extract_images.js <repo_dir> <output_json> [--cache]
|
|
9
|
+
*
|
|
10
|
+
* Budget: 1MB per file, 10MB of base64 in total. Priority names (logo, brand,
|
|
11
|
+
* favicon, …) are encoded first, then smaller files. No external deps.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
const MAX_FILE_SIZE = 1024 * 1024;
|
|
19
|
+
const MAX_TOTAL_SIZE = 10 * 1024 * 1024;
|
|
20
|
+
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico']);
|
|
21
|
+
const SKIP_DIRS = new Set([
|
|
22
|
+
'node_modules', '.git', 'vendor', 'tmp', 'log', 'coverage',
|
|
23
|
+
'.bundle', 'dist', 'build', '.next', '.nuxt', '__pycache__',
|
|
24
|
+
'.cache', '.parcel-cache', 'bower_components', 'output',
|
|
25
|
+
// The seeded cache holds card_bg.png / card_logo.* which are title-card
|
|
26
|
+
// assets, not repo UI images — they must not enter the mockup image manifest.
|
|
27
|
+
// (Harmless in article runs: an article codebase has no such cache assets.)
|
|
28
|
+
'.rtfm', '.rtfm-branding',
|
|
29
|
+
]);
|
|
30
|
+
const PRIORITY_NAMES = ['logo', 'brand', 'favicon', 'icon', 'avatar', 'placeholder', 'default', 'hero'];
|
|
31
|
+
|
|
32
|
+
const MIME_MAP = {
|
|
33
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
34
|
+
'.gif': 'image/gif', '.svg': 'image/svg+xml', '.webp': 'image/webp',
|
|
35
|
+
'.ico': 'image/x-icon',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Split an entry into files and directories the way a top-down walk sees them. */
|
|
39
|
+
function listDir(dir) {
|
|
40
|
+
let entries;
|
|
41
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
42
|
+
catch { return { dirs: [], files: [] }; }
|
|
43
|
+
const dirs = [];
|
|
44
|
+
const files = [];
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
let isDir = entry.isDirectory();
|
|
47
|
+
if (entry.isSymbolicLink()) {
|
|
48
|
+
// A symlink to a directory is listed as a directory but never followed.
|
|
49
|
+
try { isDir = fs.statSync(path.join(dir, entry.name)).isDirectory(); }
|
|
50
|
+
catch { isDir = false; }
|
|
51
|
+
if (isDir) continue;
|
|
52
|
+
}
|
|
53
|
+
(isDir ? dirs : files).push(entry.name);
|
|
54
|
+
}
|
|
55
|
+
return { dirs, files };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function walk(repoDir) {
|
|
59
|
+
const found = [];
|
|
60
|
+
const visit = dir => {
|
|
61
|
+
const { dirs, files } = listDir(dir);
|
|
62
|
+
for (const fname of files) {
|
|
63
|
+
const ext = path.extname(fname).toLowerCase();
|
|
64
|
+
if (!IMAGE_EXTENSIONS.has(ext)) continue;
|
|
65
|
+
const fpath = path.join(dir, fname);
|
|
66
|
+
let stat;
|
|
67
|
+
try { stat = fs.statSync(fpath, { bigint: true }); }
|
|
68
|
+
catch { continue; }
|
|
69
|
+
const size = Number(stat.size);
|
|
70
|
+
if (size > MAX_FILE_SIZE || size === 0) continue;
|
|
71
|
+
const mtime = Number(stat.mtimeNs / 1000000000n);
|
|
72
|
+
const relPath = path.relative(repoDir, fpath);
|
|
73
|
+
const nameNoExt = path.basename(fname, path.extname(fname));
|
|
74
|
+
// Skip fingerprinted/hashed copies (e.g. logo-abc123def456.png)
|
|
75
|
+
if ([...nameNoExt].length > 40 && nameNoExt.includes('-')) {
|
|
76
|
+
const tail = nameNoExt.slice(nameNoExt.lastIndexOf('-') + 1);
|
|
77
|
+
if ([...tail].length > 20) continue;
|
|
78
|
+
}
|
|
79
|
+
const nameLower = nameNoExt.toLowerCase();
|
|
80
|
+
found.push({
|
|
81
|
+
relPath,
|
|
82
|
+
absPath: fpath,
|
|
83
|
+
size,
|
|
84
|
+
mtime,
|
|
85
|
+
ext,
|
|
86
|
+
priority: PRIORITY_NAMES.some(name => nameLower.includes(name)),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
for (const name of dirs) {
|
|
90
|
+
if (!SKIP_DIRS.has(name)) visit(path.join(dir, name));
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
visit(repoDir);
|
|
94
|
+
found.sort((a, b) => (Number(!a.priority) - Number(!b.priority)) || (a.size - b.size));
|
|
95
|
+
return found;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Content signature of the candidate image set: changes iff an image is
|
|
100
|
+
* added, removed, resized, or modified. Cheap (stat-only) — lets a cached
|
|
101
|
+
* manifest be reused across runs without re-reading/encoding the bytes.
|
|
102
|
+
*/
|
|
103
|
+
function signature(found) {
|
|
104
|
+
const hash = crypto.createHash('sha256');
|
|
105
|
+
for (const item of [...found].sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0))) {
|
|
106
|
+
hash.update(`${item.relPath}:${item.size}:${item.mtime}\n`);
|
|
107
|
+
}
|
|
108
|
+
return hash.digest('hex');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildManifest(found) {
|
|
112
|
+
const images = {};
|
|
113
|
+
const unique = new Set();
|
|
114
|
+
let totalB64Size = 0;
|
|
115
|
+
|
|
116
|
+
for (const item of found) {
|
|
117
|
+
let data;
|
|
118
|
+
try { data = fs.readFileSync(item.absPath); }
|
|
119
|
+
catch { continue; }
|
|
120
|
+
const b64 = data.toString('base64');
|
|
121
|
+
if (totalB64Size + b64.length > MAX_TOTAL_SIZE) continue;
|
|
122
|
+
const mime = MIME_MAP[item.ext] ?? 'application/octet-stream';
|
|
123
|
+
const dataUri = `data:${mime};base64,${b64}`;
|
|
124
|
+
|
|
125
|
+
if (unique.has(dataUri)) {
|
|
126
|
+
images[item.relPath] = dataUri;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
unique.add(dataUri);
|
|
130
|
+
|
|
131
|
+
const rel = item.relPath;
|
|
132
|
+
const fname = path.basename(rel);
|
|
133
|
+
images[rel] = dataUri;
|
|
134
|
+
images['/' + rel] = dataUri;
|
|
135
|
+
images[fname] = dataUri;
|
|
136
|
+
images['/' + fname] = dataUri;
|
|
137
|
+
for (const prefix of ['/assets/', '/images/', '/img/', '/static/']) images[prefix + fname] = dataUri;
|
|
138
|
+
|
|
139
|
+
totalB64Size += b64.length;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { images, count: unique.size, totalB64Size };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function main(argv) {
|
|
146
|
+
const useCache = argv.includes('--cache');
|
|
147
|
+
const args = argv.filter(arg => arg !== '--cache');
|
|
148
|
+
if (args.length !== 2) {
|
|
149
|
+
process.stderr.write('Usage: extract_images.js <repo_dir> <output_json> [--cache]\n');
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
const [repoDir, outputFile] = args;
|
|
153
|
+
if (!fs.existsSync(repoDir) || !fs.statSync(repoDir).isDirectory()) {
|
|
154
|
+
process.stderr.write(`Error: ${repoDir} is not a directory\n`);
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
fs.mkdirSync(path.dirname(outputFile) || '.', { recursive: true });
|
|
158
|
+
|
|
159
|
+
const found = walk(repoDir);
|
|
160
|
+
const sig = signature(found);
|
|
161
|
+
|
|
162
|
+
// Cache hit: existing manifest covers the same image set -> reuse, skip the
|
|
163
|
+
// read+base64 work. The stat-only walk above is the only cost on a hit.
|
|
164
|
+
if (useCache && fs.existsSync(outputFile)) {
|
|
165
|
+
try {
|
|
166
|
+
const prev = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
|
|
167
|
+
if (prev.signature === sig && prev.images && Object.keys(prev.images).length) {
|
|
168
|
+
process.stdout.write(`cache hit (${prev.count ?? '?'} images) -> ${outputFile}\n`);
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
} catch { /* rebuild */ }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const { images, count, totalB64Size } = buildManifest(found);
|
|
175
|
+
fs.writeFileSync(outputFile, JSON.stringify({
|
|
176
|
+
count,
|
|
177
|
+
total_b64_bytes: totalB64Size,
|
|
178
|
+
signature: sig,
|
|
179
|
+
images,
|
|
180
|
+
}));
|
|
181
|
+
process.stdout.write(`${count} unique image(s), ${Math.floor(totalB64Size / 1024)}KB base64 -> ${outputFile}\n`);
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (require.main === module) process.exitCode = main(process.argv.slice(2));
|
|
186
|
+
|
|
187
|
+
module.exports = { walk, signature, buildManifest };
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate raster assets for article/walkthrough mockups.
|
|
6
|
+
*
|
|
7
|
+
* Ordinary assets are content-only leaves inside source-grounded product UI.
|
|
8
|
+
* The separately-authorised `external-surface` kind may depict a complete
|
|
9
|
+
* non-product surface (for example an email or OS permission prompt) when its
|
|
10
|
+
* exact visible copy is supplied in the request manifest.
|
|
11
|
+
*
|
|
12
|
+
* Input: <out>/generated_image_requests.json
|
|
13
|
+
* Output: <out>/generated_images.json + successful files in
|
|
14
|
+
* <out>/generated-assets/*
|
|
15
|
+
* Cache: .rtfm/generated-content/<sha256>.{png,json}
|
|
16
|
+
*
|
|
17
|
+
* Missing credentials and per-image API failures are recorded as failures and
|
|
18
|
+
* produce no image file. Invalid request manifests are hard errors.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const crypto = require('crypto');
|
|
24
|
+
|
|
25
|
+
const MAX_ASSETS = 5;
|
|
26
|
+
const DEFAULT_MODEL = 'gpt-image-2';
|
|
27
|
+
const GENERATOR_VERSION = 1;
|
|
28
|
+
const SIZE_BY_ASPECT = {
|
|
29
|
+
landscape: '1536x1024',
|
|
30
|
+
portrait: '1024x1536',
|
|
31
|
+
square: '1024x1024',
|
|
32
|
+
};
|
|
33
|
+
const ALLOWED_KINDS = new Set([
|
|
34
|
+
'photo', 'portrait', 'video-frame', 'artwork', 'media-preview', 'external-surface',
|
|
35
|
+
]);
|
|
36
|
+
const ALLOWED_EXTERNAL_SURFACES = new Set([
|
|
37
|
+
'confirmation-email',
|
|
38
|
+
'invitation-email',
|
|
39
|
+
'os-permission-dialog',
|
|
40
|
+
'browser-permission-dialog',
|
|
41
|
+
'push-notification',
|
|
42
|
+
'document',
|
|
43
|
+
'receipt',
|
|
44
|
+
'third-party-consent',
|
|
45
|
+
]);
|
|
46
|
+
const ALLOWED_SOURCE_BASES = new Set(['repository', 'user-description', 'platform-convention']);
|
|
47
|
+
const CONTENT_GUARDRAIL = [
|
|
48
|
+
'Generate only the content image that will appear inside an existing product interface.',
|
|
49
|
+
'Do not draw application UI, controls, buttons, menus, dialogs, browser or device frames,',
|
|
50
|
+
'screenshots, logos, watermarks, icons, charts, diagrams, file-type symbols, or legible text.',
|
|
51
|
+
'Do not add a border or mockup frame. Fill the image edge to edge with the requested content.',
|
|
52
|
+
].join(' ');
|
|
53
|
+
const EXTERNAL_SURFACE_GUARDRAIL = [
|
|
54
|
+
'Generate one straight-on, accurate representation of the named external surface encountered outside the product application.',
|
|
55
|
+
'This request is authorised to contain interface elements and legible text only for that external surface.',
|
|
56
|
+
'Do not draw, extend, or imitate the product application UI or product chrome.',
|
|
57
|
+
'Do not place the surface in a device mockup, marketing scene, hand-held view, or perspective composition.',
|
|
58
|
+
'Render every required string exactly as supplied, preserving spelling, punctuation, capitalisation, and word order.',
|
|
59
|
+
'Do not paraphrase required text and do not invent additional names, legal copy, buttons, links, codes, dates, or customer data.',
|
|
60
|
+
].join(' ');
|
|
61
|
+
|
|
62
|
+
function parseArgs(argv) {
|
|
63
|
+
const args = {};
|
|
64
|
+
for (let i = 0; i < argv.length; i++) {
|
|
65
|
+
const arg = argv[i];
|
|
66
|
+
if (!arg.startsWith('--')) continue;
|
|
67
|
+
const key = arg.slice(2);
|
|
68
|
+
const value = argv[i + 1];
|
|
69
|
+
if (!value || value.startsWith('--')) {
|
|
70
|
+
args[key] = true;
|
|
71
|
+
} else {
|
|
72
|
+
args[key] = value;
|
|
73
|
+
i += 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return args;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readJson(file) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw new Error(`cannot read ${file}: ${error.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function validateRequests(input) {
|
|
88
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
89
|
+
throw new Error('request manifest must be a JSON object');
|
|
90
|
+
}
|
|
91
|
+
if (input.version !== 1) throw new Error('request manifest version must be 1');
|
|
92
|
+
if (!Array.isArray(input.assets)) throw new Error('request manifest assets must be an array');
|
|
93
|
+
if (input.assets.length > MAX_ASSETS) {
|
|
94
|
+
throw new Error(`request manifest has ${input.assets.length} assets; maximum is ${MAX_ASSETS}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const ids = new Set();
|
|
98
|
+
return input.assets.map((asset, index) => {
|
|
99
|
+
const label = `assets[${index}]`;
|
|
100
|
+
if (!asset || typeof asset !== 'object' || Array.isArray(asset)) {
|
|
101
|
+
throw new Error(`${label} must be an object`);
|
|
102
|
+
}
|
|
103
|
+
if (typeof asset.id !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(asset.id)) {
|
|
104
|
+
throw new Error(`${label}.id must be a lowercase kebab-case identifier`);
|
|
105
|
+
}
|
|
106
|
+
if (ids.has(asset.id)) throw new Error(`${label}.id duplicates '${asset.id}'`);
|
|
107
|
+
ids.add(asset.id);
|
|
108
|
+
if (!ALLOWED_KINDS.has(asset.kind)) {
|
|
109
|
+
throw new Error(`${label}.kind must be one of ${[...ALLOWED_KINDS].join(', ')}`);
|
|
110
|
+
}
|
|
111
|
+
for (const field of ['purpose', 'reason', 'prompt', 'alt']) {
|
|
112
|
+
if (typeof asset[field] !== 'string' || !asset[field].trim()) {
|
|
113
|
+
throw new Error(`${label}.${field} must be a non-empty string`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!Object.hasOwn(SIZE_BY_ASPECT, asset.aspect_ratio)) {
|
|
117
|
+
throw new Error(`${label}.aspect_ratio must be landscape, portrait, or square`);
|
|
118
|
+
}
|
|
119
|
+
const usesBlocks = Array.isArray(asset.used_in_blocks);
|
|
120
|
+
if (usesBlocks) {
|
|
121
|
+
if (!asset.used_in_blocks.length || asset.used_in_blocks.some(id => typeof id !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id))) {
|
|
122
|
+
throw new Error(`${label}.used_in_blocks must contain block ids`);
|
|
123
|
+
}
|
|
124
|
+
} else if (!Array.isArray(asset.used_in_steps) || asset.used_in_steps.length === 0 ||
|
|
125
|
+
asset.used_in_steps.some(step => !Number.isInteger(step) || step < 0)) {
|
|
126
|
+
throw new Error(`${label} requires used_in_blocks (article v2) or non-negative used_in_steps (legacy/walkthrough)`);
|
|
127
|
+
}
|
|
128
|
+
const validated = {
|
|
129
|
+
id: asset.id,
|
|
130
|
+
kind: asset.kind,
|
|
131
|
+
purpose: asset.purpose.trim(),
|
|
132
|
+
reason: asset.reason.trim(),
|
|
133
|
+
prompt: asset.prompt.trim(),
|
|
134
|
+
aspect_ratio: asset.aspect_ratio,
|
|
135
|
+
alt: asset.alt.trim(),
|
|
136
|
+
...(usesBlocks
|
|
137
|
+
? { used_in_blocks: [...new Set(asset.used_in_blocks)].sort() }
|
|
138
|
+
: { used_in_steps: [...new Set(asset.used_in_steps)].sort((a, b) => a - b) }),
|
|
139
|
+
};
|
|
140
|
+
if (asset.kind === 'external-surface') {
|
|
141
|
+
if (!ALLOWED_EXTERNAL_SURFACES.has(asset.surface)) {
|
|
142
|
+
throw new Error(`${label}.surface must be one of ${[...ALLOWED_EXTERNAL_SURFACES].join(', ')}`);
|
|
143
|
+
}
|
|
144
|
+
if (!ALLOWED_SOURCE_BASES.has(asset.source_basis)) {
|
|
145
|
+
throw new Error(`${label}.source_basis must be one of ${[...ALLOWED_SOURCE_BASES].join(', ')}`);
|
|
146
|
+
}
|
|
147
|
+
if (!Array.isArray(asset.required_text) || asset.required_text.length === 0 ||
|
|
148
|
+
asset.required_text.length > 20 || asset.required_text.some(text =>
|
|
149
|
+
typeof text !== 'string' || !text.trim() || text.length > 500)) {
|
|
150
|
+
throw new Error(`${label}.required_text must contain 1-20 non-empty strings of at most 500 characters`);
|
|
151
|
+
}
|
|
152
|
+
const requiredText = asset.required_text.map(text => text.trim());
|
|
153
|
+
if (new Set(requiredText).size !== requiredText.length) {
|
|
154
|
+
throw new Error(`${label}.required_text must not contain duplicates`);
|
|
155
|
+
}
|
|
156
|
+
if (!Array.isArray(asset.source_files) || asset.source_files.some(file =>
|
|
157
|
+
typeof file !== 'string' || !file.trim() || path.isAbsolute(file) || file.split(/[\\/]/).includes('..'))) {
|
|
158
|
+
throw new Error(`${label}.source_files must be an array of safe project-relative paths`);
|
|
159
|
+
}
|
|
160
|
+
if (asset.source_basis === 'repository' && asset.source_files.length === 0) {
|
|
161
|
+
throw new Error(`${label}.source_files must not be empty when source_basis is repository`);
|
|
162
|
+
}
|
|
163
|
+
validated.surface = asset.surface;
|
|
164
|
+
validated.source_basis = asset.source_basis;
|
|
165
|
+
validated.source_files = [...new Set(asset.source_files.map(file => file.trim()))];
|
|
166
|
+
validated.required_text = requiredText;
|
|
167
|
+
} else if (asset.surface !== undefined || asset.source_basis !== undefined ||
|
|
168
|
+
asset.source_files !== undefined || asset.required_text !== undefined) {
|
|
169
|
+
throw new Error(`${label} external-surface fields are only valid when kind is external-surface`);
|
|
170
|
+
}
|
|
171
|
+
return validated;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildPrompt(asset) {
|
|
176
|
+
if (asset.kind === 'external-surface') {
|
|
177
|
+
const required = asset.required_text.map((text, index) => `${index + 1}. ${JSON.stringify(text)}`).join('\n');
|
|
178
|
+
return [
|
|
179
|
+
asset.prompt,
|
|
180
|
+
'',
|
|
181
|
+
`External surface type: ${asset.surface}.`,
|
|
182
|
+
`Source basis: ${asset.source_basis}.`,
|
|
183
|
+
'Required visible text (render verbatim):',
|
|
184
|
+
required,
|
|
185
|
+
'',
|
|
186
|
+
EXTERNAL_SURFACE_GUARDRAIL,
|
|
187
|
+
].join('\n');
|
|
188
|
+
}
|
|
189
|
+
const fictionalPeople = ['portrait', 'video-frame'].includes(asset.kind)
|
|
190
|
+
? ' Any depicted person must be fictional and must not depict or imitate an identifiable real person.'
|
|
191
|
+
: '';
|
|
192
|
+
return `${asset.prompt}\n\n${CONTENT_GUARDRAIL}${fictionalPeople}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function cacheKey({ prompt, model, size }) {
|
|
196
|
+
return crypto.createHash('sha256')
|
|
197
|
+
.update(JSON.stringify({ version: GENERATOR_VERSION, prompt, model, size }))
|
|
198
|
+
.digest('hex');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function decodeImagePayload(json) {
|
|
202
|
+
if (!json || !Array.isArray(json.data) || json.data.length === 0) {
|
|
203
|
+
throw new Error('OpenAI response is missing data[]');
|
|
204
|
+
}
|
|
205
|
+
const item = json.data[0];
|
|
206
|
+
if (item && item.b64_json) return { bytes: Buffer.from(item.b64_json, 'base64'), url: null };
|
|
207
|
+
if (item && item.url) return { bytes: null, url: item.url };
|
|
208
|
+
throw new Error('OpenAI response is missing b64_json/url');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function callOpenAI({ apiKey, model, prompt, size, fetchImpl = globalThis.fetch, apiBase }) {
|
|
212
|
+
if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable; Node.js 18+ is required');
|
|
213
|
+
const base = (apiBase || process.env.OPENAI_API_BASE_URL || 'https://api.openai.com/v1').replace(/\/$/, '');
|
|
214
|
+
const response = await fetchImpl(`${base}/images/generations`, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: {
|
|
217
|
+
Authorization: `Bearer ${apiKey}`,
|
|
218
|
+
'Content-Type': 'application/json',
|
|
219
|
+
},
|
|
220
|
+
body: JSON.stringify({ model, prompt, size, n: 1 }),
|
|
221
|
+
});
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
const body = await response.text().catch(() => '<no response body>');
|
|
224
|
+
throw new Error(`OpenAI images API returned ${response.status}: ${body.slice(0, 400)}`);
|
|
225
|
+
}
|
|
226
|
+
const decoded = decodeImagePayload(await response.json());
|
|
227
|
+
if (decoded.bytes) return decoded.bytes;
|
|
228
|
+
const imageResponse = await fetchImpl(decoded.url);
|
|
229
|
+
if (!imageResponse.ok) throw new Error(`generated image download returned ${imageResponse.status}`);
|
|
230
|
+
return Buffer.from(await imageResponse.arrayBuffer());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function generateAll({ requests, outDir, cacheDir, model = DEFAULT_MODEL, apiKey,
|
|
234
|
+
fetchImpl = globalThis.fetch, apiBase }) {
|
|
235
|
+
const assets = validateRequests(requests);
|
|
236
|
+
const generatedDir = path.join(outDir, 'generated-assets');
|
|
237
|
+
fs.rmSync(generatedDir, { recursive: true, force: true });
|
|
238
|
+
fs.mkdirSync(generatedDir, { recursive: true });
|
|
239
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
240
|
+
const results = {};
|
|
241
|
+
const failures = {};
|
|
242
|
+
|
|
243
|
+
for (const asset of assets) {
|
|
244
|
+
const prompt = buildPrompt(asset);
|
|
245
|
+
const size = SIZE_BY_ASPECT[asset.aspect_ratio];
|
|
246
|
+
const hash = cacheKey({ prompt, model, size });
|
|
247
|
+
const cachePng = path.join(cacheDir, `${hash}.png`);
|
|
248
|
+
const outputPng = path.join(generatedDir, `${asset.id}.png`);
|
|
249
|
+
let result;
|
|
250
|
+
let failure;
|
|
251
|
+
|
|
252
|
+
if (fs.existsSync(cachePng) && fs.statSync(cachePng).size > 0) {
|
|
253
|
+
fs.copyFileSync(cachePng, outputPng);
|
|
254
|
+
result = { status: 'cached', filename: `${asset.id}.png`, mime: 'image/png' };
|
|
255
|
+
} else if (!apiKey) {
|
|
256
|
+
failure = {
|
|
257
|
+
status: 'failed',
|
|
258
|
+
warning: 'OPENAI_API_KEY is not set; no image was produced',
|
|
259
|
+
};
|
|
260
|
+
} else {
|
|
261
|
+
try {
|
|
262
|
+
const bytes = await callOpenAI({ apiKey, model, prompt, size, fetchImpl, apiBase });
|
|
263
|
+
if (!bytes.length) throw new Error('OpenAI returned an empty image');
|
|
264
|
+
fs.writeFileSync(cachePng, bytes);
|
|
265
|
+
fs.copyFileSync(cachePng, outputPng);
|
|
266
|
+
result = { status: 'generated', filename: `${asset.id}.png`, mime: 'image/png' };
|
|
267
|
+
fs.writeFileSync(path.join(cacheDir, `${hash}.json`), JSON.stringify({
|
|
268
|
+
version: GENERATOR_VERSION, hash, model, size, generated_at: new Date().toISOString(),
|
|
269
|
+
}, null, 2));
|
|
270
|
+
} catch (error) {
|
|
271
|
+
failure = {
|
|
272
|
+
status: 'failed',
|
|
273
|
+
warning: `image generation failed; no image was produced: ${error.message}`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const ledger = {
|
|
279
|
+
kind: asset.kind,
|
|
280
|
+
purpose: asset.purpose,
|
|
281
|
+
reason: asset.reason,
|
|
282
|
+
alt: asset.alt,
|
|
283
|
+
aspect_ratio: asset.aspect_ratio,
|
|
284
|
+
...(asset.used_in_blocks ? { used_in_blocks: asset.used_in_blocks } : { used_in_steps: asset.used_in_steps }),
|
|
285
|
+
model,
|
|
286
|
+
size,
|
|
287
|
+
prompt_hash: hash,
|
|
288
|
+
};
|
|
289
|
+
if (asset.kind === 'external-surface') {
|
|
290
|
+
ledger.surface = asset.surface;
|
|
291
|
+
ledger.source_basis = asset.source_basis;
|
|
292
|
+
ledger.source_files = asset.source_files;
|
|
293
|
+
ledger.required_text = asset.required_text;
|
|
294
|
+
ledger.text_fidelity = {
|
|
295
|
+
enforcement: 'prompt-constrained',
|
|
296
|
+
required_text_count: asset.required_text.length,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
if (failure) {
|
|
300
|
+
failures[asset.id] = { ...failure, ...ledger };
|
|
301
|
+
console.error(` ${asset.id}: failed (${failure.warning})`);
|
|
302
|
+
} else {
|
|
303
|
+
results[asset.id] = {
|
|
304
|
+
...result,
|
|
305
|
+
path: `generated-assets/${result.filename}`,
|
|
306
|
+
...ledger,
|
|
307
|
+
};
|
|
308
|
+
console.error(` ${asset.id}: ${result.status}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (Object.keys(results).length === 0) {
|
|
313
|
+
fs.rmSync(generatedDir, { recursive: true, force: true });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const manifest = {
|
|
317
|
+
version: 1,
|
|
318
|
+
max_assets: MAX_ASSETS,
|
|
319
|
+
requested_count: assets.length,
|
|
320
|
+
count: Object.keys(results).length,
|
|
321
|
+
failure_count: Object.keys(failures).length,
|
|
322
|
+
model,
|
|
323
|
+
assets: results,
|
|
324
|
+
failures,
|
|
325
|
+
};
|
|
326
|
+
fs.writeFileSync(path.join(outDir, 'generated_images.json'), JSON.stringify(manifest, null, 2));
|
|
327
|
+
return manifest;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function main(argv = process.argv.slice(2)) {
|
|
331
|
+
const args = parseArgs(argv);
|
|
332
|
+
if (!args.requests || !args.out) {
|
|
333
|
+
console.error('Usage: generate_content_images.js --requests <json> --out <dir> [--cache <dir>] [--model <model>]');
|
|
334
|
+
return 2;
|
|
335
|
+
}
|
|
336
|
+
let requests;
|
|
337
|
+
try {
|
|
338
|
+
requests = readJson(args.requests);
|
|
339
|
+
const model = args.model || process.env.OPENAI_IMAGE_MODEL || DEFAULT_MODEL;
|
|
340
|
+
const cacheDir = args.cache || './.rtfm/generated-content';
|
|
341
|
+
const manifest = await generateAll({
|
|
342
|
+
requests,
|
|
343
|
+
outDir: args.out,
|
|
344
|
+
cacheDir,
|
|
345
|
+
model,
|
|
346
|
+
apiKey: process.env.OPENAI_API_KEY || '',
|
|
347
|
+
});
|
|
348
|
+
console.error(
|
|
349
|
+
`${manifest.requested_count} generated image request(s) processed; ` +
|
|
350
|
+
`${manifest.count} image(s) available; ${manifest.failure_count} failure(s) omitted`
|
|
351
|
+
);
|
|
352
|
+
return 0;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error(`ERROR: ${error.message}`);
|
|
355
|
+
return 1;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (require.main === module) {
|
|
360
|
+
main().then(code => { process.exitCode = code; });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
module.exports = {
|
|
364
|
+
ALLOWED_KINDS,
|
|
365
|
+
ALLOWED_EXTERNAL_SURFACES,
|
|
366
|
+
ALLOWED_SOURCE_BASES,
|
|
367
|
+
MAX_ASSETS,
|
|
368
|
+
SIZE_BY_ASPECT,
|
|
369
|
+
buildPrompt,
|
|
370
|
+
cacheKey,
|
|
371
|
+
callOpenAI,
|
|
372
|
+
decodeImagePayload,
|
|
373
|
+
generateAll,
|
|
374
|
+
main,
|
|
375
|
+
validateRequests,
|
|
376
|
+
};
|