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
268 lines • 10.5 kB
JavaScript
import { env } from "node:process";
import sharp from "sharp";
import { generateText } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { createAnthropic } from "@ai-sdk/anthropic";
import { createMistral } from "@ai-sdk/mistral";
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock";
import { createAzure } from "@ai-sdk/azure";
import { createDeepInfra } from "@ai-sdk/deepinfra";
import { createFireworks } from "@ai-sdk/fireworks";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { createTogetherAI } from "@ai-sdk/togetherai";
import { createXai } from "@ai-sdk/xai";
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
// Helper function to resize images to appropriate sizes for different providers
async function sizeHandle(buffer, quality = 100, drop = 2, maxSize = 10_000_000, // Default max size (10MB)
maxDimension = 2000) {
const sharpInstance = sharp(buffer);
const { width = 0, height = 0 } = await sharpInstance.metadata();
let done = await sharp(buffer)
.resize({
...(width > height ? { width: maxDimension } : { height: maxDimension }),
withoutEnlargement: true,
})
.jpeg({
quality,
})
.toBuffer();
while (done.byteLength > maxSize) {
quality = Math.max(quality - drop, 0);
done = await sharp(buffer)
.resize({
...(width > height
? { width: maxDimension }
: { height: maxDimension }),
withoutEnlargement: true,
})
.jpeg({
quality,
})
.toBuffer();
}
return done;
}
// Get the appropriate AI SDK model based on the provider name
function getModel(provider, model) {
switch (provider.toLowerCase()) {
case "openai": {
const openaiProvider = createOpenAI({
apiKey: env.OPENAI_API_KEY,
baseURL: env.OPENAI_BASE_URL,
});
return openaiProvider(model ?? "gpt-4o");
}
case "google": {
const googleProvider = createGoogleGenerativeAI({
apiKey: env.API_KEY ?? env.GOOGLE_API_KEY,
});
return googleProvider(model ?? "gemini-1.5-pro");
}
case "anthropic": {
const anthropicProvider = createAnthropic({
apiKey: env.ANTHROPIC_API_KEY,
});
return anthropicProvider(model ?? "claude-3-5-sonnet-20241022");
}
case "mistral": {
const mistralProvider = createMistral({
apiKey: env.MISTRAL_API_KEY,
});
return mistralProvider(model ?? "mistral-large-latest");
}
case "ollama": {
// For Ollama, use OpenAI-compatible interface
const ollamaProvider = createOpenAI({
baseURL: env.OLLAMA_BASE_URL ?? "http://localhost:11434/v1",
apiKey: "ollama", // Ollama doesn't require a real API key
});
return ollamaProvider(model ?? "llama3.2-vision");
}
case "amazon":
case "bedrock": {
const bedrockProvider = createAmazonBedrock({
accessKeyId: env.AWS_ACCESS_KEY_ID,
secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
region: env.AWS_REGION ?? "us-east-1",
});
return bedrockProvider(model ?? "anthropic.claude-3-sonnet-20240229-v1:0");
}
case "azure": {
if (!env.AZURE_OPENAI_ENDPOINT) {
throw new Error("AZURE_OPENAI_ENDPOINT environment variable is required for Azure provider");
}
const azureProvider = createAzure({
apiKey: env.AZURE_OPENAI_API_KEY,
baseURL: env.AZURE_OPENAI_ENDPOINT,
apiVersion: env.AZURE_OPENAI_API_VERSION ?? "2023-12-01-preview",
});
return azureProvider(model ?? "gpt-4-vision");
}
case "deepinfra": {
const deepInfraProvider = createDeepInfra({
apiKey: env.DEEPINFRA_API_KEY,
});
return deepInfraProvider(model ?? "cogvlm2-llama3-8b-chat");
}
case "fireworks": {
const fireworksProvider = createFireworks({
apiKey: env.FIREWORKS_API_KEY,
});
return fireworksProvider(model ?? "accounts/fireworks/models/llama-v3-8b-instruct");
}
case "openai-compatible": {
if (!env.OPENAI_COMPATIBLE_BASE_URL) {
throw new Error("OPENAI_COMPATIBLE_BASE_URL environment variable is required for openai-compatible provider");
}
const openaiCompatibleProvider = createOpenAICompatible({
apiKey: env.OPENAI_COMPATIBLE_API_KEY ?? "key",
baseURL: env.OPENAI_COMPATIBLE_BASE_URL,
name: "openai-compatible",
});
return openaiCompatibleProvider(model ?? "gpt-4-vision");
}
case "together":
case "togetherai": {
const togetherAIProvider = createTogetherAI({
apiKey: env.TOGETHER_API_KEY,
});
return togetherAIProvider(model ?? "cogvlm2-llama3-8b-chat");
}
case "xai": {
const xaiProvider = createXai({
apiKey: env.XAI_API_KEY,
});
return xaiProvider(model ?? "grok-1.5-vision");
}
case "openrouter": {
const openRouterProvider = createOpenRouter({
apiKey: env.OPENROUTER_API_KEY,
});
return openRouterProvider(model ?? "openai/gpt-4o");
}
default: {
throw new Error(`Unsupported provider: ${provider}`);
}
}
}
// Get the appropriate max size and dimension based on the provider
function getProviderLimits(provider) {
const providerLower = provider.toLowerCase();
switch (providerLower) {
case "google": {
return { maxSize: 18_000_000, maxDimension: 6000 };
} // Google has higher limits
case "anthropic": {
return { maxSize: 10_000_000, maxDimension: 3000 };
} // Claude limits
case "amazon":
case "bedrock": {
return { maxSize: 5_000_000, maxDimension: 2500 };
} // Amazon Bedrock limits
case "azure": {
return { maxSize: 10_000_000, maxDimension: 2000 };
} // Azure OpenAI limits
case "deepinfra": {
return { maxSize: 8_000_000, maxDimension: 2048 };
} // DeepInfra limits
case "fireworks": {
return { maxSize: 10_000_000, maxDimension: 2048 };
} // Fireworks limits
case "together":
case "togetherai": {
return { maxSize: 10_000_000, maxDimension: 2048 };
} // TogetherAI limits
case "xai": {
return { maxSize: 10_000_000, maxDimension: 2048 };
} // XAI limits
case "openrouter": {
return { maxSize: 10_000_000, maxDimension: 2048 };
} // OpenRouter limits
default: {
return { maxSize: 10_000_000, maxDimension: 2000 };
} // Default limits
}
}
export async function getDescription({ buffer, model, prompt = "Please describe this image.", provider = "openai", // Default provider
}) {
try {
// Handle test providers
if (provider === "test" || provider === "provider1") {
// Return the prompt as the response for testing
return prompt;
}
// Get provider-specific limits
const { maxSize, maxDimension } = getProviderLimits(provider);
// Resize the image according to provider limits
const handled = await sizeHandle(buffer, 100, 2, maxSize, maxDimension);
// Get the appropriate AI SDK model
const aiModel = getModel(provider, model);
// Create the message content using AI SDK format
const result = await generateText({
model: aiModel,
messages: [
{
role: "user",
content: [
{
type: "text",
text: prompt,
},
{
type: "image",
image: handled, // AI SDK can handle Buffer directly
},
],
},
],
});
return result.text || "";
}
catch (error) {
console.error("An error occurred while getting the description:", error);
throw error; // Re-throw the error to be handled by the caller
}
}
export async function getTags({ buffer, model, prompt = "Please tag this image with relevant keywords. Output format: <tag1>, <tag2>, <tag3>, ...", provider = "openai", // Default provider
}) {
try {
// Handle test providers
if (provider === "test" || provider === "provider1") {
// Return the prompt as the response for testing
return prompt;
}
// Get provider-specific limits
const { maxSize, maxDimension } = getProviderLimits(provider);
// Resize the image according to provider limits
const handled = await sizeHandle(buffer, 100, 2, maxSize, maxDimension);
// Get the appropriate AI SDK model
const aiModel = getModel(provider, model);
// Create the message content using AI SDK format
const result = await generateText({
model: aiModel,
messages: [
{
role: "user",
content: [
{
type: "text",
text: prompt,
},
{
type: "image",
image: handled, // AI SDK can handle Buffer directly
},
],
},
],
});
return result.text || "";
}
catch (error) {
console.error("An error occurred while getting the tags:", error);
throw error; // Re-throw the error to be handled by the caller
}
}
//# sourceMappingURL=ai-sdk.js.map