UNPKG

ai-pp3

Version:

CLI tool combining multimodal AI analysis with RawTherapee's engine to generate optimized PP3 profiles for RAW photography

336 lines 12.8 kB
#!/usr/bin/env node import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { convertDngToImageWithPP3 } from "./raw-therapee-wrap.js"; import { generatePP3FromRawImage, generateMultiPP3FromRawImage, } from "./agent.js"; import fs from "node:fs"; import path from "node:path"; import packageJson from "../package.json" with { type: "json" }; function getOutputFormat(options) { if (options.png) return "png"; if (options.tiff) return "tiff"; return "jpeg"; } async function validateInputFile(inputPath) { if (!inputPath) { throw new Error("Input path cannot be empty"); } try { await fs.promises.access(inputPath, fs.constants.R_OK); } catch (error) { if (error instanceof Error && "code" in error) { if (error.code === "ENOENT") { throw new Error(`Input file not found: ${inputPath}`); } else if (error.code === "EACCES") { throw new Error(`Permission denied reading input file: ${inputPath}`); } throw error; } } } function generateOutputPaths(inputPath, options, format) { const pp3Path = options.output ?? inputPath.replace(/\.[^.]+$/, ".pp3"); const imagePath = options.output ?? inputPath.replace(/\.[^.]+$/, `_processed.${format}`); return { pp3Path, imagePath }; } async function cleanupIntermediateFiles(allResults, bestResult) { for (const genResult of allResults) { if (genResult !== bestResult) { try { await fs.promises.unlink(genResult.pp3Path); await fs.promises.unlink(genResult.processedImagePath); } catch { // Ignore cleanup errors } } } } // Helper functions to reduce cognitive complexity async function generateMultiPP3Result(inputPath, options, format) { return await generateMultiPP3FromRawImage({ inputPath, basePP3Path: options.base, // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing providerName: options.provider || "openai", // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing visionModel: options.model || "gpt-4-vision-preview", verbose: options.verbose, keepPreview: options.keepPreview, prompt: options.prompt, preset: options.preset, sections: options.sections?.split(",").filter((s) => s.trim() !== ""), previewQuality: options.previewQuality, previewFormat: options.previewFormat, maxRetries: options.maxRetries, generations: options.generations, outputFormat: format, outputQuality: options.quality, tiffCompression: options.compression, bitDepth: Number(options.bitDepth), }); } async function copyFinalOutput(sourcePath, destinationPath, verbose) { if (verbose) { console.log(`Copying final output from ${sourcePath} to ${destinationPath}`); } try { // Ensure the destination directory exists const destinationDirectory = path.dirname(destinationPath); await fs.promises.mkdir(destinationDirectory, { recursive: true }); // Copy the file await fs.promises.copyFile(sourcePath, destinationPath); } catch (error) { if (verbose) { console.error(`Failed to copy file: ${error instanceof Error ? error.message : String(error)}`); } throw error; } } async function processMultiGeneration(inputPath, options) { if (options.verbose) { // Show the correct number of generations based on whether we're using multiple models const generationCount = Array.isArray(options.model) ? options.model.length : options.generations; console.log(`Multi-generation mode: generating ${String(generationCount)} different PP3 profiles`); } const format = getOutputFormat(options); const result = await generateMultiPP3Result(inputPath, options, format); const { pp3Path, imagePath } = generateOutputPaths(inputPath, options, format); await fs.promises.writeFile(pp3Path, result.bestResult.pp3Content); if (options.verbose) { console.log(`Selected generation ${String(result.bestResult.generationIndex + 1)} as the best result`); // In verbose mode, we already show the full evaluation in the agent.ts file // Here we just show a summary for consistency with non-verbose mode console.log(`AI evaluation summary: ${result.evaluationReason.split("\n")[0]}`); } else { // In non-verbose mode, show just the first line of the evaluation console.log(`AI evaluation: ${result.evaluationReason.split("\n")[0]}`); } if (options.pp3Only) { return; } if (result.bestResult.processedImagePath !== imagePath) { await copyFinalOutput(result.bestResult.processedImagePath, imagePath, options.verbose); } if (!options.verbose && !options.keepPreview) { await cleanupIntermediateFiles(result.allResults, result.bestResult); } } async function processSingleGeneration(inputPath, options) { const pp3Content = await generatePP3FromRawImage({ inputPath, basePP3Path: options.base, // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing providerName: options.provider || "openai", // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing visionModel: options.model || "gpt-4-vision-preview", verbose: options.verbose, keepPreview: options.keepPreview, prompt: options.prompt, preset: options.preset, sections: options.sections?.split(",").filter((s) => s.trim() !== ""), previewQuality: options.previewQuality, previewFormat: options.previewFormat, maxRetries: options.maxRetries, }); if (!pp3Content) { throw new Error("Failed to generate PP3 content"); } const format = getOutputFormat(options); const { pp3Path, imagePath } = generateOutputPaths(inputPath, options, format); await fs.promises.writeFile(pp3Path, pp3Content); if (options.pp3Only) { return; } await convertDngToImageWithPP3({ input: inputPath, output: imagePath, pp3Path, format: format, tiffCompression: options.compression, bitDepth: Number(options.bitDepth), }); } export async function processImage(inputPath, options = {}) { await validateInputFile(inputPath); // Use multi-generation if generations > 1 OR if model is an array of models return (options.generations && options.generations > 1) || Array.isArray(options.model) ? processMultiGeneration(inputPath, options) : processSingleGeneration(inputPath, options); } // Create the CLI program // eslint-disable-next-line sonarjs/void-use void yargs(hideBin(process.argv)) .scriptName("ai-pp3") .usage("$0 <input> [options]") .command("$0 <input>", "Process a RAW image file with AI-generated PP3 profile", (yargs) => { return yargs .positional("input", { describe: "Input RAW file path", type: "string", demandOption: true, }) .option("output", { alias: "o", describe: "Output file path (defaults to input.pp3 or input_processed.jpg)", type: "string", }) .option("pp3-only", { describe: "Only generate PP3 file without processing the image", type: "boolean", }) .option("prompt", { alias: "p", describe: "Prompt text for AI analysis", type: "string", }) .option("preset", { describe: "Preset style to use (aggressive, creative, balanced, technical)", type: "string", default: "aggressive", }) .option("provider", { describe: "AI provider to use", type: "string", default: "openai", }) .option("model", { describe: "Model name to use (can be comma-separated list for multiple models)", type: "string", default: "gpt-4-vision-preview", }) .option("verbose", { alias: "v", describe: "Enable verbose logging", type: "boolean", }) .option("keep-preview", { alias: "k", describe: "Keep the preview file after processing", type: "boolean", }) .option("quality", { alias: "q", describe: "Quality of the output image (JPEG only)", type: "number", }) .option("preview-quality", { describe: "Quality for preview generation (1-100, JPEG only)", type: "number", coerce: (value) => { const quality = Number.parseInt(String(value), 10); if (Number.isNaN(quality) || quality < 1 || quality > 100) { throw new Error("Preview quality must be between 1 and 100"); } return quality; }, }) .option("preview-format", { describe: "Preview image format (jpeg or png)", type: "string", choices: ["jpeg", "png"], default: "jpeg", }) .option("tiff", { describe: "Output as TIFF format", type: "boolean", }) .option("png", { describe: "Output as PNG format", type: "boolean", }) .option("compression", { describe: "TIFF compression type (z/none)", type: "string", choices: ["z", "none"], }) .option("bit-depth", { describe: "Bit depth (8 or 16)", type: "number", default: 16, choices: [8, 16], }) .option("sections", { describe: "Comma-separated list of PP3 sections to process", type: "string", }) .option("base", { describe: "Base PP3 file to improve upon", type: "string", }) .option("max-retries", { describe: "Maximum number of retries for AI API calls", type: "number", coerce: (value) => { const retries = Number.parseInt(String(value), 10); if (Number.isNaN(retries) || retries < 0) { throw new Error("Max retries must be a non-negative integer"); } return retries; }, }) .option("generations", { describe: "Generate multiple PP3 profiles and use AI to select the best one", type: "number", coerce: (value) => { const generations = Number.parseInt(String(value), 10); if (Number.isNaN(generations) || generations < 1 || generations > 10) { throw new Error("Generations must be between 1 and 10"); } return generations; }, }); }, async (argv) => { try { await processImage(argv.input, { output: argv.output, pp3Only: argv["pp3-only"], provider: argv.provider, model: argv.model.includes(",") ? argv.model.split(",").map((m) => m.trim()) : argv.model, verbose: argv.verbose, keepPreview: argv["keep-preview"], quality: argv.quality, prompt: argv.prompt, preset: argv.preset, base: argv.base, sections: argv.sections, tiff: argv.tiff, png: argv.png, compression: argv.compression, bitDepth: argv["bit-depth"], previewQuality: argv["preview-quality"], previewFormat: argv["preview-format"], maxRetries: argv["max-retries"], generations: argv.generations, }); } catch (error_) { const error = error_ instanceof Error ? error_ : new Error("Unknown error occurred"); console.error("Error:", error.message); process.exit(1); } }) .describe("version", "Show version number") .alias("version", "V") .version(packageJson.version) .epilogue("AI-Powered PP3 Profile Generator for RawTherapee\nSpecializes in bulk generation and customization of PP3 development profiles\nKey features:\n- AI-driven analysis of RAW files (DNG/NEF/CR2/ARW)\n- Batch PP3 creation with consistent processing parameters\n- Customizable development settings through natural language prompts\n- Seamless integration with existing PP3 workflows\n- Multi-model support for different processing styles\n- Interactive preview generation with quality controls\nDocumentation available in README for advanced customization") .help() .alias("help", "h") .strict() .parse(); //# sourceMappingURL=bin.js.map