exif-ai
Version:
A powerful Node.js CLI and library that uses AI providers (OpenAI, Google Gemini, Anthropic Claude, Mistral, Ollama, Amazon Bedrock, Azure OpenAI, DeepInfra, Fireworks, XAI, OpenRouter, and more) to intelligently write image descriptions and tags to EXIF
199 lines • 6.98 kB
JavaScript
import { readFile } from "node:fs/promises";
import { exiftool } from "exiftool-vendored";
import path from "node:path";
import { env } from "node:process";
import ISO6391 from "iso-639-1";
// @ts-expect-error - xhr2 doesn't have TypeScript definitions
import xhr2 from "xhr2";
// Export the new improved API
export * from "./api.js";
import fetch, { Headers, Request, Response, } from "node-fetch";
import { getDescription } from "./tasks/description.js";
import { getTags } from "./tasks/tags.js";
import { HttpsProxyAgent } from "https-proxy-agent";
import { getText } from "./fluent/index.js";
if (Boolean(env.https_proxy) ||
Boolean(env.HTTPS_PROXY) ||
Boolean(env.http_proxy) ||
Boolean(env.HTTP_PROXY)) {
// @ts-expect-error - Polyfilling fetch for proxy support
globalThis.fetch = function (url, init) {
let agentObject;
if (env.https_proxy) {
agentObject = { agent: new HttpsProxyAgent(env.https_proxy) };
}
else if (env.HTTPS_PROXY) {
agentObject = { agent: new HttpsProxyAgent(env.HTTPS_PROXY) };
}
else if (env.http_proxy) {
agentObject = { agent: new HttpsProxyAgent(env.http_proxy) };
}
else if (env.HTTP_PROXY) {
agentObject = { agent: new HttpsProxyAgent(env.HTTP_PROXY) };
}
return fetch(url, agentObject ? { ...init, ...agentObject } : init);
};
// @ts-expect-error - Polyfilling Headers
globalThis.Headers = Headers;
// @ts-expect-error - Polyfilling Request
globalThis.Request = Request;
// @ts-expect-error - Polyfilling Response
globalThis.Response = Response;
}
// Set XMLHttpRequest for environments that don't have it
global.XMLHttpRequest = xhr2;
const lang = env.LANG?.slice(0, 2);
/**
* Imports the AI SDK provider module
*/
async function importProviderModule(provider, verbose) {
try {
const providerModule = await import(`./provider/ai-sdk.js`);
if (verbose)
console.log("Using AI SDK with provider:", provider);
return providerModule;
}
catch (error) {
console.error("Failed to import AI SDK provider module", error);
return null;
}
}
/**
* Processes the image and generates metadata
*/
async function processImage({ tasks, buffer, resolvedPath, model, descriptionPrompt, tagPrompt, providerArgs, providerModule, verbose, descriptionTags, tagTags, existingTags, repeat, provider, }) {
// AI SDK doesn't use file_id
const file_id = undefined;
if (verbose) {
// log tasks' prompt
console.log("Description prompt:", descriptionPrompt);
console.log("Tag prompt:", tagPrompt);
}
return Promise.all([
tasks.includes("description")
? getDescription({
buffer,
model,
prompt: descriptionPrompt,
providerArgs,
providerModule,
verbose,
descriptionTags,
existingTags,
path: resolvedPath,
file_id,
repeat,
provider, // Pass the provider name
})
: undefined,
tasks.includes("tag") || tasks.includes("tags")
? getTags({
buffer,
model,
prompt: tagPrompt,
providerArgs,
providerModule,
tagTags,
existingTags,
additionalTags: undefined,
path: resolvedPath,
file_id,
repeat,
provider, // Pass the provider name
})
: undefined,
]);
}
/**
* Displays the results without writing to file
*/
function displayResults(result, verbose) {
console.log(JSON.stringify(result));
if (verbose)
console.log("Dry run - did not write to file");
}
/**
* Writes the results to the file
*/
async function writeResults(result, resolvedPath, writeArguments, verbose) {
if (Object.keys(result).length > 0) {
await exiftool.write(resolvedPath, result, { writeArgs: writeArguments });
if (verbose)
console.log("Wrote description to file:", resolvedPath);
}
else if (verbose) {
console.log("No new tags to write, skipping.");
}
}
/**
* Handles the results for a dry run (display only)
*/
function handleDryRun(result, verbose) {
displayResults(result, verbose);
}
/**
* Handles the results for a real run (write to file)
*/
async function handleFileWrite(result, resolvedPath, writeArguments, verbose) {
await writeResults(result, resolvedPath, writeArguments, verbose);
}
export async function execute({ tasks = ["description"], path: imagePath, provider, model, descriptionTags = [
"XPComment",
"Description",
"ImageDescription",
"Caption-Abstract",
], tagTags = ["Subject", "TagsList", "Keywords"], descriptionPrompt = getText("description-prompt-input") ??
`Describe image in ${lang ? ISO6391.getName(lang) : "English"}`, tagPrompt = getText("tag-prompt-input") ??
`Tag image in ${lang ? ISO6391.getName(lang) : "English"} words based on subject, object, event, place. Output format: <tag1>, <tag2>, <tag3>, <tag4>, <tag5>, ..., <tagN>`, verbose = false, dry = false, writeArgs, providerArgs, avoidOverwrite = false, repeat = 0, }) {
if (["description", "tag", "tags"].every((t) => !tasks.includes(t)))
return;
const resolvedPath = path.resolve(imagePath);
try {
// Read the file once to get the buffer and existing tags
const buffer = await readFile(resolvedPath);
if (verbose)
console.log("Read file from", resolvedPath);
// Check existing EXIF tags only if avoidOverwrite is true
const existingTags = avoidOverwrite
? await exiftool.read(resolvedPath)
: undefined;
// Import AI SDK provider module
const providerModule = await importProviderModule(provider, verbose);
if (!providerModule)
return;
const [description, tags] = await processImage({
tasks,
buffer,
resolvedPath,
model,
descriptionPrompt,
tagPrompt,
providerArgs,
providerModule,
verbose,
descriptionTags,
tagTags,
existingTags,
repeat,
provider,
});
const result = {
...description,
...tags,
};
if (dry) {
handleDryRun(result, verbose);
}
else {
await handleFileWrite(result, resolvedPath, writeArgs, verbose);
}
}
catch (error) {
console.error("An error occurred:", error);
}
finally {
// Ensure ExifTool session is closed properly
await exiftool.end();
}
}
//# sourceMappingURL=index.js.map