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
130 lines • 4.07 kB
JavaScript
import { objectFromEntries } from "ts-extras";
/**
* Determines the appropriate comma separator based on the content
*/
function determineCommaSeparator(text) {
if (text.includes(",")) {
return ",";
}
if (text.includes(",")) {
return ",";
}
return "\n";
}
/**
* Counts the number of spaces in a string
*/
function countSpaces(text) {
return (text.match(/\s/g) ?? []).length;
}
function formatTags(tags) {
if (typeof tags !== "string") {
return tags ?? [];
}
// Split the text by newlines first to avoid regex backtracking issues
const lines = tags.split(/[\n\r]+/);
const numberedLines = lines.filter((line) => /^\d+/.test(line));
const hasMultipleNumberedLines = numberedLines.length > 1;
if (hasMultipleNumberedLines) {
return numberedLines.map((s) => {
return s
.replaceAll(/tag\d+/g, "")
.replaceAll(/[[\].{}<>/*'"()。]/g, "")
.replace(/^\d+/, "") // Remove leading digits
.replaceAll(/[::]*/g, "")
.trim();
});
}
const cleanedTags = tags
.replaceAll(/tag\d+/g, "")
.replaceAll(/[[\].{}<>/*'"()。]/g, "");
// Determine separators
const colonSeparator = cleanedTags.includes(":") ? ":" : ":";
const commaSeparator = determineCommaSeparator(cleanedTags);
return (cleanedTags
.split(colonSeparator)
.at(-1)
?.split(commaSeparator)
.map((s) => s
.trim()
.replaceAll(/\n$/g, "")
.replaceAll(/^\d+\s+/g, "")
.replaceAll(/[::]*/g, ""))
.filter((s) => s.length > 0 && s !== "\n" && countSpaces(s) <= 1) ?? []);
}
/**
* Fetches tags from the provider
*/
async function fetchTags({ module, buffer, model, prompt, providerArgs, path, file_id, provider, verbose, repeat = 0, }) {
let tags = [];
for (let index = 0; index < repeat + 1; index++) {
try {
const result = await module.getTags?.({
buffer,
model,
prompt,
providerArgs,
path,
file_id,
provider, // Pass the provider name to the AI SDK
});
tags = formatTags(result);
}
catch (error) {
if (verbose)
console.error("Failed to get tags from provider:", error);
}
if (tags.length > 1)
break;
}
return tags;
}
/**
* Creates a record of tag entries
*/
function createTagsRecord(formatted, tagTags, existingTags) {
if (formatted.length === 0) {
return {};
}
if (!existingTags) {
return objectFromEntries(tagTags.map((key) => [key, formatted]));
}
return objectFromEntries(tagTags.map((key) => {
const existingTag = existingTags[key];
let combinedTags;
if (existingTag == null) {
combinedTags = formatted;
}
else if (Array.isArray(existingTag)) {
combinedTags = existingTag.concat(formatted);
}
else {
combinedTags = formatted.concat(existingTag.trim());
}
return [key, Array.from(new Set(combinedTags))];
}));
}
export async function getTags({ buffer, model, prompt, providerModule, providerArgs, verbose = false, tagTags, existingTags, additionalTags, path, file_id, repeat, provider, }) {
// Get tags from provider
let tags = [];
if (providerModule && typeof providerModule === "object") {
const module = providerModule;
tags = await fetchTags({
module,
buffer,
model,
prompt,
providerArgs,
path,
file_id,
provider,
verbose,
repeat,
});
}
const formatted = tags.concat(additionalTags ?? []);
if (verbose)
console.log("Tags are:", formatted);
return createTagsRecord(formatted, tagTags, existingTags);
}
//# sourceMappingURL=tags.js.map