Compare commits

...

7 commits

Author SHA1 Message Date
Falko Zurell
aa2cedde8d corrected JSON comments in the README 2025-01-20 10:31:33 +01:00
Falko Zurell
bba97a2185 clarified where the config.json should go. 2025-01-20 10:28:57 +01:00
Falko Zurell
80ca4ce73c removed unused crate part 2025-01-20 09:53:20 +01:00
Falko Zurell
df29dbe4e5 bumbed version number for new OpenAI integration 2025-01-20 09:52:03 +01:00
Falko Zurell
a2e93a625c added integration of OpenAI Image Description 2025-01-20 09:47:00 +01:00
Falko Zurell
60acee3277 Merge branch 'origin/master' 2025-01-18 10:12:50 +01:00
Falko Zurell
da7efc83a3 we better use those new variables in package.sh 2025-01-18 10:12:43 +01:00
5 changed files with 105 additions and 11 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "pixelfed_batch_uploader"
version = "1.0.2"
version = "1.0.3"
edition = "2021"
[build]
@ -10,4 +10,5 @@ rustflags = ["--out-dir", "target/output"]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["blocking", "json", "multipart"] }
tokio = { version = "1", features = ["full"] }
tokio = { version = "1.0", features = ["full"] }
base64 = "0.21"

View file

@ -6,8 +6,15 @@ Two variables in the post description can be give (see the `config.json.example`
Usage: `./pixelfed_batch_uploader ../../Downloads/Instagram-Backup/media/posts/201406 --title "June 2014"`
The `config.json` must be in the same directory the program is called from (`$PWD`)
[![asciicast](https://asciinema.mxhdr.net/a/6.svg)](https://asciinema.mxhdr.net/a/6)
Check the package of this repo 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

@ -1,8 +1,12 @@
{
"pixelfed_url": "https://pixelfed.example.com",
# See https://docs.pixelfed.org/running-pixelfed/installation.html#setting-up-services
// See https://docs.pixelfed.org/running-pixelfed/installation.html#setting-up-services
"access_token": "sdg;lkjrglksjh;lkshj;lksjthrst;hoijrt;ihj;sithj;itjh",
"visibility": "unlisted",
"batch_size": 10,
"default_text": "Instagram dump from @title@ @batch@ #instabackup #instaimport #instaexit"
"default_text": "Instagram dump from @title@ @batch@ #instabackup #instaimport #instaexit",
// https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key
"openai_api_key": "0bff275feca7baab5ac508e635543f59fff42d4436c9918cd37c330f9adb4eb4fda643c212794b800bb05fb26016f55425c6755a3525c64792197e4d0fbe95d5",
"openai_api_url": "https://api.openai.com/v1/chat/completions",
"openai_model": "gpt-4o"
}

View file

@ -1,6 +1,6 @@
#!/bin/bash -x
RELEASE_VERSION="1.0.2"
RELEASE_VERSION="1.0.3"
PLATFORM=$(rustc -vV | grep host | cut -d ' ' -f2)

View file

@ -1,7 +1,10 @@
use std::fs;
use serde::Deserialize;
use serde_json::{json, Value};
use reqwest::blocking::Client;
use reqwest::{self};
use std::error::Error;
use base64::{Engine as _, engine::general_purpose::STANDARD};
#[derive(Debug, Deserialize)]
struct Config {
@ -10,6 +13,10 @@ struct Config {
visibility: String, // Should be "unlisted"
default_text: String,
batch_size: usize,
openai_api_key: String,
openai_api_url: String,
openai_model: String
}
fn load_config() -> Result<Config, Box<dyn Error>> {
@ -33,19 +40,92 @@ fn get_jpeg_files(directory: &str) -> Vec<String> {
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 {
template
.replace("@batch@", &format!("Batch {} out of {}", batch_num, total_batches))
.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 mut media_ids = Vec::new();
let mut media_descriptions = Vec::new();
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());
let form = reqwest::blocking::multipart::Form::new()
let form = reqwest::blocking::multipart::Form::new().text("description", image_description.clone())
.file("file", image_path)?;
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!({
"status": post_text,
"media_ids": media_ids,
"alt_texts": media_descriptions,
"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)
.bearer_auth(&config.access_token)
.json(&body)
@ -78,7 +159,8 @@ fn upload_images_batch(client: &Client, config: &Config, images: &[String], batc
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
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;
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() {
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.");