/** * image-ops — still-image post-processing (currently Magnific image upscale). * * `vclaw video image-ops --op upscale` runs the Magnific image upscaler * (`/v1/ai/image-upscaler-precision-v2`, VERIFIED LIVE — distinct from the * 502-ing VIDEO upscaler). A local image is base64-encoded inline (the image * endpoint accepts base64, no host needed); an http(s) URL is passed through. * * PURE planning core (param mapping) + an injectable runner so the executor is * unit-testable offline; `--dry-run` returns the plan without spending. */ import { readFile } from 'node:fs/promises'; import { submitMagnificImageUpscale, awaitMagnificImage, downloadMagnificFile } from './native-magnific.js'; export type ImageOp = 'upscale'; export const IMAGE_OP_IDS: ImageOp[] = ['upscale']; export const IMAGE_OPS_BACKENDS = ['magnific'] as const; export type ImageOpsBackend = (typeof IMAGE_OPS_BACKENDS)[number]; export type MagnificImageFlavor = 'sublime' | 'photo' | 'photo_denoiser'; export const MAGNIFIC_IMAGE_FLAVORS: MagnificImageFlavor[] = ['sublime', 'photo', 'photo_denoiser']; /** Magnific image-upscaler request params (verified live). */ export interface MagnificImageUpscaleParams { scale_factor: number; // 2..16 sharpen: number; // 0..100 smart_grain: number; // 0..100 ultra_detail: number; // 0..100 flavor: MagnificImageFlavor; } function clamp(v: number, lo: number, hi: number): number { return Math.min(hi, Math.max(lo, v)); } export interface ImageUpscaleOptions { scaleFactor?: number; flavor?: MagnificImageFlavor; sharpen?: number; smartGrain?: number; ultraDetail?: number; /** Logo/graphics-safe preset: no hallucinated detail/grain — for flat art & text. */ logoSafe?: boolean; } /** * Map options to Magnific image-upscaler params. PURE. `--logo-safe` is the * verified preset for flat graphics/text (sharpen 10, smart_grain 0, ultra_detail 0) * so logos don't get hallucinated texture; otherwise photo defaults (Magnific's own). */ export function magnificImageParamsFor(opts: ImageUpscaleOptions): MagnificImageUpscaleParams { const logo = opts.logoSafe ?? false; return { scale_factor: clamp(opts.scaleFactor ?? 4, 2, 16), sharpen: clamp(opts.sharpen ?? (logo ? 10 : 7), 0, 100), smart_grain: clamp(opts.smartGrain ?? (logo ? 0 : 7), 0, 100), ultra_detail: clamp(opts.ultraDetail ?? (logo ? 0 : 30), 0, 100), flavor: opts.flavor ?? (logo ? 'photo' : 'sublime'), }; } /** True for an http(s) URL (passed through as-is); otherwise the input is a local file → base64. */ export function isRemoteImage(input: string): boolean { return /^https?:\/\//i.test(input) || input.startsWith('data:'); } export interface RunImageOpsOptions extends ImageUpscaleOptions { op: ImageOp; backend: ImageOpsBackend; input: string; output: string; dryRun?: boolean; env?: NodeJS.ProcessEnv; /** Injectable: read a local file → base64 (default fs). */ readImage?: (path: string) => Promise; /** Injectable transports (default native-magnific). */ submit?: (image: string, params: MagnificImageUpscaleParams) => Promise; awaitTask?: (taskId: string) => Promise; download?: (url: string, dest: string) => Promise; } export interface RunImageOpsResult { op: ImageOp; backend: ImageOpsBackend; output: string; dryRun: boolean; params?: MagnificImageUpscaleParams; taskId?: string; outputUrl?: string; } export async function runImageOps(opts: RunImageOpsOptions): Promise { const { op, backend, input, output } = opts; if (backend !== 'magnific') { throw new Error(`image-ops: unknown backend "${backend}".`); } if (op !== 'upscale') { throw new Error(`image-ops: unsupported --op "${op}". Supported: ${IMAGE_OP_IDS.join(', ')}.`); } const params = magnificImageParamsFor(opts); if (opts.dryRun) { return { op, backend, output, dryRun: true, params }; } const readImage = opts.readImage ?? (async (p: string) => (await readFile(p)).toString('base64')); const submit = opts.submit ?? ((image, p) => submitMagnificImageUpscale(image, { ...p }, { ...(opts.env ? { env: opts.env } : {}) })); const awaitTask = opts.awaitTask ?? ((taskId: string) => awaitMagnificImage(taskId, { ...(opts.env ? { env: opts.env } : {}) })); const download = opts.download ?? ((url: string, dest: string) => downloadMagnificFile(url, dest)); const image = isRemoteImage(input) ? input : await readImage(input); const taskId = await submit(image, params); const outputUrl = await awaitTask(taskId); await download(outputUrl, output); return { op, backend, output, dryRun: false, params, taskId, outputUrl }; }