ai-pp3
Version:
CLI tool combining multimodal AI analysis with RawTherapee's engine to generate optimized PP3 profiles for RAW photography
263 lines • 12.2 kB
JavaScript
// AI processing functionality extracted from agent.ts
import { generateText } from "ai";
import fs from "node:fs";
import { handleProviderSetup } from "../utils/ai-provider.js";
import { readImageData, readBasePP3Content, } from "../file-operations/file-handlers.js";
import { splitPP3ContentBySections, splitContentIntoSections, } from "../pp3-sections/section-parser.js";
import { getPromptByPreset, EVALUATION_PROMPT } from "../prompts.js";
import { parseSearchReplaceBlocks, parseDirectSectionChanges, } from "../pp3-parser.js";
import { applyDirectSectionChanges, reconstructPP3Content, } from "../pp3-sections/section-manipulation.js";
import { applySearchReplaceBlocks } from "../pp3-processing/search-replace.js";
/**
* Generates AI response based on image and text
*/
export async function generateAIResponse(aiProvider, extractedText, imageData, maxRetries, providerName, verbose) {
try {
if (verbose) {
console.log(`Sending request to AI provider (${providerName})...`);
}
const response = await generateText({
model: aiProvider,
messages: [
{
role: "user",
content: [
{
type: "text",
text: extractedText,
},
{
type: "image",
image: imageData,
},
],
},
],
maxRetries,
});
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("\n=== COMPLETE AI RESPONSE ===");
console.log(responseText);
console.log("=== END OF AI RESPONSE ===\n");
}
return responseText;
}
catch (error) {
if (verbose) {
console.error(`AI provider error (${providerName}):`, error);
}
throw new Error(`AI provider error (${providerName}): ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Processes AI generation for PP3 content
*/
export async function processAIGeneration(previewPath, basePP3Path, sections, providerName, visionModel, prompt, preset, maxRetries, verbose) {
const imageData = await readImageData(previewPath, verbose);
const basePP3Content = await readBasePP3Content(basePP3Path, verbose);
const { includedSections, excludedSections, sectionOrders } = splitPP3ContentBySections(basePP3Content ?? "", sections);
const aiProvider = handleProviderSetup(providerName, visionModel);
const toBeEdited = includedSections.join("\n");
const promptText = prompt ?? getPromptByPreset(preset);
const extractedText = `${promptText}\n\n${toBeEdited}`;
// Detailed logging is now handled inside generateAIResponse
const responseText = await generateAIResponse(aiProvider, extractedText, imageData, maxRetries, providerName, verbose);
if (verbose)
console.log("AI response received and processed");
// Try to parse direct section changes first
const sectionChanges = parseDirectSectionChanges(responseText);
if (sectionChanges.length > 0) {
if (verbose) {
console.log(`Found ${String(sectionChanges.length)} direct section changes`);
}
// Apply direct section changes
return applyDirectSectionChanges(basePP3Content ?? "", sectionChanges, verbose);
}
else {
// Fall back to search/replace blocks for backward compatibility
if (verbose)
console.log("No direct section changes found, trying 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 changes found in AI response");
}
const pp3Content = applySearchReplaceBlocks(toBeEdited, searchReplaceBlocks, verbose);
const { sections: editedSections } = splitContentIntoSections(pp3Content);
return reconstructPP3Content(sectionOrders, editedSections, includedSections, excludedSections);
}
}
/**
* Prepares image contents for evaluation
*/
export async function prepareImageContents(generationResults, verbose) {
const imageContents = [{ type: "text", text: EVALUATION_PROMPT }];
// Filter out failed generations
const successfulResults = generationResults.filter((result) => result.success);
if (verbose && successfulResults.length < generationResults.length) {
console.log(`Skipping ${String(generationResults.length - successfulResults.length)} failed generations in evaluation`);
}
// Create a mapping for display indices (1-based) that skips failed generations
let displayIndex = 1;
for (const result of successfulResults) {
try {
const imageData = await fs.promises.readFile(result.evaluationImagePath);
imageContents.push({ type: "text", text: `\n\nGeneration ${String(displayIndex)}:` }, { type: "image", image: imageData });
displayIndex++;
}
catch (error) {
// Safe error handling
const errorMessage = error instanceof Error
? error.message
: "Unknown error reading evaluation image";
if (verbose) {
console.warn(`Failed to read evaluation image ${result.evaluationImagePath}:`, errorMessage);
}
// Mark this generation as failed since we couldn't read its image
result.success = false;
}
}
return imageContents;
}
/**
* Parses the best generation index from AI response
*/
export function parseBestGenerationIndex(responseText, generationResults) {
const bestGenerationMatch = /BEST_GENERATION:\s*(\d+)/i.exec(responseText);
if (!bestGenerationMatch) {
// Default to the first successful generation if no match
const firstSuccessfulIndex = generationResults.findIndex((result) => result.success);
return Math.max(firstSuccessfulIndex, 0);
}
// Get the display index (1-based) from the AI response
const displayIndex = Number.parseInt(bestGenerationMatch[1], 10);
// Create a mapping from display indices to actual indices
const successfulResults = generationResults.filter((result) => result.success);
const displayToActualMap = new Map();
let currentDisplayIndex = 1;
for (const result of successfulResults) {
displayToActualMap.set(currentDisplayIndex, result.generationIndex);
currentDisplayIndex++;
}
// Get the actual index from the map, or default to the first successful one
const actualIndex = displayToActualMap.get(displayIndex);
if (actualIndex !== undefined) {
return actualIndex;
}
// Fallback to the first successful generation
const firstSuccessfulIndex = generationResults.findIndex((result) => result.success);
return Math.max(firstSuccessfulIndex, 0);
}
/**
* Handles the case when there's only one successful generation
*/
function handleSingleGeneration(successfulResults, generationResults) {
const originalIndex = generationResults.indexOf(successfulResults[0]);
return {
bestIndex: originalIndex,
evaluationReason: "Only one successful generation available",
};
}
/**
* Attempts to evaluate generations with a specific model
*/
async function attemptEvaluationWithModel(model, modelIndex, models, providerName, imageContents, maxRetries, successfulResults, generationResults, verbose) {
if (verbose && models.length > 1) {
console.log(`Attempting evaluation with model ${model} (${String(modelIndex + 1)}/${String(models.length)})...`);
}
try {
const aiProvider = handleProviderSetup(providerName, model);
const response = await generateText({
model: aiProvider,
messages: [
{
role: "user",
content: imageContents,
},
],
maxRetries,
});
const responseText = typeof response === "string" ? response : response.text;
const bestIndex = parseBestGenerationIndex(responseText, successfulResults);
// Map the best index from the successful results array back to the original array
const originalIndex = generationResults.findIndex((result) => result.generationIndex === successfulResults[bestIndex].generationIndex);
const finalIndex = originalIndex === -1 ? bestIndex : originalIndex;
if (verbose) {
console.log(`AI selected generation ${String(successfulResults[bestIndex].generationIndex + 1)} as the best`);
console.log("\n=== COMPLETE AI EVALUATION RESPONSE ===");
console.log(responseText);
console.log("=== END OF AI EVALUATION RESPONSE ===\n");
}
return {
bestIndex: finalIndex,
evaluationReason: responseText,
};
}
catch {
// Handle error in the calling function
return null;
}
}
/**
* Handles the fallback case when all models fail
*/
function handleAllModelsFailed(generationResults, lastError, verbose) {
if (verbose) {
console.warn("All AI evaluation models failed, using first generation as fallback:", lastError);
}
// Find the index of the first successful generation in the original array
const firstSuccessfulIndex = generationResults.findIndex((result) => result.success);
return {
bestIndex: Math.max(firstSuccessfulIndex, 0),
evaluationReason: `AI evaluation failed: ${lastError instanceof Error ? lastError.message : "Unknown error"}. Using first successful generation as fallback.`,
};
}
/**
* Evaluates multiple generations and selects the best one
* If multiple models are specified, it will try each one sequentially until successful
*/
export async function evaluateGenerations(generationResults, providerName, visionModel, maxRetries, verbose) {
// Filter out failed generations
const successfulResults = generationResults.filter((result) => result.success);
if (successfulResults.length === 0) {
throw new Error("No successful generations to evaluate");
}
if (successfulResults.length === 1) {
return handleSingleGeneration(successfulResults, generationResults);
}
// Only pass successful generations to prepareImageContents
const imageContents = await prepareImageContents(successfulResults, verbose);
if (verbose) {
console.log(`Evaluating ${String(successfulResults.length)} successful generations with AI...`);
}
// Convert visionModel to array for sequential attempts
const models = Array.isArray(visionModel) ? visionModel : [visionModel];
let lastError = null;
// Try each model sequentially until one succeeds
for (let modelIndex = 0; modelIndex < models.length; modelIndex++) {
const currentModel = models[modelIndex];
const result = await attemptEvaluationWithModel(currentModel, modelIndex, models, providerName, imageContents, maxRetries, successfulResults, generationResults, verbose);
if (result) {
return result;
}
else {
// Handle error and try next model if available
const error = new Error(`Evaluation with model ${currentModel} failed`);
lastError = error;
if (verbose) {
console.warn(`AI evaluation with model ${currentModel} failed: ${error.message}`);
}
if (modelIndex < models.length - 1 && verbose) {
console.log(`Trying next model: ${models[modelIndex + 1]}...`);
}
}
}
// If we get here, all models failed
return handleAllModelsFailed(generationResults, lastError, verbose);
}
//# sourceMappingURL=ai-processor.js.map