diff --git a/.env.example b/.env.example index c4abcc8..55b0347 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,8 @@ -# OpenAPI API -OPENAI_API_KEY=your_openai_api_key -OPENAI_API_URL=https://api.openai.com/v1/images/generate-description # Update this to the actual endpoint -OPENAI_MODEL=gpt-4-vision # Update to the model you want to use - -# WordPress API -WP_URL=https://yourwordpresssite.com -WP_USERNAME=your_wp_username -WP_PASSWORD=your_wp_application_password +#WordPress API +WP_URL=https://your.wordpress.com +WP_USERNAME=username +WP_APP_PASSWORD="ApplicationPassword" +# OpenAI API +OPENAI_API_KEY="SecretApplicationKey" +OPENAI_API_URL=https://api.openai.com/v1/chat/completions +OPENAI_MODEL=gpt-4o diff --git a/Cargo.toml b/Cargo.toml index ae2e3f2..beb84d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,11 +2,9 @@ name = "image_wp_uploader" version = "0.1.0" edition = "2021" - [dependencies] -tokio = { version = "1", features = ["full"] } -reqwest = { version = "0.11", features = ["json"] } -serde = { version = "1.0", features = ["derive"] } +tokio = { version = "1.0", features = ["full"] } +reqwest = { version = "0.11", features = ["json", "multipart"] } +dotenv = "0.15" serde_json = "1.0" -dotenvy = "0.15" -base64 = "0.21" # Use the latest version of the crate +base64 = "0.21" diff --git a/src/main.rs b/src/main.rs index 4d31cdf..f4d800d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,155 +1,158 @@ +use dotenv::dotenv; +use reqwest::{self, multipart}; +use serde_json::{json, Value}; use std::env; -use std::fs::File; -use std::io::Read; -use base64::engine::general_purpose::STANDARD; -use base64::Engine; // Import the Engine trait - -use reqwest::Client; -use serde_json::Value; +use std::error::Error; +use std::path::Path; +use tokio; +use base64::{Engine as _, engine::general_purpose::STANDARD}; #[derive(Debug)] struct Config { wp_url: String, wp_username: String, - wp_password: String, - openai_api_url: String, + wp_app_password: String, openai_api_key: String, + openai_api_url: String, openai_model: String, } -async fn generate_description(config: &Config, image_data: &[u8]) -> Result> { - let client = Client::new(); +impl Config { + fn from_env() -> Result> { + dotenv()?; + + Ok(Config { + wp_url: env::var("WP_URL")?, + wp_username: env::var("WP_USERNAME")?, + wp_app_password: env::var("WP_APP_PASSWORD")?, + openai_api_key: env::var("OPENAI_API_KEY")?, + openai_api_url: env::var("OPENAI_API_URL")?, + openai_model: env::var("OPENAI_MODEL")?, + }) + } +} - // Encode the image data in base64 using STANDARD engine - let encoded_image = STANDARD.encode(image_data); +#[tokio::main] +async fn main() -> Result<(), Box> { + // Get image path from command line arguments + let args: Vec = std::env::args().collect(); + if args.len() != 2 { + eprintln!("Usage: {} ", args[0]); + std::process::exit(1); + } + let image_path = &args[1]; - // Call OpenAI API with gpt-4-turbo + // Load configuration + let config = Config::from_env()?; + + // Get image description from ChatGPT + let description = get_image_description(&config, image_path).await?; + println!("Generated description: {}", description); + + // Upload image to WordPress and set ALT text + upload_to_wordpress(&config, image_path, &description).await?; + + Ok(()) +} + +async fn get_image_description(config: &Config, image_path: &str) -> Result> { + // Read and encode image + let image_data = std::fs::read(image_path)?; + 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)) - .json(&serde_json::json!({ - "model": config.openai_model, + .header("Content-Type", "application/json") + .json(&json!({ + "model": config.openai_model, // Now using the model from config + "max_tokens": 300, "messages": [ - { - "role": "system", - "content": "You are an AI assistant that describes images for blog posts." - }, { "role": "user", - "content": "Describe the content of this image.", - "image": encoded_image + "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?; - let response_text = response.text().await?; - let json: Value = serde_json::from_str(&response_text)?; + // 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()); + } - // Safely extract the description - let description = json["choices"] + let result: Value = response.json().await?; + + // More detailed error handling for JSON parsing + let description = result["choices"] .get(0) - .and_then(|choice| choice["message"]["content"].as_str()) - .unwrap_or("No description generated") + .ok_or("No choices in response")? + ["message"]["content"] + .as_str() + .ok_or("Invalid content format in response")? .to_string(); Ok(description) } async fn upload_to_wordpress( - wp_url: &str, - wp_username: &str, - wp_password: &str, - image_data: &[u8], - filename: &str, + config: &Config, + image_path: &str, description: &str, -) -> Result<(), Box> { - let client = Client::new(); - - // Encode the authentication string using base64::engine - let auth = STANDARD.encode(format!("{}:{}", wp_username, wp_password)); +) -> Result<(), Box> { + let client = reqwest::Client::new(); + let file_data = tokio::fs::read(image_path).await?; + let filename = Path::new(image_path) + .file_name() + .ok_or("Invalid filename")? + .to_str() + .ok_or("Invalid UTF-8 in filename")?; - // Upload image to WordPress + // Create form with image file + let part = multipart::Part::bytes(file_data) + .file_name(filename.to_string()) + .mime_str("image/jpeg")?; + + let form = multipart::Form::new() + .part("file", part) + .text("alt_text", description.to_string()) + .text("description", description.to_string()); + + // Upload to WordPress let response = client - .post(format!("{}/wp-json/wp/v2/media", wp_url)) - .header("Authorization", format!("Basic {}", auth)) - .header("Content-Disposition", format!("attachment; filename=\"{}\"", filename)) - .body(image_data.to_vec()) + .post(format!("{}/wp-json/wp/v2/media", config.wp_url)) + .basic_auth(&config.wp_username, Some(&config.wp_app_password)) + .multipart(form) .send() .await?; - let response_text = response.text().await?; - - // Deserialize the response as a JSON object (Value) - let json: Value = serde_json::from_str(&response_text)?; - - // Safely extract the media_id from the response - let media_id = json["id"] - .as_i64() - .unwrap_or(0) as i32; - - // Create a post with the image and description - client - .post(format!("{}/wp-json/wp/v2/posts", wp_url)) - .header("Authorization", format!("Basic {}", auth)) - .json(&serde_json::json!({ - "title": filename, - "content": description, - "status": "publish", - "featured_media": media_id, - })) - .send() - .await?; + let status = response.status(); + if status.is_success() { + let response_json: Value = response.json().await?; + println!("Successfully uploaded image to WordPress with description"); + if let Some(url) = response_json["source_url"].as_str() { + println!("Image URL: {}", url); + } + } else { + let error_text = response.text().await?; + println!("Failed to upload image: {}", status); + println!("Response: {}", error_text); + } Ok(()) } - -async fn process_image(config: &Config, image_path: &str) -> Result<(), Box> { - // Read the image file - let mut file = File::open(image_path)?; - let mut image_data = Vec::new(); - file.read_to_end(&mut image_data)?; - - // Generate description from OpenAI - let description = generate_description(config, &image_data).await?; - - // Upload image and create WordPress post - let filename = image_path.split('/').last().unwrap_or("image"); - upload_to_wordpress( - &config.wp_url, - &config.wp_username, - &config.wp_password, - &image_data, - filename, - &description, - ).await?; - - Ok(()) -} - -#[tokio::main] -async fn main() { - // Read command-line arguments - let args: Vec = env::args().collect(); - if args.len() < 2 { - eprintln!("Usage: {} ", args[0]); - return; - } - - let image_path = &args[1]; - - // Load configuration from .env or configuration file - let config = Config { - wp_url: "https://falko.zurell.de".to_string(), - wp_username: "falko".to_string(), - wp_password: "G6FI qmWi OG1M vXqP 1p5j rDDS".to_string(), - openai_api_url: "https://api.openai.com/v1/chat/completions".to_string(), - openai_api_key: "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".to_string(), - openai_model: "gpt-4.0".to_string(), - }; - - if let Err(e) = process_image(&config, image_path).await { - eprintln!("Error processing image: {}", e); - } -}