refactor(baoyu-image-gen): export functions for testability and add module entry guard

This commit is contained in:
Jim Liu 宝玉
2026-03-13 16:16:50 -05:00
parent a11613c11b
commit 3398509d9e
5 changed files with 54 additions and 40 deletions
@@ -13,7 +13,7 @@ function getBaseUrl(): string {
return base.replace(/\/+$/g, "");
}
function parseAspectRatio(ar: string): { width: number; height: number } | null {
export function parseAspectRatio(ar: string): { width: number; height: number } | null {
const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
if (!match) return null;
const w = parseFloat(match[1]!);
@@ -45,7 +45,7 @@ const STANDARD_SIZES_2K: [number, number][] = [
[2048, 2048],
];
function getSizeFromAspectRatio(ar: string | null, quality: CliArgs["quality"]): string {
export function getSizeFromAspectRatio(ar: string | null, quality: CliArgs["quality"]): string {
const is2k = quality === "2k";
const defaultSize = is2k ? "1536*1536" : "1024*1024";
@@ -71,7 +71,7 @@ function getSizeFromAspectRatio(ar: string | null, quality: CliArgs["quality"]):
return best;
}
function normalizeSize(size: string): string {
export function normalizeSize(size: string): string {
return size.replace("x", "*");
}
@@ -17,16 +17,16 @@ export function getDefaultModel(): string {
return process.env.GOOGLE_IMAGE_MODEL || "gemini-3-pro-image-preview";
}
function normalizeGoogleModelId(model: string): string {
export function normalizeGoogleModelId(model: string): string {
return model.startsWith("models/") ? model.slice("models/".length) : model;
}
function isGoogleMultimodal(model: string): boolean {
export function isGoogleMultimodal(model: string): boolean {
const normalized = normalizeGoogleModelId(model);
return GOOGLE_MULTIMODAL_MODELS.some((m) => normalized.includes(m));
}
function isGoogleImagen(model: string): boolean {
export function isGoogleImagen(model: string): boolean {
const normalized = normalizeGoogleModelId(model);
return GOOGLE_IMAGEN_MODELS.some((m) => normalized.includes(m));
}
@@ -35,7 +35,7 @@ function getGoogleApiKey(): string | null {
return process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY || null;
}
function getGoogleImageSize(args: CliArgs): "1K" | "2K" | "4K" {
export function getGoogleImageSize(args: CliArgs): "1K" | "2K" | "4K" {
if (args.imageSize) return args.imageSize as "1K" | "2K" | "4K";
return args.quality === "2k" ? "2K" : "1K";
}
@@ -46,7 +46,7 @@ function getGoogleBaseUrl(): string {
return base.replace(/\/+$/g, "");
}
function buildGoogleUrl(pathname: string): string {
export function buildGoogleUrl(pathname: string): string {
const base = getGoogleBaseUrl();
const cleanedPath = pathname.replace(/^\/+/g, "");
if (base.endsWith("/v1beta")) return `${base}/${cleanedPath}`;
@@ -162,7 +162,7 @@ async function postGoogleJson<T>(pathname: string, body: unknown): Promise<T> {
return postGoogleJsonViaFetch<T>(url, apiKey, body);
}
function buildPromptWithAspect(
export function buildPromptWithAspect(
prompt: string,
ar: string | null,
quality: CliArgs["quality"],
@@ -177,7 +177,7 @@ function buildPromptWithAspect(
return result;
}
function addAspectRatioToPrompt(prompt: string, ar: string | null): string {
export function addAspectRatioToPrompt(prompt: string, ar: string | null): string {
if (!ar) return prompt;
return `${prompt} Aspect ratio: ${ar}.`;
}
@@ -194,7 +194,7 @@ async function readImageAsBase64(
return { data: buf.toString("base64"), mimeType };
}
function extractInlineImageData(response: {
export function extractInlineImageData(response: {
candidates?: Array<{
content?: { parts?: Array<{ inlineData?: { data?: string } }> };
}>;
@@ -208,7 +208,7 @@ function extractInlineImageData(response: {
return null;
}
function extractPredictedImageData(response: {
export function extractPredictedImageData(response: {
predictions?: Array<any>;
generatedImages?: Array<any>;
}): string | null {
@@ -8,7 +8,7 @@ export function getDefaultModel(): string {
type OpenAIImageResponse = { data: Array<{ url?: string; b64_json?: string }> };
function parseAspectRatio(ar: string): { width: number; height: number } | null {
export function parseAspectRatio(ar: string): { width: number; height: number } | null {
const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
if (!match) return null;
const w = parseFloat(match[1]!);
@@ -23,7 +23,7 @@ type SizeMapping = {
portrait: string;
};
function getOpenAISize(
export function getOpenAISize(
model: string,
ar: string | null,
quality: CliArgs["quality"]
@@ -201,7 +201,7 @@ async function generateWithOpenAIEdits(
return extractImageFromResponse(result);
}
function getMimeType(filename: string): string {
export function getMimeType(filename: string): string {
const ext = path.extname(filename).toLowerCase();
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
if (ext === ".webp") return "image/webp";
@@ -209,7 +209,7 @@ function getMimeType(filename: string): string {
return "image/png";
}
async function extractImageFromResponse(result: OpenAIImageResponse): Promise<Uint8Array> {
export async function extractImageFromResponse(result: OpenAIImageResponse): Promise<Uint8Array> {
const img = result.data[0];
if (img?.b64_json) {
@@ -20,7 +20,7 @@ function getBaseUrl(): string {
return base.replace(/\/+$/g, "");
}
function parseModelId(model: string): { owner: string; name: string; version: string | null } {
export function parseModelId(model: string): { owner: string; name: string; version: string | null } {
const [ownerName, version] = model.split(":");
const parts = ownerName!.split("/");
if (parts.length !== 2 || !parts[0] || !parts[1]) {
@@ -31,7 +31,7 @@ function parseModelId(model: string): { owner: string; name: string; version: st
return { owner: parts[0], name: parts[1], version: version || null };
}
function buildInput(prompt: string, args: CliArgs, referenceImages: string[]): Record<string, unknown> {
export function buildInput(prompt: string, args: CliArgs, referenceImages: string[]): Record<string, unknown> {
const input: Record<string, unknown> = { prompt };
if (args.aspectRatio) {
@@ -144,7 +144,7 @@ async function pollPrediction(apiToken: string, getUrl: string): Promise<Predict
throw new Error(`Replicate prediction timed out after ${MAX_POLL_MS / 1000}s`);
}
function extractOutputUrl(prediction: PredictionResponse): string {
export function extractOutputUrl(prediction: PredictionResponse): string {
const output = prediction.output;
if (typeof output === "string") return output;