added integration of OpenAI Image Description

This commit is contained in:
Falko Zurell 2025-01-20 09:47:00 +01:00
parent 60acee3277
commit a2e93a625c
4 changed files with 98 additions and 7 deletions

View file

@ -10,4 +10,5 @@ rustflags = ["--out-dir", "target/output"]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
reqwest = { version = "0.11", features = ["blocking", "json", "multipart"] } reqwest = { version = "0.11", features = ["blocking", "json", "multipart"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1.0", features = ["full"] }
base64 = "0.21"

View file

@ -11,3 +11,8 @@ Usage: `./pixelfed_batch_uploader ../../Downloads/Instagram-Backup/media/posts/2
Check the [package of this repo](https://repos.mxhdr.net/maxheadroom/insta-import-pixelfed/packages) to get pre-compiled binaries for macOS (Apple Silicon), Linux x86_64, Windows ARM Check the [package of this repo](https://repos.mxhdr.net/maxheadroom/insta-import-pixelfed/packages) to get pre-compiled binaries for macOS (Apple Silicon), Linux x86_64, Windows ARM
## OpenAI Integration for Image Description
Added OpenAI integration to generate image descriptions and put them into the ALT text for each image. If an `openai_api_key` is present in the `config.json` then the Image description is fetched from the OpenAI API.

View file

@ -4,5 +4,8 @@
"access_token": "sdg;lkjrglksjh;lkshj;lksjthrst;hoijrt;ihj;sithj;itjh", "access_token": "sdg;lkjrglksjh;lkshj;lksjthrst;hoijrt;ihj;sithj;itjh",
"visibility": "unlisted", "visibility": "unlisted",
"batch_size": 10, "batch_size": 10,
"default_text": "Instagram dump from @title@ @batch@ #instabackup #instaimport #instaexit" "default_text": "Instagram dump from @title@ @batch@ #instabackup #instaimport #instaexit",
"openai_api_key": "0bff275feca7baab5ac508e635543f59fff42d4436c9918cd37c330f9adb4eb4fda643c212794b800bb05fb26016f55425c6755a3525c64792197e4d0fbe95d5",
"openai_api_url": "https://api.openai.com/v1/chat/completions",
"openai_model": "gpt-4o"
} }

View file

@ -1,7 +1,10 @@
use std::fs; use std::fs;
use serde::Deserialize; use serde::Deserialize;
use serde_json::{json, Value};
use reqwest::blocking::Client; use reqwest::blocking::Client;
use reqwest::{self, multipart};
use std::error::Error; use std::error::Error;
use base64::{Engine as _, engine::general_purpose::STANDARD};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct Config { struct Config {
@ -10,6 +13,10 @@ struct Config {
visibility: String, // Should be "unlisted" visibility: String, // Should be "unlisted"
default_text: String, default_text: String,
batch_size: usize, batch_size: usize,
openai_api_key: String,
openai_api_url: String,
openai_model: String
} }
fn load_config() -> Result<Config, Box<dyn Error>> { fn load_config() -> Result<Config, Box<dyn Error>> {
@ -33,19 +40,92 @@ fn get_jpeg_files(directory: &str) -> Vec<String> {
images images
} }
async fn get_image_description(config: &Config, image_path: &String) -> Result<String, Box<dyn Error>> {
// Read and encode image
let image_data = tokio::fs::read(image_path)
.await
.map_err(|e| format!("Failed to read image file: {}", e))?;
let base64_image = STANDARD.encode(&image_data);
// Create ChatGPT API request
let client = reqwest::Client::new();
let response = client
.post(&config.openai_api_url)
.header("Authorization", format!("Bearer {}", config.openai_api_key))
.header("Content-Type", "application/json")
.json(&json!({
"model": config.openai_model,
"max_tokens": 300,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Please describe this image concisely for use as an alt text description. Focus on key visual elements and context."
},
{
"type": "image_url",
"image_url": {
"url": format!("data:image/jpeg;base64,{}", base64_image)
}
}
]
}
]
}))
.send()
.await?;
// Improved error handling for API response
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(format!("OpenAI API error: {}", error_text).into());
}
let result: Value = response.json().await?;
// More detailed error handling for JSON parsing
let description = result["choices"]
.get(0)
.ok_or("No choices in response")?
["message"]["content"]
.as_str()
.ok_or("Invalid content format in response")?
.to_string();
Ok(description)
}
fn format_post_text(template: &str, batch_num: usize, total_batches: usize, title: &str) -> String { fn format_post_text(template: &str, batch_num: usize, total_batches: usize, title: &str) -> String {
template template
.replace("@batch@", &format!("Batch {} out of {}", batch_num, total_batches)) .replace("@batch@", &format!("Batch {} out of {}", batch_num, total_batches))
.replace("@title@", title) .replace("@title@", title)
} }
fn upload_images_batch(client: &Client, config: &Config, images: &[String], batch_num: usize, total_batches: usize, title: &str) -> Result<(), Box<dyn Error>> { async fn upload_images_batch(client: &Client, config: &Config, images: &[String], batch_num: usize, total_batches: usize, title: &str) -> Result<(), Box<dyn Error>> {
let url = format!("{}/api/v1/media", config.pixelfed_url); let url = format!("{}/api/v1/media", config.pixelfed_url);
let mut media_ids = Vec::new(); let mut media_ids = Vec::new();
let mut media_descriptions = Vec::new();
for image_path in images { for image_path in images {
println!("Fetching image description from OpenAI for {}", image_path.to_string());
let image_description: String ;
if !config.openai_api_key.is_empty() {
image_description = get_image_description(&config, &image_path).await?;
println!("DESC:\n {} \n", &image_description);
media_descriptions.push(image_description.clone());
} else {
image_description = String::new();
}
println!("Uploading image {}", image_path.to_string()); println!("Uploading image {}", image_path.to_string());
let form = reqwest::blocking::multipart::Form::new()
let form = reqwest::blocking::multipart::Form::new().text("description", image_description.clone())
.file("file", image_path)?; .file("file", image_path)?;
let res = client.post(&url) let res = client.post(&url)
@ -65,10 +145,11 @@ fn upload_images_batch(client: &Client, config: &Config, images: &[String], batc
let body = serde_json::json!({ let body = serde_json::json!({
"status": post_text, "status": post_text,
"media_ids": media_ids, "media_ids": media_ids,
"alt_texts": media_descriptions,
"visibility": config.visibility, "visibility": config.visibility,
}); });
println!("Posting batch {} out of {}", batch_num, total_batches); println!("Posting batch {} out of {} with media {}", batch_num, total_batches, media_ids.len());
client.post(&post_url) client.post(&post_url)
.bearer_auth(&config.access_token) .bearer_auth(&config.access_token)
.json(&body) .json(&body)
@ -78,7 +159,8 @@ fn upload_images_batch(client: &Client, config: &Config, images: &[String], batc
Ok(()) Ok(())
} }
fn main() -> Result<(), Box<dyn Error>> { #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
if args.len() < 2 { if args.len() < 2 {
eprintln!("Usage: {} <directory> [--title <title>]", args[0]); eprintln!("Usage: {} <directory> [--title <title>]", args[0]);
@ -98,7 +180,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let total_batches = (images.len() + config.batch_size - 1) / config.batch_size; let total_batches = (images.len() + config.batch_size - 1) / config.batch_size;
println!("Found a total of {} images to upload. Will take {} batches", images.len(), total_batches); println!("Found a total of {} images to upload. Will take {} batches", images.len(), total_batches);
for (i, chunk) in images.chunks(config.batch_size).enumerate() { for (i, chunk) in images.chunks(config.batch_size).enumerate() {
upload_images_batch(&client, &config, chunk, i + 1, total_batches, &title)?; upload_images_batch(&client, &config, chunk, i + 1, total_batches, &title).await?;
} }
println!("All images uploaded successfully."); println!("All images uploaded successfully.");