ai-pp3
Version:
CLI tool combining multimodal AI analysis with RawTherapee's engine to generate optimized PP3 profiles for RAW photography
264 lines • 10.3 kB
JavaScript
// eslint-disable-next-line unicorn/import-style
import { basename, dirname, join } from "node:path";
import { convertDngToImage, convertDngToImageWithPP3, } from "./raw-therapee-wrap.js";
import { generateText } from "ai";
import fs from "node:fs";
import { PREVIEW_SETTINGS } from "./constants.js";
import { validateFileAccess, handleFileError } from "./utils/validation.js";
import { handleProviderSetup } from "./utils/ai-provider.js";
import { parseSearchReplaceBlocks } from "./pp3-parser.js";
import { BASE_PROMPT } from "./prompts.js";
async function createPreviewImage({ inputPath, previewPath, basePP3Path, quality, verbose, }) {
try {
await (basePP3Path
? convertDngToImageWithPP3({
input: inputPath,
output: previewPath,
pp3Path: basePP3Path,
format: "jpeg",
quality,
})
: convertDngToImage({
input: inputPath,
output: previewPath,
format: "jpeg",
quality,
}));
if (verbose)
console.log(`Preview file created at ${previewPath}`);
return true;
}
catch (error) {
if (error instanceof Error)
throw error;
throw new Error("Unknown error creating preview image");
}
}
export async function generatePP3FromRawImage({ inputPath, basePP3Path, providerName = "openai", visionModel = "gpt-4-vision-preview", verbose = false, keepPreview = false, prompt = BASE_PROMPT, sections = [
"Exposure",
"Retinex",
"Local Contrast",
"Wavlet",
"Vibrance",
"White Balance",
"Color appearance",
"Shadows & Highlights",
"RGB Curves",
"ColorToning",
"ToneEqualizer",
"Sharpening",
"Defringing",
"Dehaze",
"Directional Pyramid Denoising",
], previewQuality = PREVIEW_SETTINGS.quality, }) {
// Validate input file extension
const extension = inputPath.toLowerCase().slice(inputPath.lastIndexOf("."));
if (verbose)
console.log(`Analyzing image ${inputPath} with ${providerName} model ${visionModel}`);
// Generate preview path in same directory as input file
const previewPath = join(dirname(inputPath), `${basename(inputPath, extension)}_preview.jpg`);
let previewCreated = false;
try {
// Verify input file exists and is readable
await validateFileAccess(inputPath, "read");
// Check if preview path is writable
await validateFileAccess(dirname(previewPath), "write");
// Create preview with specific quality settings
if (verbose)
console.log(`Generating preview with quality=${String(previewQuality)}`);
previewCreated = await createPreviewImage({
inputPath,
previewPath,
basePP3Path,
quality: previewQuality,
verbose,
});
basePP3Path = basePP3Path ?? previewPath + ".pp3";
// Read preview file
let imageData = undefined;
try {
imageData = await fs.promises.readFile(previewPath);
if (verbose)
console.log("Preview file read successfully");
}
catch (error) {
handleFileError(error, previewPath, "read");
}
if (imageData == null) {
throw new Error("Failed to read preview image data");
}
// Read base PP3
let basePP3Content = undefined;
try {
basePP3Content = await fs.promises.readFile(basePP3Path, "utf8");
if (verbose)
console.log(`Base PP3 file read successfully from ${basePP3Path}`);
}
catch (error) {
handleFileError(error, basePP3Path, "read");
}
const lines = basePP3Content?.split("\n") ?? [];
const includedSections = [];
const excludedSections = [];
const sectionOrders = []; // 明确类型为 string[]
let currentSection = "";
let currentSectionName = "";
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith("[")) {
if (currentSection) {
if (sections.includes(currentSectionName)) {
includedSections.push(currentSection);
}
else {
excludedSections.push(currentSection);
}
}
const sectionName = trimmedLine.slice(1, -1);
currentSection = trimmedLine;
currentSectionName = sectionName;
sectionOrders.push(sectionName);
}
else {
currentSection += `\n${trimmedLine}`;
}
}
// 处理最后一个 section
if (currentSection) {
if (sections.includes(currentSectionName)) {
includedSections.push(currentSection);
}
else {
excludedSections.push(currentSection);
}
}
const aiProvider = handleProviderSetup(providerName, visionModel);
const toBeEdited = includedSections.join("\n");
const extractedText = `${prompt}\n\n${toBeEdited}`;
// Generate PP3 using AI
if (verbose)
console.log("Sending request to AI provider...", extractedText);
let response;
try {
response = await generateText({
model: aiProvider,
messages: [
{
role: "user",
content: [
{
type: "text",
text: extractedText,
},
{
type: "image",
image: imageData,
},
],
},
],
});
}
catch (error) {
throw new Error(`AI provider error (${providerName}): ${error instanceof Error ? error.message : "Unknown error"}`);
}
// Extract response text
const responseText = typeof response === "string" ? response : response.text;
if (!responseText) {
throw new Error("AI response was empty or in an unexpected format");
}
if (verbose)
console.log("Received response from AI provider:", responseText);
// parse search/replace blocks
// 解析搜索/替换块
const searchReplaceBlocks = parseSearchReplaceBlocks(responseText.replaceAll("```", ""));
if (searchReplaceBlocks.length === 0) {
if (verbose)
console.log("No valid search/replace blocks found");
throw new Error("No valid search/replace blocks found");
}
// 读取基础 pp3 文件
let pp3Content = toBeEdited;
// 应用每个搜索/替换块
for (const block of searchReplaceBlocks) {
const { search, replace } = block;
// Check if search or replace string is empty
if (!search || !replace) {
throw new Error("Invalid search/replace block format");
}
// Log the search and replace strings for debugging
if (verbose) {
console.log(`Searching for: ${search}`);
console.log(`Replacing with: ${replace}`);
}
pp3Content = pp3Content.replace(search.trim(), replace.trim());
}
const editedLines = pp3Content.split("\n");
const editedSections = [];
currentSection = "";
for (const line of editedLines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith("[")) {
if (currentSection !== "") {
editedSections.push(currentSection);
currentSection = "";
}
currentSection = line;
}
else {
currentSection += `\n${line}`;
}
}
// 处理最后一个 section
if (currentSection) {
editedSections.push(currentSection);
}
return sectionOrders
.map((sectionName) => {
return (editedSections.find((section) => section.startsWith(`[${sectionName}]`)) ??
includedSections.find((section) => section.startsWith(`[${sectionName}]`)) ??
excludedSections.find((section) => section.startsWith(`[${sectionName}]`)) ??
"");
})
.join("\n");
}
catch (error) {
if (error instanceof Error) {
if (verbose) {
console.error("Error during PP3 generation:");
console.error(error.message);
if (error.stack)
console.error(error.stack);
}
throw error;
}
throw new Error(`Unknown error during PP3 generation: ${String(error)}`);
}
finally {
// Clean up preview file in finally block unless keepPreview is true
if (previewCreated && !keepPreview) {
try {
await fs.promises.unlink(previewPath);
await fs.promises.unlink(previewPath + ".pp3");
if (verbose)
console.log("Preview file cleaned up");
}
catch (cleanupError) {
if (cleanupError instanceof Error &&
"code" in cleanupError &&
verbose) {
if (cleanupError.code === "ENOENT") {
console.warn("Preview file was already deleted");
}
else if (cleanupError.code === "EACCES") {
console.warn("Permission denied deleting preview file:", cleanupError.message);
}
else {
console.warn("Failed to clean up preview file:", cleanupError.message);
}
}
}
}
}
}
//# sourceMappingURL=agent.js.map