393 lines
9.9 KiB
TypeScript
393 lines
9.9 KiB
TypeScript
import Anthropic from '@anthropic-ai/sdk';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
const anthropic = new Anthropic({
|
|
apiKey: process.env.ANTHROPIC_API_KEY || '',
|
|
});
|
|
|
|
// Example 1: Single image analysis
|
|
async function analyzeSingleImage(imagePath: string) {
|
|
// Read and encode image as base64
|
|
const imageData = fs.readFileSync(imagePath);
|
|
const base64Image = imageData.toString('base64');
|
|
|
|
// Determine media type from file extension
|
|
const ext = path.extname(imagePath).toLowerCase();
|
|
const mediaTypeMap: Record<string, string> = {
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.png': 'image/png',
|
|
'.webp': 'image/webp',
|
|
'.gif': 'image/gif',
|
|
};
|
|
const mediaType = mediaTypeMap[ext] || 'image/jpeg';
|
|
|
|
const message = await anthropic.messages.create({
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
max_tokens: 1024,
|
|
messages: [
|
|
{
|
|
role: 'user',
|
|
content: [
|
|
{
|
|
type: 'image',
|
|
source: {
|
|
type: 'base64',
|
|
media_type: mediaType,
|
|
data: base64Image,
|
|
},
|
|
},
|
|
{
|
|
type: 'text',
|
|
text: 'What is in this image? Describe it in detail.',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
const textContent = message.content.find(block => block.type === 'text');
|
|
if (textContent && textContent.type === 'text') {
|
|
console.log('Claude:', textContent.text);
|
|
}
|
|
|
|
return message;
|
|
}
|
|
|
|
// Example 2: Multiple images comparison
|
|
async function compareImages(image1Path: string, image2Path: string) {
|
|
const image1Data = fs.readFileSync(image1Path).toString('base64');
|
|
const image2Data = fs.readFileSync(image2Path).toString('base64');
|
|
|
|
const message = await anthropic.messages.create({
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
max_tokens: 1024,
|
|
messages: [
|
|
{
|
|
role: 'user',
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: 'Compare these two images. What are the similarities and differences?',
|
|
},
|
|
{
|
|
type: 'image',
|
|
source: {
|
|
type: 'base64',
|
|
media_type: 'image/jpeg',
|
|
data: image1Data,
|
|
},
|
|
},
|
|
{
|
|
type: 'image',
|
|
source: {
|
|
type: 'base64',
|
|
media_type: 'image/jpeg',
|
|
data: image2Data,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
const textContent = message.content.find(block => block.type === 'text');
|
|
if (textContent && textContent.type === 'text') {
|
|
console.log('Comparison:', textContent.text);
|
|
}
|
|
}
|
|
|
|
// Example 3: Vision with tools
|
|
const searchTool: Anthropic.Tool = {
|
|
name: 'search_product',
|
|
description: 'Search for similar products',
|
|
input_schema: {
|
|
type: 'object',
|
|
properties: {
|
|
keywords: {
|
|
type: 'array',
|
|
items: { type: 'string' },
|
|
description: 'Keywords to search for',
|
|
},
|
|
},
|
|
required: ['keywords'],
|
|
},
|
|
};
|
|
|
|
async function visionWithTools(imagePath: string) {
|
|
const imageData = fs.readFileSync(imagePath).toString('base64');
|
|
|
|
const message = await anthropic.messages.create({
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
max_tokens: 1024,
|
|
tools: [searchTool],
|
|
messages: [
|
|
{
|
|
role: 'user',
|
|
content: [
|
|
{
|
|
type: 'image',
|
|
source: {
|
|
type: 'base64',
|
|
media_type: 'image/jpeg',
|
|
data: imageData,
|
|
},
|
|
},
|
|
{
|
|
type: 'text',
|
|
text: 'Identify the objects in this image and search for similar products',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
console.log('Stop reason:', message.stop_reason);
|
|
|
|
if (message.stop_reason === 'tool_use') {
|
|
for (const block of message.content) {
|
|
if (block.type === 'tool_use') {
|
|
console.log('Tool requested:', block.name);
|
|
console.log('Search keywords:', block.input);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Example 4: Multi-turn conversation with images
|
|
async function multiTurnVision(imagePath: string) {
|
|
const imageData = fs.readFileSync(imagePath).toString('base64');
|
|
|
|
const messages: Anthropic.MessageParam[] = [
|
|
{
|
|
role: 'user',
|
|
content: [
|
|
{
|
|
type: 'image',
|
|
source: {
|
|
type: 'base64',
|
|
media_type: 'image/jpeg',
|
|
data: imageData,
|
|
},
|
|
},
|
|
{
|
|
type: 'text',
|
|
text: 'What objects are visible in this image?',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
// First turn
|
|
const response1 = await anthropic.messages.create({
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
max_tokens: 1024,
|
|
messages,
|
|
});
|
|
|
|
const text1 = response1.content.find(b => b.type === 'text');
|
|
if (text1 && text1.type === 'text') {
|
|
console.log('Claude:', text1.text);
|
|
messages.push({ role: 'assistant', content: text1.text });
|
|
}
|
|
|
|
// Second turn - follow-up question (image still in context)
|
|
messages.push({
|
|
role: 'user',
|
|
content: 'What color is the largest object?',
|
|
});
|
|
|
|
const response2 = await anthropic.messages.create({
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
max_tokens: 1024,
|
|
messages,
|
|
});
|
|
|
|
const text2 = response2.content.find(b => b.type === 'text');
|
|
if (text2 && text2.type === 'text') {
|
|
console.log('Claude:', text2.text);
|
|
}
|
|
}
|
|
|
|
// Example 5: Vision with prompt caching
|
|
async function visionWithCaching(imagePath: string) {
|
|
const imageData = fs.readFileSync(imagePath).toString('base64');
|
|
|
|
// First request - cache the image
|
|
const response1 = await anthropic.messages.create({
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
max_tokens: 1024,
|
|
messages: [
|
|
{
|
|
role: 'user',
|
|
content: [
|
|
{
|
|
type: 'image',
|
|
source: {
|
|
type: 'base64',
|
|
media_type: 'image/jpeg',
|
|
data: imageData,
|
|
},
|
|
cache_control: { type: 'ephemeral' },
|
|
},
|
|
{
|
|
type: 'text',
|
|
text: 'Describe the main objects in this image',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
console.log('First request - cache creation:', response1.usage.cache_creation_input_tokens);
|
|
|
|
const text1 = response1.content.find(b => b.type === 'text');
|
|
if (text1 && text1.type === 'text') {
|
|
console.log('Response 1:', text1.text);
|
|
}
|
|
|
|
// Second request - use cached image (within 5 minutes)
|
|
const response2 = await anthropic.messages.create({
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
max_tokens: 1024,
|
|
messages: [
|
|
{
|
|
role: 'user',
|
|
content: [
|
|
{
|
|
type: 'image',
|
|
source: {
|
|
type: 'base64',
|
|
media_type: 'image/jpeg',
|
|
data: imageData, // Same image
|
|
},
|
|
cache_control: { type: 'ephemeral' },
|
|
},
|
|
{
|
|
type: 'text',
|
|
text: 'What colors are prominent in this image?',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
console.log('Second request - cache read:', response2.usage.cache_read_input_tokens);
|
|
console.log('Token savings: ~90%');
|
|
}
|
|
|
|
// Example 6: Image URL (if accessible)
|
|
async function analyzeImageFromURL(imageUrl: string) {
|
|
// Note: Image must be publicly accessible
|
|
const message = await anthropic.messages.create({
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
max_tokens: 1024,
|
|
messages: [
|
|
{
|
|
role: 'user',
|
|
content: [
|
|
{
|
|
type: 'image',
|
|
source: {
|
|
type: 'url',
|
|
url: imageUrl,
|
|
},
|
|
},
|
|
{
|
|
type: 'text',
|
|
text: 'Analyze this image',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
const textContent = message.content.find(block => block.type === 'text');
|
|
if (textContent && textContent.type === 'text') {
|
|
console.log('Analysis:', textContent.text);
|
|
}
|
|
}
|
|
|
|
// Example 7: Image validation helper
|
|
function validateImage(filePath: string): { valid: boolean; error?: string } {
|
|
if (!fs.existsSync(filePath)) {
|
|
return { valid: false, error: 'File does not exist' };
|
|
}
|
|
|
|
const stats = fs.statSync(filePath);
|
|
const fileSizeMB = stats.size / (1024 * 1024);
|
|
|
|
if (fileSizeMB > 5) {
|
|
return { valid: false, error: 'Image exceeds 5MB limit' };
|
|
}
|
|
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
const supportedFormats = ['.jpg', '.jpeg', '.png', '.webp', '.gif'];
|
|
|
|
if (!supportedFormats.includes(ext)) {
|
|
return { valid: false, error: `Unsupported format. Use: ${supportedFormats.join(', ')}` };
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
// Example 8: Batch image analysis
|
|
async function analyzeMultipleImages(imagePaths: string[]) {
|
|
const results = [];
|
|
|
|
for (const imagePath of imagePaths) {
|
|
console.log(`\nAnalyzing: ${imagePath}`);
|
|
|
|
const validation = validateImage(imagePath);
|
|
if (!validation.valid) {
|
|
console.error(`Error: ${validation.error}`);
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const result = await analyzeSingleImage(imagePath);
|
|
results.push({ imagePath, result });
|
|
} catch (error) {
|
|
console.error(`Failed to analyze ${imagePath}:`, error);
|
|
}
|
|
|
|
// Rate limiting pause
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
// Run examples (with placeholder paths)
|
|
if (require.main === module) {
|
|
const exampleImagePath = './example-image.jpg';
|
|
|
|
// Check if example image exists
|
|
if (fs.existsSync(exampleImagePath)) {
|
|
console.log('=== Single Image Analysis ===\n');
|
|
analyzeSingleImage(exampleImagePath)
|
|
.then(() => {
|
|
console.log('\n=== Vision with Caching ===\n');
|
|
return visionWithCaching(exampleImagePath);
|
|
})
|
|
.catch(console.error);
|
|
} else {
|
|
console.log('Example image not found. Create example-image.jpg to test.');
|
|
console.log('\nValidation example:');
|
|
const validation = validateImage('./non-existent.jpg');
|
|
console.log(validation);
|
|
}
|
|
}
|
|
|
|
export {
|
|
analyzeSingleImage,
|
|
compareImages,
|
|
visionWithTools,
|
|
multiTurnVision,
|
|
visionWithCaching,
|
|
analyzeImageFromURL,
|
|
validateImage,
|
|
analyzeMultipleImages,
|
|
};
|