Compare commits
7 commits
Author | SHA1 | Date | |
---|---|---|---|
|
aa2cedde8d | ||
|
bba97a2185 | ||
|
80ca4ce73c | ||
|
df29dbe4e5 | ||
|
a2e93a625c | ||
|
60acee3277 | ||
|
da7efc83a3 |
5 changed files with 105 additions and 11 deletions
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
92
src/main.rs
92
src/main.rs
|
@ -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.");
|
||||
|
|
Loading…
Reference in a new issue