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]
|
[package]
|
||||||
name = "pixelfed_batch_uploader"
|
name = "pixelfed_batch_uploader"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
|
@ -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"
|
||||||
|
|
|
@ -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"`
|
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)
|
[![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",
|
"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",
|
"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",
|
||||||
|
// 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
|
#!/bin/bash -x
|
||||||
|
|
||||||
RELEASE_VERSION="1.0.2"
|
RELEASE_VERSION="1.0.3"
|
||||||
|
|
||||||
PLATFORM=$(rustc -vV | grep host | cut -d ' ' -f2)
|
PLATFORM=$(rustc -vV | grep host | cut -d ' ' -f2)
|
||||||
|
|
||||||
|
|
92
src/main.rs
92
src/main.rs
|
@ -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};
|
||||||
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.");
|
||||||
|
|
Loading…
Reference in a new issue