Created
October 15, 2024 12:53
-
-
Save huksley/b5a74851ea6374919f0bcfb9a427fb55 to your computer and use it in GitHub Desktop.
Rename portraits with Claude. Reads directory of files with photos of a person and asks Claude to rename them according to the surrounding.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable @typescript-eslint/no-require-imports */ | |
const crypto = require("node:crypto"); | |
const fs = require("node:fs"); | |
const sharp = require("sharp"); | |
const logger = console; | |
const Redis = require("ioredis"); | |
const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379", { | |
enableOfflineQueue: true, | |
commandTimeout: 3000, | |
lazyConnect: true | |
}); | |
const getset = async (key, func, ms) => { | |
const reply = await redis.get(key); | |
if (reply) return JSON.parse(reply); | |
const value = await func(); | |
await redis.set(key, JSON.stringify(value), "PX", ms); | |
return value; | |
}; | |
logger.info("Connecting to Redis", process.env.REDIS_URL); | |
redis.connect(); | |
const resizeMax = async sourceImageBuffer => { | |
const tmp = await sharp(sourceImageBuffer) | |
.rotate() | |
.resize({ | |
width: 1024, | |
height: 1024, | |
fit: "inside", | |
position: "center" | |
}) | |
.jpeg({ | |
quality: 80, | |
progressive: true, | |
chromaSubsampling: "4:4:4" | |
}) | |
.withMetadata(); | |
return await tmp.toBuffer(); | |
}; | |
const image2TextClaude = async (imageBuffer, prompt) => { | |
const claudeApiKey = process.env.ANTHROPIC_API_KEY; | |
if (!claudeApiKey) { | |
logger.warn("Claude API key is not configured"); | |
throw new Error("Configuration error"); | |
} | |
imageBuffer = await resizeMax(imageBuffer); | |
const hash = crypto.createHash("sha256"); | |
const base64Image = imageBuffer.toString("base64"); | |
hash.update(base64Image); | |
hash.update(prompt); | |
hash.update(new Date().toISOString().split("T")[0]); // Daily cache | |
hash.update(claudeApiKey); | |
const reqKey = hash.digest("base64"); | |
const now = Date.now(); | |
await redis; | |
logger.info("Claude request, image hash", reqKey); | |
const response = await getset( | |
`claude:${reqKey}`, | |
async () => { | |
const response = await fetch("https://api.anthropic.com/v1/messages", { | |
method: "POST", | |
headers: { | |
"Content-Type": "application/json", | |
"x-api-key": claudeApiKey, | |
"anthropic-version": "2023-06-01" | |
}, | |
body: JSON.stringify({ | |
model: "claude-3-5-sonnet-20240620", | |
messages: [ | |
{ | |
role: "user", | |
content: [ | |
{ | |
type: "image", | |
source: { | |
type: "base64", | |
media_type: "image/jpeg", | |
data: base64Image | |
} | |
}, | |
{ | |
type: "text", | |
text: prompt | |
} | |
] | |
} | |
], | |
max_tokens: 1000 | |
}) | |
}).then(r => r.json()); | |
return { | |
response, | |
now, | |
cached: false | |
}; | |
}, | |
1000 * 60 * 60 * 24 | |
); | |
if (response) { | |
response.cached = response.now !== now; | |
} | |
logger.info( | |
"Claude response, image hash", | |
reqKey, | |
"cached", | |
response?.cached, | |
"response", | |
JSON.stringify(response?.response, null, 2), | |
"time", | |
Date.now() - now, | |
"ms" | |
); | |
const text = response?.response?.content[0]?.text; | |
if (!text) { | |
logger.warn("Claude response is empty"); | |
throw new Error("Response error"); | |
} | |
return { text, response }; | |
}; | |
/** Read dir jpg files and ask claude for file name to rename */ | |
async function main1() { | |
const dir = process.argv[2]; | |
const outDir = process.argv[3]; | |
if (!dir) { | |
logger.error("Directory is not specified"); | |
return; | |
} | |
logger.info("Processing directory", dir); | |
const files = fs.readdirSync(dir); | |
for (const file of files) { | |
if (file.endsWith(".jpg")) { | |
const imageBuffer = fs.readFileSync(`${dir}/${file}`); | |
logger.info("Processing file", file); | |
const { text } = await image2TextClaude( | |
imageBuffer, | |
` | |
On this picture the main element is a person. His name is ${process.env.USER}. | |
Suggest a name of the file for this picture, which I can rename a file to, without using a special symbols, | |
use description of the person, place and environment. | |
Always assume it is ${process.env.USER}, do not put a generic name like "person" or "man". | |
Provide only the file name, nothing else.` | |
); | |
const fname = text.endsWith(".jpg") ? text : `${text}.jpg`; | |
logger.warn(file, fname); | |
if (outDir) { | |
logger.info("Renaming file", file, "to", outDir, fname); | |
fs.renameSync(`${dir}/${file}`, `${outDir}/${fname}`); | |
} | |
} | |
} | |
logger.info("Done"); | |
} | |
// Read and convert images to a 1024x1024 | |
async function main2() { | |
const dir = process.argv[2]; | |
const outDir = process.argv[3]; | |
if (!dir || !outDir) { | |
logger.error("Directories are not specified"); | |
return; | |
} | |
logger.info("Processing directory", dir); | |
const files = fs.readdirSync(dir); | |
for (const file of files) { | |
if (file.endsWith(".jpg")) { | |
const imageBuffer = fs.readFileSync(`${dir}/${file}`); | |
const resized = await resizeMax(imageBuffer); | |
logger.info("Writing file", file, "to", outDir); | |
fs.writeFileSync(`${outDir}/${file}`, resized); | |
} | |
} | |
logger.info("Done"); | |
} | |
main1(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment