Files

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));
}