214 lines
6.2 KiB
JavaScript
214 lines
6.2 KiB
JavaScript
import path from "node:path";
|
|
import process from "node:process";
|
|
import { homedir } from "node:os";
|
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
|
export const DEFAULT_IMAGE_DIR = "garden-gpt-image-2/image";
|
|
export const DEFAULT_PROMPT_DIR = "garden-gpt-image-2/prompt";
|
|
export const DEFAULT_MODEL = "gpt-image-2";
|
|
|
|
export async function readEnvFile(filePath) {
|
|
try {
|
|
const text = await readFile(filePath, "utf8");
|
|
const result = {};
|
|
for (const line of text.split("\n")) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
const pivot = trimmed.indexOf("=");
|
|
if (pivot === -1) continue;
|
|
const key = trimmed.slice(0, pivot).trim();
|
|
let value = trimmed.slice(pivot + 1).trim();
|
|
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
result[key] = value;
|
|
}
|
|
return result;
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
export async function loadAmbientEnv() {
|
|
const places = [
|
|
path.join(process.cwd(), ".env"),
|
|
path.join(process.cwd(), ".gateway.env"),
|
|
path.join(homedir(), ".gateway.env"),
|
|
];
|
|
|
|
for (const filePath of places) {
|
|
const pairs = await readEnvFile(filePath);
|
|
for (const [key, value] of Object.entries(pairs)) {
|
|
if (!process.env[key]) process.env[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function readPromptInput(prompt, promptFile) {
|
|
if (prompt) return prompt.trim();
|
|
if (promptFile) {
|
|
const text = await readFile(path.resolve(promptFile), "utf8");
|
|
return text.trim();
|
|
}
|
|
throw new Error("Prompt is required. Use --prompt or --promptfile.");
|
|
}
|
|
|
|
export function slugify(value, fallback = "image-task") {
|
|
const base = String(value || "").trim().toLowerCase();
|
|
const ascii = base
|
|
.normalize("NFKD")
|
|
.replace(/[\u0300-\u036f]/g, "")
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "")
|
|
.slice(0, 48);
|
|
return ascii || fallback;
|
|
}
|
|
|
|
export function makeTimestamp() {
|
|
const now = new Date();
|
|
const yyyy = String(now.getFullYear());
|
|
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
const dd = String(now.getDate()).padStart(2, "0");
|
|
const hh = String(now.getHours()).padStart(2, "0");
|
|
const mi = String(now.getMinutes()).padStart(2, "0");
|
|
const ss = String(now.getSeconds()).padStart(2, "0");
|
|
return `${yyyy}${mm}${dd}-${hh}${mi}${ss}`;
|
|
}
|
|
|
|
export function buildDefaultImagePath(kind, hint, ext = ".png") {
|
|
const stamp = makeTimestamp();
|
|
const slug = slugify(hint, kind === "edit" ? "edited-image" : "generated-image");
|
|
const file = `${slug}-${stamp}${ext}`;
|
|
return path.join(DEFAULT_IMAGE_DIR, file);
|
|
}
|
|
|
|
export function buildDefaultPromptPath(hint) {
|
|
const stamp = makeTimestamp();
|
|
const slug = slugify(hint, "prompt");
|
|
return path.join(DEFAULT_PROMPT_DIR, `${slug}-${stamp}.md`);
|
|
}
|
|
|
|
export function resolveOutput(raw, fallbackPath) {
|
|
const target = raw || fallbackPath;
|
|
const full = path.resolve(target);
|
|
return path.extname(full) ? full : `${full}.png`;
|
|
}
|
|
|
|
export async function savePrompt(promptText, rawPath, hint) {
|
|
const finalPath = path.resolve(rawPath || buildDefaultPromptPath(hint));
|
|
await mkdir(path.dirname(finalPath), { recursive: true });
|
|
await writeFile(finalPath, `${promptText.trim()}\n`, "utf8");
|
|
return finalPath;
|
|
}
|
|
|
|
export function mimeFor(filePath) {
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
|
if (ext === ".webp") return "image/webp";
|
|
if (ext === ".gif") return "image/gif";
|
|
return "image/png";
|
|
}
|
|
|
|
export async function ensureFilesExist(files, label) {
|
|
for (const item of files) {
|
|
try {
|
|
await readFile(path.resolve(item));
|
|
} catch {
|
|
throw new Error(`${label} not found: ${path.resolve(item)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function encodeImages(files) {
|
|
const images = [];
|
|
for (const file of files) {
|
|
const absolute = path.resolve(file);
|
|
const bytes = await readFile(absolute);
|
|
images.push({
|
|
name: path.basename(absolute),
|
|
mime_type: mimeFor(absolute),
|
|
data: Buffer.from(bytes).toString("base64"),
|
|
absolute,
|
|
});
|
|
}
|
|
return images;
|
|
}
|
|
|
|
export function buildBaseUrl() {
|
|
return (process.env.OPENAI_BASE_URL || "https://api.openai.com/v1").replace(/\/$/, "");
|
|
}
|
|
|
|
export function requireApiKey() {
|
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
if (!apiKey) throw new Error("OPENAI_API_KEY is required.");
|
|
return apiKey;
|
|
}
|
|
|
|
export async function postJson(url, payload) {
|
|
const apiKey = requireApiKey();
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
authorization: `Bearer ${apiKey}`,
|
|
"content-type": "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
throw new Error(`Image API error (${res.status}): ${text}`);
|
|
}
|
|
|
|
return res.json();
|
|
}
|
|
|
|
export async function postMultipart(url, form) {
|
|
const apiKey = requireApiKey();
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
authorization: `Bearer ${apiKey}`,
|
|
},
|
|
body: form,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
throw new Error(`Image API error (${res.status}): ${text}`);
|
|
}
|
|
|
|
return res.json();
|
|
}
|
|
|
|
export async function fetchBytesFromUrl(url) {
|
|
const res = await fetch(url);
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
throw new Error(`Failed to download generated image (${res.status}): ${text}`);
|
|
}
|
|
return Buffer.from(await res.arrayBuffer());
|
|
}
|
|
|
|
export async function extractGeneratedBytes(json) {
|
|
const first = json?.data?.[0];
|
|
if (!first) throw new Error("API response did not include data[0].");
|
|
if (first.b64_json) return Buffer.from(first.b64_json, "base64");
|
|
if (first.url) return fetchBytesFromUrl(first.url);
|
|
throw new Error("API response did not include b64_json or url.");
|
|
}
|
|
|
|
export async function saveImage(outputPath, bytes) {
|
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
await writeFile(outputPath, bytes);
|
|
}
|
|
|
|
export function printJson(data) {
|
|
console.log(JSON.stringify(data, null, 2));
|
|
}
|
|
|
|
export function appendIfPresent(target, key, value) {
|
|
if (value === undefined || value === null || value === "") return;
|
|
target.append(key, String(value));
|
|
}
|