diff --git a/packages/media/cpp/CMakeLists.txt b/packages/media/cpp/CMakeLists.txt index dc19e1ee..15da0ebb 100644 --- a/packages/media/cpp/CMakeLists.txt +++ b/packages/media/cpp/CMakeLists.txt @@ -145,6 +145,7 @@ add_executable(pm-image src/core/glob_paths.cpp src/core/output_path.cpp src/core/resize.cpp + src/core/transform.cpp src/core/url_fetch.cpp src/http/serve.cpp src/ipc/ipc_serve.cpp diff --git a/packages/media/cpp/dist/pm-image.exe b/packages/media/cpp/dist/pm-image.exe index c414451a..077f6735 100644 Binary files a/packages/media/cpp/dist/pm-image.exe and b/packages/media/cpp/dist/pm-image.exe differ diff --git a/packages/media/cpp/dist/pm-image.pdb b/packages/media/cpp/dist/pm-image.pdb index 6709282d..8bfb6215 100644 Binary files a/packages/media/cpp/dist/pm-image.pdb and b/packages/media/cpp/dist/pm-image.pdb differ diff --git a/packages/media/cpp/src/core/url_fetch.cpp b/packages/media/cpp/src/core/url_fetch.cpp index 9af50c05..340caf7d 100644 --- a/packages/media/cpp/src/core/url_fetch.cpp +++ b/packages/media/cpp/src/core/url_fetch.cpp @@ -10,14 +10,14 @@ namespace fs = std::filesystem; namespace media { -namespace { - -std::once_flag g_curl_init; +static std::once_flag g_curl_init; void ensure_curl_global() { std::call_once(g_curl_init, []() { curl_global_init(CURL_GLOBAL_DEFAULT); }); } +namespace { + static size_t write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) { auto *os = static_cast(userdata); const size_t n = size * nmemb; diff --git a/packages/media/cpp/src/core/url_fetch.hpp b/packages/media/cpp/src/core/url_fetch.hpp index 054aa6fc..317a714b 100644 --- a/packages/media/cpp/src/core/url_fetch.hpp +++ b/packages/media/cpp/src/core/url_fetch.hpp @@ -5,6 +5,9 @@ namespace media { +/// Call curl_global_init once (thread-safe via std::once_flag). +void ensure_curl_global(); + bool is_http_url(const std::string &s); /** Derive filename (last path segment) for single-file output when input is a URL. */ diff --git a/packages/media/cpp/src/main.cpp b/packages/media/cpp/src/main.cpp index 693d3d00..d005a9bf 100644 --- a/packages/media/cpp/src/main.cpp +++ b/packages/media/cpp/src/main.cpp @@ -27,6 +27,7 @@ std::string join_src_semicolons(const std::vector &v) { #include "core/output_path.hpp" #include "core/resize.hpp" +#include "core/transform.hpp" #include "http/serve.hpp" #include "ipc/ipc_serve.hpp" #if defined(_WIN32) @@ -145,6 +146,23 @@ int main(int argc, char **argv) { "Windows: new Win32++ ribbon UI with drag-drop queue and settings panel"); #endif + // ── transform (AI image editing) ────────────────────────────────── + std::string tf_input; + std::string tf_output; + std::string tf_prompt; + std::string tf_provider = "google"; + std::string tf_model = "gemini-2.0-flash-exp"; + std::string tf_api_key; + + auto *transform_cmd = app.add_subcommand("transform", "AI image editing (Gemini / Google)"); + transform_cmd->add_option("input", tf_input, "Input image path")->required(true); + transform_cmd->add_option("output", tf_output, "Output path (omit = auto from input + prompt)"); + transform_cmd->add_option("-p,--prompt", tf_prompt, "Editing prompt")->required(true); + transform_cmd->add_option("--provider", tf_provider, "AI provider (google)")->default_val("google"); + transform_cmd->add_option("--model", tf_model, "Model name")->default_val("gemini-2.0-flash-exp"); + transform_cmd->add_option("--api-key", tf_api_key, "API key (or set IMAGE_TRANSFORM_GOOGLE_API_KEY env)"); + + // ── serve ─────────────────────────────────────────────────────────── std::string host = "127.0.0.1"; int port = 8080; bool serve_no_cache = false; @@ -383,6 +401,33 @@ int main(int argc, char **argv) { return 0; } + if (transform_cmd->parsed()) { + media::TransformOptions topts; + topts.provider = tf_provider; + topts.model = tf_model; + topts.prompt = tf_prompt; + topts.api_key = tf_api_key; + + if (topts.api_key.empty()) { + const char* env_key = std::getenv("IMAGE_TRANSFORM_GOOGLE_API_KEY"); + if (env_key && env_key[0] != '\0') topts.api_key = env_key; + } + + auto progress = [](const std::string& msg) { + std::cerr << msg << "\n"; + }; + + auto result = media::transform_image(tf_input, tf_output, topts, progress); + if (!result.ok) { + std::cerr << "transform error: " << result.error << "\n"; + return 1; + } + std::cout << result.output_path << "\n"; + if (!result.ai_text.empty()) + std::cerr << "AI: " << result.ai_text << "\n"; + return 0; + } + if (serve_cmd->parsed()) { media::CacheServerDefaults cd; cd.enabled = !serve_no_cache;