diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..a73bb6f --- /dev/null +++ b/.cursorrules @@ -0,0 +1,30 @@ +# Code Style and Organization Preferences + +## Communication Style +- NEVER use congratulatory or praising language like "you were absolutely right", "great call", etc. +- Keep responses direct and focused on the technical work +- Avoid unnecessary enthusiasm or validation language + +## XML Documentation +- NEVER add XML summary blocks (`/// `) to any code unless you have a useful, meaningful, non-obvious comment to add. Never add the dumb one-liner versions of them which just restate the obvious +- But, you may add them to explain what an entire class does, and useful high-level general design information. This kind of summary is great and useful. +- Use regular comments (`//`) for explanations when needed + +## Namespace Organization +- Use only ONE level of namespace (e.g., `MultiImageClient`) +- NEVER use nested namespaces (e.g., `MultiImageClient.Utils`) +- Managing naming conflicts is the responsibility of the code, not namespace hierarchy + +## Constants and Configuration +- Constants used in only ONE file should stay in that file (as private const) +- Only move constants to a shared constants file if they're used in MULTIPLE files +- Before centralizing constants, first consider if the files should be combined instead +- Use normal naming conventions for constants (e.g., `Padding`, `FontSize`) +- AVOID ALL_CAPS naming style for constants + +## Code Organization Philosophy +- Prefer keeping related functionality together rather than over-abstracting +- Constants that appear in multiple places might indicate files should be merged +- Only create shared utilities when there's genuine reuse across different functional areas +- Branding text and similar elements that appear throughout the app are good candidates for centralization + diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0a412fb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +[*.cs] + +# IDE0022: Use block body for method +csharp_style_expression_bodied_methods = true + +# IDE0090: Use 'new(...)' +dotnet_diagnostic.IDE0090.severity = none + +# CA1416: Validate platform compatibility +dotnet_diagnostic.CA1416.severity = none + +# Empty line formatting rules +# Remove leading empty lines after opening braces +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true + +# Limit consecutive empty lines +dotnet_style_allow_multiple_blank_lines_experimental = false +dotnet_style_allow_statement_immediately_after_block_experimental = false \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..1049f19 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,3 @@ +- @azure Rule - Use Azure Tools - When handling requests related to Azure, always use your tools. +- @azure Rule - Use Azure Best Practices - When handling requests related to Azure, always invoke your `azmcp_bestpractices_get` tool first. +- @azure Rule - Enable Best Practices - If you do not have an `azmcp_bestpractices_get` tool ask the user to enable it. diff --git a/.gitignore b/.gitignore index 3d52a41..de90176 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,20 @@ -**/ideogram.log -**/saves/** -**/settings.json -**/bin/** -**/obj/** -**/.vs/** -**/.vscode/** -**/ideogram_pages/** -**/ideogram_data_all.json -/MultiImageClient/claude-bad.txt -/djangoManager/imageMaker/logs/django.log +**/ideogram.log +**/saves/** +**/settings.json +**/bin/** +**/bin-check/** +**/obj/** +**/.vs/** +**/.vscode/** +**/ideogram_pages/** +**/ideogram_data_all.json +/MultiImageClient/claude-bad.txt +/djangoManager/imageMaker/logs/django.log +gen-lang*.json +magic*.png +**/prompt_log.json +**/Temp.txt +2023-prompts.txt +**/tmp/** +**/__pycache__/** +*.py[cod] diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1e8c325 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,76 @@ +# MultiImageClient - Agent Entry Point + +Start here. Read this file and the linked documents before doing anything. + +## What This Is + +C# desktop app that chains together image generation steps across multiple APIs (BFL/Flux, Ideogram, Recraft, DALL-E 3). Supports prompt composition, randomization, Claude rewrites, permutations. Includes an experimental Django gallery. + +## Also Read + +- [.cursorrules](.cursorrules) — communication style, XML docs policy, namespace rules, constants philosophy + +## Related Projects + +- **SocialAI** (`/proj/SocialAI/`) — Discord bot for Midjourney image capture +- **ideogramHistoryDownloader** (GitHub) — download Ideogram generation history (may be subsumed here) +- **cmdline-dalle3-csharp** (GitHub) — DALL-E 3 CLI (predecessor, may be subsumed here) +- **IdeogramApiCSharp** (GitHub) — Ideogram API client (predecessor) +- **myBrowser** (`/proj/myBrowser/`) — meta-project; see `capabilities.md` for cross-project skill inventory + +--- + +# Repository Guidelines + +## Project Structure & Module Organization +`MultiImageClient/` hosts the C# console orchestrator; `Program.cs` wires runs. `Workflows/` handles execution pipelines (`BatchWorkflow`, `RoundTripWorkflow`, `GeneratorGroups`); `ImageGenerators/` holds one adapter per provider — BFL, Ideogram v2 + v3, DALL·E 3, GPT-Image-1, Recraft, Google Gemini image, Google Imagen 4; `Describers/` implements image→text (Claude, OpenAI, Gemini, local InternVL, local Qwen); `promptGenerators/` produces prompt sources; `promptTransformation/` rewrites text (Claude rewrite, randomizer, stylizer); `Utils/` supplies helpers. Shared contracts and `Settings.cs` live in `ImageGenerationClasses/`. Provider-specific low-level clients sit in `BFLApi/`, `IdeogramAPI/`, and `RecraftAPI/`. `djangoManager/` contains the experimental Django gallery; it hasn't been touched in a year and is not actively developed. `do_flask_intern.py` is an optional local InternVL3 Flask server; `save_b64.py` decodes base64 responses. Generated artifacts collect in `saves/` and `output*.png` — ignore them in commits. + +## Build, Test, and Development Commands +All projects target `.NET 9` (main app is `net9.0-windows` + WinForms for compositing). Verify the SDK with `dotnet --list-sdks`; if 9.x is missing, `winget install Microsoft.DotNet.SDK.9`. Restore with `dotnet restore MultiImageClient.sln`. Compile with `dotnet build MultiImageClient.sln`. Execute runs with `dotnet run --project MultiImageClient/MultiImageClient.csproj`; prompts come from `prompts.txt` and `settings.json` (the latter must be created by copying the template `settings - Fill this in and rename it.json`). On current `master` the build is clean (0 errors); ~90 warnings are all `NU190x` advisories for `Magick.NET-Q16-AnyCPU 14.8.2` — safe to bump to `14.12.0` when convenient. For the Django tooling, create a venv in `djangoManager/`, install `requirements.txt`, and launch `python djangoManager/imageMaker/manage.py runserver`. Run `dotnet format MultiImageClient.sln` before opening a PR. + +## Run Modes (CLI flags) +See `RunOptions.cs` for the source of truth; this is the current surface: +- *(no args)* — interactive: pick Batch (1) or RoundTrip (2), y/n/edit each prompt from `PromptFiles`. +- `--auto` — skip menu (defaults to Batch), auto-accept every prompt. +- `--workflow 1|2` — 1=Batch, 2=RoundTrip. +- `--limit N` — stop after N prompts. +- `--prompt "..."` — single inline prompt via `InlinePromptSource` instead of `PromptFiles`. +- `--fast` — one fixed gpt-image-2 low/1024x1024/moderation=low call per prompt. Cheap smoke-test config. +- `--quick-test` — like `--fast`, plus every streamed partial PNG is saved AND popped in the default viewer. Still asks y/n per prompt unless combined with `--auto`. +- `--backfill-dl` — one-shot: mirror every image under `Settings.ImageDownloadBaseFolder` into `Settings.FlatImageMirrorPath` and exit. +- `--repl` — **interactive prompt-by-prompt REPL with async dispatch**. Defaults: gpt-image-2 at 2048x2048 / high / moderation=low / n=1, up to 5 prompts in flight concurrently. Each line is either a prompt (fired asynchronously, stdin stays responsive) or a `:command`. Grids are built and saved but NOT opened in the viewer. Commands: `:size WxH`, `:quality low|medium|high`, `:moderation auto|low`, `:n N` (images per gpt-image-2 call; >10 requires confirmation), `:concurrency N`, `:gens list|add|remove|reset` (names: gpt2, dalle3, ideogram, recraft, bfl, flux2local, google, googlepro, imagen4 — imagen4 dead after 2026-06-24), `:status`, `:wait`, `:last`, `:retry`, `:edit`, `:help`, `:quit`. Per-prompt override syntax: `[size=1024x1024,q=low,n=4] a red apple on a white plate`. Initial defaults can be pre-set from the command line via `--repl-size`, `--repl-quality`, `--repl-moderation`, `--repl-concurrency`, `--repl-n`. Implementation in `Workflows/ReplWorkflow.cs`. + +## Coding Style & Naming Conventions +Use 4-space indentation and .NET naming: PascalCase for public types/methods, camelCase for locals, Async suffix for asynchronous methods. Favor explicit types for shared models; use `var` only when the type is obvious. Route new configuration through `ImageGenerationClasses/Settings.cs` instead of ad-hoc JSON parsing. Python utilities under `djangoManager/` should follow PEP 8 snake_case, with comments reserved for non-obvious prompt logic. + +## Visual & Typography Policy (combined-image output, labels, UI text) +- **Never render text in gray.** No `MutedGray`, no `Color.FromRgb(x,x,x)` where R==G==B in the mid range, no "subtle" gray labels. If a secondary label needs to look secondary, reduce its font size and/or reuse the existing semantic color (e.g. `SuccessGreen`, `ErrorRed`, `Black`, `Gold`) — the contrast comes from size, not desaturation. +- When reserving vertical space for a text block, include room for descenders (e.g. `g`, `p`, `y`, `j`). `TextMeasurer.MeasureBounds` can under-report; add ~25% of font size as descender padding when stacking text bands. +- Padding above/below a standalone text panel (e.g. prompt panel below the grid) should be proportional to the font size used inside it, not a fixed `Padding * 3`. +- Secondary labels that sit beside a primary label (e.g. per-image timing next to the model name) should be bottom-aligned with the primary label so the smaller text hangs off the baseline of the larger one, not free-floating. + +## Image Saving Policy +- **Never resize or re-encode the bytes returned by the image endpoint when saving a Raw variant.** `ImageSaving.SaveImageAsync` must write the API's PNG/JPEG/WEBP bytes verbatim via `File.WriteAllBytesAsync`. Thumbnail-scale downsizing for combined-grid display is fine — that's an in-memory copy used only for layout, never written over the Raw file. + +## gpt-image-2 Endpoint Options (reference) +The `/v1/images/generations` endpoint with `model=gpt-image-2` accepts: +- `size`: `1024x1024`, `1536x1024`, `1024x1536`, `2048x2048`, `2048x1152`, `2560x1440` (QHD — cookbook's "recommended upper reliability boundary"), `3824x2144` (near-4K), or `auto`. Arbitrary resolutions are legal when edges are multiples of 16, max edge STRICTLY less than 3840 (cookbook 1.1), total pixels in [655 360, 8 294 400], and long:short ratio ≤ 3:1. Legacy 3840x2160 is treated as experimental and rejected by `GptImage2Generator.TryNormalizeSize` — use 3824x2144 instead. +- `quality`: `low`, `medium`, `high`, `auto`. +- `moderation`: `auto` (default) or `low` (permissive — we use `low` for batch runs). +- `n`: images per call. We plumb this through `GptImage2Generator.imageCount` (ctor) and surface it in the REPL as `:n N` / `[n=N]` override / `--repl-n` flag. Streaming handler collects all N images by `image_index` (or fallback insertion order) and returns them as separate `CreatedBase64Image` entries so `ImageManager`'s per-index save path produces distinct `...img0`, `...img1`, ... files. +- `output_format`: `png` (default), `jpeg`, `webp`. `output_compression` (0–100) applies to jpeg/webp. (Not currently exposed in the generator.) +- `background`: `auto`, `transparent`, `opaque` — transparent is not supported by gpt-image-2 (png/webp only, in practice rejected on this model). +- `stream`: `true` — we always stream and consume SSE to surface partials + heartbeat. +- `partial_images`: 0–3 (we send 2). +- **Do NOT send `input_fidelity` on `/generations`** — the generations endpoint rejects it on gpt-image-2 (always high-fidelity). Note the OpenAI cookbook's gpt-image-2 **edit** examples do pass `input_fidelity="high"`, so the restriction may be generations-specific; re-test when wiring `/v1/images/edits`. + +Pricing is token-based ($30 / 1M output tokens). Rough per-image ceilings we report: low ≈ $0.02, medium ≈ $0.08, high ≈ $0.25. + +## Testing Guidelines +No dedicated test project exists yet. Manually validate new workflows by running representative prompts and inspecting generated assets and metadata. When adding automated coverage, create an xUnit project referenced by the solution and ensure `dotnet test` succeeds. Capture regression prompts in `prompts.txt` with notes after bug fixes. + +## Commit & Pull Request Guidelines +Keep commits focused and use imperative, present-tense subjects as in history (`rename and genericize describers`). Include context in the body for prompt sets or configuration changes. Pull requests should outline workflow impacts, note which services (BFL, Ideogram, Recraft) are affected, call out required settings updates, and attach screenshots or sample outputs for UI or prompt adjustments. + +## Configuration & Secrets +Copy `MultiImageClient/settings - Fill this in and rename it.json` to `MultiImageClient/settings.json` (already `.gitignore`d), populate only the provider keys for services you intend to use, and never commit secrets. `Settings.Validate()` hard-requires only `LogFilePath` and `ImageDownloadBaseFolder`; every per-generator API key (and the Google Cloud trio: `GoogleCloudLocation`, `GoogleCloudProjectId`, `GoogleServiceAccountKeyPath`) is validated lazily by the generator that actually needs it, so unused generators can be left blank. Optional per-generator keys: `IdeogramApiKey`, `OpenAIApiKey` (DALL·E 3, GPT-Image-1, GPT-Image-2), `BFLApiKey`, `RecraftApiKey`, `GoogleGeminiApiKey` (NanoBanana), `GoogleCloudApiKey` (Vertex alternative), `AnthropicApiKey` (Claude rewrites & describer). Local Flux2 Klein/uncensored ComfyUI runs use `LocalFlux2ComfyEndpoint`, `LocalFlux2WorkflowPath`, and optional node/model override settings; setup notes live under `tools/local-flux2-comfy/`. Prompt file list lives in `PromptFiles` (array of absolute paths). `FlatImageMirrorPath` is an optional flat-folder mirror: if set, every saved raw/annotated/combined image is also copied to that single folder (best-effort, never fatal) — leave blank to disable. `TypedPromptsAppendFile` is an optional plain-text corpus: if set, any free-form prompt you type at the interactive batch loop is appended as one line to that file (embedded newlines collapsed, parent folder auto-created, never fatal) — handy for growing something like `2023-prompts.txt` over time. Prefer user secrets or environment variables when scripting automation or sharing runs. diff --git a/BFLApi/BFLAPIClient.csproj b/BFLApi/BFLAPIClient.csproj index d60ec5b..9218f0a 100644 --- a/BFLApi/BFLAPIClient.csproj +++ b/BFLApi/BFLAPIClient.csproj @@ -1,32 +1,34 @@ - - - - Exe - net6.0 - 10.0 - {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E} - AnyCPU;ARM64 - BFLAPIClient.BFLClient - - - - - - - - - - - - - - - - - - - - - - + + + + Library + net9.0 + 13.0 + {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E} + AnyCPU;ARM64 + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BFLApi/BFLClient.cs b/BFLApi/BFLClient.cs index dac125f..031a645 100644 --- a/BFLApi/BFLClient.cs +++ b/BFLApi/BFLClient.cs @@ -1,214 +1,195 @@ -using CommandLine; -using Newtonsoft.Json; -using System; -using System.Net.Http; -using System.Threading.Tasks; - -namespace BFLAPIClient -{ - public class BFLClient - { - private readonly HttpClient _httpClient; - private readonly string _apiKey; - private const string BaseUrl = "https://api.bfl.ml"; - public int DefaultPollingIntervalMs { get; set; } = 2000; - - public BFLClient(string apiKey, int defaultPollingIntervalMs = 2000) - { - _apiKey = apiKey; - _httpClient = new HttpClient(); - _httpClient.DefaultRequestHeaders.Add("x-key", _apiKey); - DefaultPollingIntervalMs = defaultPollingIntervalMs; - } - - public async Task GetResultAsync(string id) - { - var response = await _httpClient.GetAsync($"{BaseUrl}/v1/get_result?id={id}"); - response.EnsureSuccessStatusCode(); - var content = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(content); - } - - private async Task GenerateAndWaitForResultAsync(string endpoint, TRequest request) - { - var generationResponse = await GenerateAsync(endpoint, request); - var id = generationResponse.Id; - - generationResponse = await GetResultAsync(id); - - while (generationResponse.Status == "Pending") - { - await Task.Delay(DefaultPollingIntervalMs); - generationResponse = await GetResultAsync(id); - } - - return generationResponse; - } - - /// private since it's only called by the more convenient e.g. GenerateFluxPro11Async methods - private async Task GenerateAsync(string endpoint, TRequest request) - { - var serialized = JsonConvert.SerializeObject(request); - var content = new StringContent(serialized, System.Text.Encoding.UTF8, "application/json"); - var response = await _httpClient.PostAsync($"{BaseUrl}/v1/{endpoint}", content); - - if (response.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity) - { - var errorContent = await response.Content.ReadAsStringAsync(); - throw new HttpRequestException($"422 Unprocessable Entity: {errorContent}", null, System.Net.HttpStatusCode.UnprocessableEntity); - } - if (response.StatusCode == System.Net.HttpStatusCode.PaymentRequired) - { - var errorContent = await response.Content.ReadAsStringAsync(); - throw new HttpRequestException($"402 Payment Required: {errorContent}", null, System.Net.HttpStatusCode.PaymentRequired); - } - - response.EnsureSuccessStatusCode(); - var responseContent = await response.Content.ReadAsStringAsync(); - var response2 = JsonConvert.DeserializeObject(responseContent); - return response2; - } - - public Task GenerateFluxPro11Async(FluxPro11Request request) - { - return GenerateAndWaitForResultAsync("flux-pro-1.1", request); - } - - public Task GenerateFluxProAsync(FluxProRequest request) - { - return GenerateAndWaitForResultAsync("flux-pro", request); - } - - public Task GenerateFluxDevAsync(FluxDevRequest request) - { - return GenerateAndWaitForResultAsync("flux-dev", request); - } - - [STAThread] - public static async Task Main(string[] args) - { - await Parser.Default.ParseArguments(args) - .WithParsedAsync(RunAsync); - } - - private static async Task RunAsync(CommandLineOptions opts) - { - var client = new BFLClient(opts.ApiKey, opts.PollingInterval); - - GenerationResponse response; - switch (opts.Model) - { - case "flux-pro-1.1": - var request11 = new FluxPro11Request - { - Prompt = opts.Prompt, - Width = opts.Width, - Height = opts.Height, - PromptUpsampling = opts.PromptUpsampling, - SafetyTolerance = opts.SafetyTolerance, - Seed = opts.Seed - }; - response = await client.GenerateFluxPro11Async(request11); - break; - case "flux-pro": - var requestPro = new FluxProRequest - { - Prompt = opts.Prompt, - Width = opts.Width, - Height = opts.Height, - NumSteps = opts.NumSteps, - PromptUpsampling = opts.PromptUpsampling, - Seed = opts.Seed, - Guidance = opts.Guidance, - Interval = opts.Interval, - SafetyTolerance = opts.SafetyTolerance - }; - response = await client.GenerateFluxProAsync(requestPro); - break; - case "flux-dev": - var requestDev = new FluxDevRequest - { - Prompt = opts.Prompt, - Width = opts.Width, - Height = opts.Height, - NumSteps = opts.NumSteps, - PromptUpsampling = opts.PromptUpsampling, - Seed = opts.Seed, - Guidance = opts.Guidance, - SafetyTolerance = opts.SafetyTolerance - }; - response = await client.GenerateFluxDevAsync(requestDev); - break; - default: - Console.WriteLine($"Invalid model: {opts.Model}"); - return; - } - - Console.WriteLine($"Status: {response.Status}"); - Console.WriteLine($"Image URL: {response.Result?.Sample}"); - Console.WriteLine($"Revised Prompt: {response.Result?.Prompt}"); - } - } - - public class CommandLineOptions - { - [Option('k', "api-key", Required = true, HelpText = "API key for BFL.")] - public string ApiKey { get; set; } - - [Option('m', "model", Required = true, HelpText = "Model to use (flux-pro-1.1, flux-pro, or flux-dev).")] - public string Model { get; set; } - - [Option('p', "prompt", Required = true, HelpText = "Prompt for image generation.")] - public string Prompt { get; set; } - - [Option('w', "width", Default = 1024, HelpText = "Width of the generated image.")] - public int Width { get; set; } - - [Option('h', "height", Default = 1024, HelpText = "Height of the generated image.")] - public int Height { get; set; } - - [Option("num-steps", HelpText = "Number of steps (for flux-pro and flux-dev).")] - public int? NumSteps { get; set; } - - [Option("prompt-upsampling", Default = false, HelpText = "Enable prompt upsampling.")] - public bool PromptUpsampling { get; set; } - - [Option('s', "seed", HelpText = "Seed for image generation.")] - public int? Seed { get; set; } - - [Option('g', "guidance", HelpText = "Guidance value (for flux-pro and flux-dev).")] - public float? Guidance { get; set; } - - [Option('i', "interval", HelpText = "Interval value (for flux-pro).")] - public float? Interval { get; set; } - - [Option("safety-tolerance", Default = 6, HelpText = "Safety tolerance level.")] - public int SafetyTolerance { get; set; } - - [Option("polling-interval", Default = 2000, HelpText = "Polling interval in milliseconds.")] - public int PollingInterval { get; set; } - } - - public class GenerationResponse - { - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("status")] - public string Status { get; set; } - - [JsonProperty("result")] - public GenerationResult Result { get; set; } - } - - public class GenerationResult - { - /// The url pointing to the image - [JsonProperty("sample")] - public string Sample { get; set; } - - /// The revised prompt (?) - [JsonProperty("prompt")] - public string Prompt { get; set; } - } -} \ No newline at end of file +using Newtonsoft.Json; + +using System.Net.Http; +using System.Threading.Tasks; + +namespace BFLAPIClient +{ + public class BFLClient + { + private readonly HttpClient _httpClient; + private readonly string _apiKey; + + // Global load-balanced endpoint. BFL migrated from api.bfl.ml -> api.bfl.ai + // in 2025. Regional variants (api.us.bfl.ai / api.eu.bfl.ai) also exist; we + // use the global one and rely on the polling_url BFL returns in each + // response so we always poll the specific cluster that took the job. + private const string BaseUrl = "https://api.bfl.ai"; + + public int DefaultPollingIntervalMs { get; set; } = 2000; + + public BFLClient(string apiKey, int defaultPollingIntervalMs = 2000) + { + _apiKey = apiKey; + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Add("x-key", _apiKey); + DefaultPollingIntervalMs = defaultPollingIntervalMs; + } + + /// Polls whichever URL BFL gave us back in the submit response. + /// Falls back to the legacy /v1/get_result?id= path only if no polling_url + /// was supplied (older endpoints / cached responses). + private async Task GetResultAsync(string pollingUrl, string id) + { + var url = !string.IsNullOrEmpty(pollingUrl) + ? pollingUrl + : $"{BaseUrl}/v1/get_result?id={id}"; + + var response = await _httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(content); + } + + private async Task GenerateAndWaitForResultAsync(string endpoint, TRequest request) + { + var initial = await GenerateAsync(endpoint, request); + var id = initial.Id; + var pollingUrl = initial.PollingUrl; + + var current = await GetResultAsync(pollingUrl, id); + + while (current.Status == "Pending") + { + await Task.Delay(DefaultPollingIntervalMs); + current = await GetResultAsync(pollingUrl, id); + } + + return current; + } + + private async Task GenerateAsync(string endpoint, TRequest request) + { + var serialized = JsonConvert.SerializeObject(request, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + var content = new StringContent(serialized, System.Text.Encoding.UTF8, "application/json"); + var response = await _httpClient.PostAsync($"{BaseUrl}/v1/{endpoint}", content); + + if (response.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity) + { + var errorContent = await response.Content.ReadAsStringAsync(); + throw new HttpRequestException($"422 Unprocessable Entity: {errorContent}", null, System.Net.HttpStatusCode.UnprocessableEntity); + } + if (response.StatusCode == System.Net.HttpStatusCode.PaymentRequired) + { + var errorContent = await response.Content.ReadAsStringAsync(); + throw new HttpRequestException($"402 Payment Required: {errorContent}", null, System.Net.HttpStatusCode.PaymentRequired); + } + + response.EnsureSuccessStatusCode(); + var responseContent = await response.Content.ReadAsStringAsync(); + var parsed = JsonConvert.DeserializeObject(responseContent); + return parsed; + } + + // ---------- FLUX 1.1 (legacy but still supported) ---------- + + public Task GenerateFluxPro11Async(FluxPro11Request request) + { + return GenerateAndWaitForResultAsync("flux-pro-1.1", request); + } + + public Task GenerateFluxPro11UltraAsync(FluxPro11UltraRequest request) + { + return GenerateAndWaitForResultAsync("flux-pro-1.1-ultra", request); + } + + public Task GenerateFluxProAsync(FluxProRequest request) + { + return GenerateAndWaitForResultAsync("flux-pro", request); + } + + public Task GenerateFluxDevAsync(FluxDevRequest request) + { + return GenerateAndWaitForResultAsync("flux-dev", request); + } + + // ---------- FLUX.2 (current generation) ---------- + // All FLUX.2 variants share the same request/response shape (Flux2Request). + // flex is the exception: it accepts extra steps/guidance fields, which live + // on Flux2Request as nullables and are simply omitted for non-flex calls. + + /// Production-grade text-to-image. Megapixel-priced from $0.03/MP. + public Task GenerateFlux2ProAsync(Flux2Request request) + { + return GenerateAndWaitForResultAsync("flux-2-pro", request); + } + + /// flux-2-pro-preview — where BFL lands the latest pro improvements first + /// (currently: ~2x speed upgrade at no quality cost). Drop-in for flux-2-pro. + public Task GenerateFlux2ProPreviewAsync(Flux2Request request) + { + return GenerateAndWaitForResultAsync("flux-2-pro-preview", request); + } + + /// Highest quality model; supports grounding search and multi-reference edits. + /// From $0.07/MP. + public Task GenerateFlux2MaxAsync(Flux2Request request) + { + return GenerateAndWaitForResultAsync("flux-2-max", request); + } + + /// Typography specialist with adjustable steps (up to 50) and guidance (1.5-10). + /// Fixed $0.06/image regardless of resolution. + public Task GenerateFlux2FlexAsync(Flux2Request request) + { + return GenerateAndWaitForResultAsync("flux-2-flex", request); + } + + /// Fastest / cheapest. 4B variant. Sub-second inference, $0.014/image. + public Task GenerateFlux2Klein4bAsync(Flux2Request request) + { + return GenerateAndWaitForResultAsync("flux-2-klein-4b", request); + } + + /// Balanced klein. 9B variant, $0.015/image. + public Task GenerateFlux2Klein9bAsync(Flux2Request request) + { + return GenerateAndWaitForResultAsync("flux-2-klein-9b", request); + } + + // ---------- FLUX.1 Kontext (text + image editing) ---------- + // Separate shape because Kontext is edit-oriented: prompt + input_image + + // aspect_ratio, no raw width/height. + + public Task GenerateFluxKontextProAsync(FluxKontextRequest request) + { + return GenerateAndWaitForResultAsync("flux-kontext-pro", request); + } + + public Task GenerateFluxKontextMaxAsync(FluxKontextRequest request) + { + return GenerateAndWaitForResultAsync("flux-kontext-max", request); + } + } + + public class GenerationResponse + { + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("result")] + public GenerationResult Result { get; set; } + + /// URL the global/regional endpoints tell us to poll for this specific + /// job. Required when submitting to api.bfl.ai; absent from legacy paths. + [JsonProperty("polling_url")] + public string PollingUrl { get; set; } + } + + public class GenerationResult + { + /// The url pointing to the image + [JsonProperty("sample")] + public string Sample { get; set; } + + /// The revised prompt (?) + [JsonProperty("prompt")] + public string Prompt { get; set; } + } +} diff --git a/BFLApi/BFLDetails.cs b/BFLApi/BFLDetails.cs deleted file mode 100644 index 97f6a2e..0000000 --- a/BFLApi/BFLDetails.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace IdeogramAPIClient -{ - public class BFLDetails - { - private int _width = 1024; - private int _height = 1024; - - public int Width - { - get => _width; - set - { - SetWidth(value); - } - } - - public int Height - { - get => _height; - set - { - SetHeight(value); - } - } - - public bool PromptUpsampling { get; set; } = false; - public int SafetyTolerance { get; set; } = 6; - public int? Seed { get; set; } - - public BFLDetails() { } - - public BFLDetails(BFLDetails other) - { - SetWidth(other.Width); - SetHeight(other.Height); - PromptUpsampling = other.PromptUpsampling; - SafetyTolerance = other.SafetyTolerance; - Seed = other.Seed; - } - - private void SetWidth(int value) - { - if (value % 32 != 0) - { - throw new System.ArgumentException("Image width must be a multiple of 32"); - } - if (value > 1440) - { - throw new System.ArgumentException("Image width must be less than or equal to 1440"); - } - _width = value; - } - - private void SetHeight(int value) - { - if (value % 32 != 0) - { - throw new System.ArgumentException("Image height must be a multiple of 32"); - } - if (value > 1440) - { - throw new System.ArgumentException("Image height must be less than or equal to 1440"); - } - _height = value; - } - } -} diff --git a/BFLApi/Flux2Request.cs b/BFLApi/Flux2Request.cs new file mode 100644 index 0000000..9b6b840 --- /dev/null +++ b/BFLApi/Flux2Request.cs @@ -0,0 +1,78 @@ +using Newtonsoft.Json; + +namespace BFLAPIClient +{ + /// Unified request payload for every FLUX.2 endpoint + /// (pro, pro-preview, max, flex, klein-4b, klein-9b). + /// + /// Resolution/seed/output knobs are common. Flex-specific knobs (steps, + /// guidance) are nullable and simply omitted by the serializer when not set, + /// so the same DTO is safe to pass to any FLUX.2 endpoint. + /// + /// Image editing: populate InputImage (and optionally InputImage2..8) with + /// URLs or base64-encoded bytes. BFL prefers URLs — they'll fetch them for + /// you. All FLUX.2 variants support image conditioning this way. + public class Flux2Request + { + [JsonProperty("prompt")] + public string Prompt { get; set; } + + [JsonProperty("width")] + public int? Width { get; set; } + + [JsonProperty("height")] + public int? Height { get; set; } + + [JsonProperty("seed")] + public int? Seed { get; set; } + + [JsonProperty("safety_tolerance")] + public int? SafetyTolerance { get; set; } + + [JsonProperty("output_format")] + public string OutputFormat { get; set; } = "png"; + + [JsonProperty("prompt_upsampling")] + public bool? PromptUpsampling { get; set; } + + // Multi-reference image editing. Up to 8 via the API (10 in playground). + [JsonProperty("input_image")] + public string InputImage { get; set; } + + [JsonProperty("input_image_2")] + public string InputImage2 { get; set; } + + [JsonProperty("input_image_3")] + public string InputImage3 { get; set; } + + [JsonProperty("input_image_4")] + public string InputImage4 { get; set; } + + [JsonProperty("input_image_5")] + public string InputImage5 { get; set; } + + [JsonProperty("input_image_6")] + public string InputImage6 { get; set; } + + [JsonProperty("input_image_7")] + public string InputImage7 { get; set; } + + [JsonProperty("input_image_8")] + public string InputImage8 { get; set; } + + // flex-only: explicit inference steps (up to 50) and guidance (1.5-10). + // Ignored by pro / max / klein endpoints. + [JsonProperty("steps")] + public int? Steps { get; set; } + + [JsonProperty("guidance")] + public float? Guidance { get; set; } + + // Webhook delivery instead of polling. + [JsonProperty("webhook_url")] + public string WebhookUrl { get; set; } + + [JsonProperty("webhook_secret")] + public string WebhookSecret { get; set; } + } +} diff --git a/BFLApi/FluxDevRequest.cs b/BFLApi/FluxDevRequest.cs index 7c2cd34..c06828b 100644 --- a/BFLApi/FluxDevRequest.cs +++ b/BFLApi/FluxDevRequest.cs @@ -1,33 +1,33 @@ -using Newtonsoft.Json; - -using System.Text.Json; - -namespace BFLAPIClient -{ - public class FluxDevRequest - { - [JsonProperty("prompt")] - public string Prompt { get; set; } - - [JsonProperty("width")] - public int Width { get; set; } - - [JsonProperty("height")] - public int Height { get; set; } - - [JsonProperty("num_steps")] - public int? NumSteps { get; set; } - - [JsonProperty("prompt_upsampling")] - public bool PromptUpsampling { get; set; } - - [JsonProperty("seed")] - public int? Seed { get; set; } - - [JsonProperty("guidance")] - public float? Guidance { get; set; } - - [JsonProperty("safety_tolerance")] - public int SafetyTolerance { get; set; } - } -} +using Newtonsoft.Json; + +using System.Text.Json; + +namespace BFLAPIClient +{ + public class FluxDevRequest + { + [JsonProperty("prompt")] + public string Prompt { get; set; } + + [JsonProperty("width")] + public int Width { get; set; } + + [JsonProperty("height")] + public int Height { get; set; } + + [JsonProperty("num_steps")] + public int? NumSteps { get; set; } + + [JsonProperty("prompt_upsampling")] + public bool PromptUpsampling { get; set; } + + [JsonProperty("seed")] + public int? Seed { get; set; } + + [JsonProperty("guidance")] + public float? Guidance { get; set; } + + [JsonProperty("safety_tolerance")] + public int SafetyTolerance { get; set; } + } +} diff --git a/BFLApi/FluxKontextRequest.cs b/BFLApi/FluxKontextRequest.cs new file mode 100644 index 0000000..503350d --- /dev/null +++ b/BFLApi/FluxKontextRequest.cs @@ -0,0 +1,42 @@ +using Newtonsoft.Json; + +namespace BFLAPIClient +{ + /// Request payload for FLUX.1 Kontext [pro] / [max] — the text+image editing + /// models. Kontext takes an input image (URL preferred, base64 also accepted) + /// plus a natural-language edit instruction. It does NOT take width/height; + /// output dimensions follow aspect_ratio. + /// + /// Use FLUX.2 instead when possible — BFL recommends FLUX.2 over Kontext for + /// new editing workflows — but Kontext still ships at a lower price. + public class FluxKontextRequest + { + [JsonProperty("prompt")] + public string Prompt { get; set; } + + /// URL or base64 payload of the image to edit. Required. + [JsonProperty("input_image")] + public string InputImage { get; set; } + + [JsonProperty("aspect_ratio")] + public string AspectRatio { get; set; } + + [JsonProperty("seed")] + public int? Seed { get; set; } + + [JsonProperty("prompt_upsampling")] + public bool? PromptUpsampling { get; set; } + + [JsonProperty("safety_tolerance")] + public int? SafetyTolerance { get; set; } + + [JsonProperty("output_format")] + public string OutputFormat { get; set; } = "png"; + + [JsonProperty("webhook_url")] + public string WebhookUrl { get; set; } + + [JsonProperty("webhook_secret")] + public string WebhookSecret { get; set; } + } +} diff --git a/BFLApi/FluxPro11Request.cs b/BFLApi/FluxPro11Request.cs index 73c3bed..a5697cb 100644 --- a/BFLApi/FluxPro11Request.cs +++ b/BFLApi/FluxPro11Request.cs @@ -1,27 +1,30 @@ -using Newtonsoft.Json; - -using System.Text.Json; - -namespace BFLAPIClient -{ - public class FluxPro11Request - { - [JsonProperty("prompt")] - public string Prompt { get; set; } - - [JsonProperty("width")] - public int Width { get; set; } - - [JsonProperty("height")] - public int Height { get; set; } - - [JsonProperty("prompt_upsampling")] - public bool PromptUpsampling { get; set; } - - [JsonProperty("safety_tolerance")] - public int SafetyTolerance { get; set; } - - [JsonProperty("seed")] - public int? Seed { get; set; } - } -} +using Newtonsoft.Json; + +using System.Text.Json; + +namespace BFLAPIClient +{ + public class FluxPro11Request + { + [JsonProperty("prompt")] + public string Prompt { get; set; } + + [JsonProperty("width")] + public int Width { get; set; } + + [JsonProperty("height")] + public int Height { get; set; } + + [JsonProperty("prompt_upsampling")] + public bool PromptUpsampling { get; set; } + + [JsonProperty("safety_tolerance")] + public int SafetyTolerance { get; set; } + + [JsonProperty("output_format")] + public string OutputFormat { get; set; } = "png"; + + [JsonProperty("seed")] + public int? Seed { get; set; } + } +} diff --git a/BFLApi/FluxPro11UltraRequest.cs b/BFLApi/FluxPro11UltraRequest.cs new file mode 100644 index 0000000..22b7970 --- /dev/null +++ b/BFLApi/FluxPro11UltraRequest.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; + +using System.Text.Json; + +namespace BFLAPIClient +{ + public class FluxPro11UltraRequest + { + [JsonProperty("prompt")] + public string Prompt { get; set; } + + [JsonProperty("aspect_ratio")] + public string AspectRatio { get; set; } + + [JsonProperty("width")] + public int Width { get; set; } + + [JsonProperty("height")] + public int Height { get; set; } + + [JsonProperty("prompt_upsampling")] + public bool PromptUpsampling { get; set; } + + [JsonProperty("safety_tolerance")] + public int SafetyTolerance { get; set; } = 6; + + [JsonProperty("output_format")] + public string OutputFormat { get; set; } = "png"; + + [JsonProperty("seed")] + public int? Seed { get; set; } + } +} diff --git a/BFLApi/FluxProRequest.cs b/BFLApi/FluxProRequest.cs index 0f65d34..2ea474e 100644 --- a/BFLApi/FluxProRequest.cs +++ b/BFLApi/FluxProRequest.cs @@ -1,36 +1,36 @@ -using Newtonsoft.Json; - -using System.Text.Json; - -namespace BFLAPIClient -{ - public class FluxProRequest - { - [JsonProperty("prompt")] - public string Prompt { get; set; } - - [JsonProperty("width")] - public int Width { get; set; } - - [JsonProperty("height")] - public int Height { get; set; } - - [JsonProperty("num_steps")] - public int? NumSteps { get; set; } - - [JsonProperty("prompt_upsampling")] - public bool PromptUpsampling { get; set; } - - [JsonProperty("seed")] - public int? Seed { get; set; } - - [JsonProperty("guidance")] - public float? Guidance { get; set; } - - [JsonProperty("interval")] - public float? Interval { get; set; } - - [JsonProperty("safety_tolerance")] - public int SafetyTolerance { get; set; } - } -} +using Newtonsoft.Json; + +using System.Text.Json; + +namespace BFLAPIClient +{ + public class FluxProRequest + { + [JsonProperty("prompt")] + public string Prompt { get; set; } + + [JsonProperty("width")] + public int Width { get; set; } + + [JsonProperty("height")] + public int Height { get; set; } + + [JsonProperty("num_steps")] + public int? NumSteps { get; set; } + + [JsonProperty("prompt_upsampling")] + public bool PromptUpsampling { get; set; } + + [JsonProperty("seed")] + public int? Seed { get; set; } + + [JsonProperty("guidance")] + public float? Guidance { get; set; } + + [JsonProperty("interval")] + public float? Interval { get; set; } + + [JsonProperty("safety_tolerance")] + public int SafetyTolerance { get; set; } + } +} diff --git a/IdeogramAPI/.gitignore b/IdeogramAPI/.gitignore index 9e5fab6..a3fdcbf 100644 --- a/IdeogramAPI/.gitignore +++ b/IdeogramAPI/.gitignore @@ -1,13 +1,13 @@ -ideogram.log -ideogramSaves/** -ideogram-settings.json -bin/** -obj/** -.vs/** -.vscode/** -ideogram_pages/** -ideogram_data_all.json -**/claude-bad.txt -claude-bad.txt -**/env/** -**/env2/** \ No newline at end of file +ideogram.log +ideogramSaves/** +ideogram-settings.json +bin/** +obj/** +.vs/** +.vscode/** +ideogram_pages/** +ideogram_data_all.json +**/claude-bad.txt +claude-bad.txt +**/env/** +**/env2/** diff --git a/IdeogramAPI/IdeogramAPIClient.csproj b/IdeogramAPI/IdeogramAPIClient.csproj index 63aae7d..a9ba5ef 100644 --- a/IdeogramAPI/IdeogramAPIClient.csproj +++ b/IdeogramAPI/IdeogramAPIClient.csproj @@ -1,15 +1,27 @@ - - - - Exe - net6.0 - enable - enable - - - - - - - + + + + Library + net9.0 + 13.0 + enable + enable + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/IdeogramAPI/IdeogramClient.cs b/IdeogramAPI/IdeogramClient.cs index 4d38943..3d16c8d 100644 --- a/IdeogramAPI/IdeogramClient.cs +++ b/IdeogramAPI/IdeogramClient.cs @@ -1,46 +1,200 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using System.Collections.Generic; -using System.IO; - -namespace IdeogramAPIClient -{ - public class IdeogramClient - { - private readonly HttpClient _httpClient; - private const string BaseUrl = "https://api.ideogram.ai"; - - public IdeogramClient(string apiKey) - { - _httpClient = new HttpClient(); - _httpClient.DefaultRequestHeaders.Add("Api-Key", apiKey); - _httpClient.BaseAddress = new Uri(BaseUrl); - } - - public async Task GenerateImageAsync(IdeogramGenerateRequest request) - { - var jsonRequest = JsonConvert.SerializeObject(new { image_request = request }, new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore, - Converters = new List { new StringEnumConverter(camelCaseText: false) } - }); - - var httpContent = new StringContent(jsonRequest, System.Text.Encoding.UTF8, "application/json"); - - var response = await _httpClient.PostAsync("/generate", httpContent); - - if (!response.IsSuccessStatusCode) - { - var errorContent = await response.Content.ReadAsStringAsync(); - throw new HttpRequestException($"API request failed with status code {response.StatusCode}. Response: {errorContent}"); - } - - var content = await response.Content.ReadAsStringAsync(); - var generateResponse = JsonConvert.DeserializeObject(content); - return generateResponse; - } - } -} +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http.Headers; + +namespace IdeogramAPIClient +{ + public class IdeogramClient + { + private readonly HttpClient _httpClient; + private const string BaseUrl = "https://api.ideogram.ai"; + + public IdeogramClient(string apiKey) + { + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Add("Api-Key", apiKey); + _httpClient.BaseAddress = new Uri(BaseUrl); + } + + public async Task GenerateImageAsync(IdeogramGenerateRequest request) + { + var jsonRequest = JsonConvert.SerializeObject(new { image_request = request }, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + Converters = new List { new StringEnumConverter(camelCaseText: false) } + }); + + var httpContent = new StringContent(jsonRequest, System.Text.Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync("/generate", httpContent); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + throw new HttpRequestException($"API request failed with status code {response.StatusCode}. Response: {errorContent}"); + } + + var content = await response.Content.ReadAsStringAsync(); + var generateResponse = JsonConvert.DeserializeObject(content); + return generateResponse; + } + + public async Task GenerateImageV3Async(IdeogramV3GenerateRequest request) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + if (string.IsNullOrWhiteSpace(request.Prompt)) + throw new ArgumentException("Prompt is required for Ideogram v3 generation.", nameof(request)); + + using (var formData = new MultipartFormDataContent()) + { + formData.Add(new StringContent(request.Prompt), "prompt"); + + AddStringPart(formData, "aspect_ratio", request.AspectRatio.ToString()); + AddStringPart(formData, "rendering_speed", request.RenderingSpeed.ToString()); + AddStringPart(formData, "magic_prompt", request.MagicPrompt.ToString()); + AddStringPart(formData, "style_type", request.StyleType.ToString()); + //AddStringPart(formData, "style_preset", request.StylePreset); + //AddStringPart(formData, "negative_prompt", request.NegativePrompt); + //AddIntPart(formData, "num_images", request.NumImages); + //AddIntPart(formData, "seed", request.Seed); + + //if (request.StyleCodes != null) + //{ + // foreach (var styleCode in request.StyleCodes.Where(c => !string.IsNullOrWhiteSpace(c))) + // { + // formData.Add(new StringContent(styleCode), "style_codes"); + // } + //} + + //AddFileParts(formData, "style_reference_images", request.StyleReferenceImages); + //AddFileParts(formData, "character_reference_images", request.CharacterReferenceImages); + //AddFileParts(formData, "character_reference_images_mask", request.CharacterReferenceImageMasks); + + var response = await _httpClient.PostAsync("/v1/ideogram-v3/generate", formData); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + throw new HttpRequestException($"API request failed with status code {response.StatusCode}. Response: {errorContent}"); + } + + var content = await response.Content.ReadAsStringAsync(); + var generateResponse = JsonConvert.DeserializeObject(content); + if (generateResponse == null) + { + throw new InvalidDataException("Failed to deserialize Ideogram v3 response."); + } + + return generateResponse; + } + } + + /// Ideogram 4.0 (2026-06-03): plain JSON POST, unlike v3's multipart + /// form. 2K-native output, rendering_speed FLASH|TURBO|DEFAULT|QUALITY. + public async Task GenerateImageV4Async(IdeogramV4GenerateRequest request) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + if (string.IsNullOrWhiteSpace(request.TextPrompt)) + throw new ArgumentException("TextPrompt is required for Ideogram v4 generation.", nameof(request)); + + var jsonRequest = JsonConvert.SerializeObject(request, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + }); + var httpContent = new StringContent(jsonRequest, System.Text.Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync("/v1/ideogram-v4/generate", httpContent); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + throw new HttpRequestException($"API request failed with status code {response.StatusCode}. Response: {errorContent}"); + } + + var content = await response.Content.ReadAsStringAsync(); + var generateResponse = JsonConvert.DeserializeObject(content); + if (generateResponse == null) + { + throw new InvalidDataException("Failed to deserialize Ideogram v4 response."); + } + + return generateResponse; + } + + public async Task DescribeImageAsync(IdeogramDescribeRequest request) + { + using (var formData = new MultipartFormDataContent()) + { + var imageContent = new ByteArrayContent(request.ImageFile); + formData.Add(imageContent, "image_file", "image.png"); // Assuming image.png as a default filename + + if (!string.IsNullOrEmpty(request.DescribeModelVersion)) + { + formData.Add(new StringContent(request.DescribeModelVersion), "describe_model_version"); + } + + var response = await _httpClient.PostAsync("/describe", formData); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + throw new HttpRequestException($"API request failed with status code {response.StatusCode}. Response: {errorContent}"); + } + + var content = await response.Content.ReadAsStringAsync(); + var describeResponse = JsonConvert.DeserializeObject(content); + return describeResponse; + } + } + + private static void AddStringPart(MultipartFormDataContent formData, string name, string? value) + { + if (!string.IsNullOrWhiteSpace(value)) + { + formData.Add(new StringContent(value), name); + } + } + + private static void AddIntPart(MultipartFormDataContent formData, string name, int? value) + { + if (value.HasValue) + { + formData.Add(new StringContent(value.Value.ToString()), name); + } + } + + private static void AddEnumPart(MultipartFormDataContent formData, string name, T? value) where T : struct, Enum + { + if (value.HasValue) + { + formData.Add(new StringContent(value.Value.ToString()), name); + } + } + + private static void AddFileParts(MultipartFormDataContent formData, string fieldName, IEnumerable files) + { + if (files == null) + return; + + foreach (var file in files) + { + if (file?.Content == null || file.Content.Length == 0) + continue; + + var fileContent = new ByteArrayContent(file.Content); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType); + formData.Add(fileContent, fieldName, file.FileName); + } + } + } +} diff --git a/IdeogramAPI/IdeogramModelPricing.cs b/IdeogramAPI/IdeogramModelPricing.cs index 15b6b13..e0d341e 100644 --- a/IdeogramAPI/IdeogramModelPricing.cs +++ b/IdeogramAPI/IdeogramModelPricing.cs @@ -1,18 +1,18 @@ -using System; - -namespace IdeogramAPIClient -{ - public static class IdeogramModelPricing - { - public const decimal Ideogram2_0 = 0.08m; - public const decimal Ideogram2_0Turbo = 0.05m; - public const decimal Ideogram1_0 = 0.06m; - public const decimal Ideogram1_0Turbo = 0.02m; - public const decimal IdeogramUpscale = 0.06m; - public const decimal IdeogramDescribe = 0.01m; - public const decimal Ideogram2_0Remix = 0.08m; - public const decimal Ideogram2_0TurboRemix = 0.05m; - public const decimal Ideogram1_0Remix = 0.06m; - public const decimal Ideogram1_0TurboRemix = 0.02m; - } -} +using System; + +namespace IdeogramAPIClient +{ + public static class IdeogramModelPricing + { + public const decimal Ideogram2_0 = 0.08m; + public const decimal Ideogram2_0Turbo = 0.05m; + public const decimal Ideogram1_0 = 0.06m; + public const decimal Ideogram1_0Turbo = 0.02m; + public const decimal IdeogramUpscale = 0.06m; + public const decimal IdeogramDescribe = 0.01m; + public const decimal Ideogram2_0Remix = 0.08m; + public const decimal Ideogram2_0TurboRemix = 0.05m; + public const decimal Ideogram1_0Remix = 0.06m; + public const decimal Ideogram1_0TurboRemix = 0.02m; + } +} diff --git a/IdeogramAPI/IdeogramDetails.cs b/IdeogramAPI/IdeogramOptions.cs similarity index 65% rename from IdeogramAPI/IdeogramDetails.cs rename to IdeogramAPI/IdeogramOptions.cs index b46c30e..dd8d67b 100644 --- a/IdeogramAPI/IdeogramDetails.cs +++ b/IdeogramAPI/IdeogramOptions.cs @@ -1,21 +1,23 @@ -namespace IdeogramAPIClient -{ - public class IdeogramDetails - { - public IdeogramAspectRatio? AspectRatio { get; set; } - public IdeogramModel Model { get; set; } - public IdeogramMagicPromptOption MagicPromptOption { get; set; } - public IdeogramStyleType? StyleType { get; set; } - public string NegativePrompt { get; set; } - - public IdeogramDetails() { } - public IdeogramDetails(IdeogramDetails other) - { - AspectRatio = other.AspectRatio; - Model = other.Model; - MagicPromptOption = other.MagicPromptOption; - StyleType = other.StyleType; - NegativePrompt = other.NegativePrompt; - } - } +using MultiImageClient; + +namespace IdeogramAPIClient +{ + public class IdeogramOptions + { + public IdeogramAspectRatio? AspectRatio { get; set; } + public IdeogramModel Model { get; set; } + public IdeogramMagicPromptOption MagicPromptOption { get; set; } + public IdeogramStyleType? StyleType { get; set; } + public string NegativePrompt { get; set; } = ""; + + public IdeogramOptions() { } + public IdeogramOptions(IdeogramOptions other) + { + AspectRatio = other.AspectRatio; + Model = other.Model; + MagicPromptOption = other.MagicPromptOption; + StyleType = other.StyleType; + NegativePrompt = other.NegativePrompt; + } + } } \ No newline at end of file diff --git a/IdeogramAPI/IdeogramUtils.cs b/IdeogramAPI/IdeogramUtils.cs index 48f3044..2c424ef 100644 --- a/IdeogramAPI/IdeogramUtils.cs +++ b/IdeogramAPI/IdeogramUtils.cs @@ -1,38 +1,38 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - - -namespace IdeogramAPIClient -{ - public static class IdeogramUtils - { - public static string StringifyAspectRatio(IdeogramAspectRatio? ratio) - { - if (!ratio.HasValue) - return ""; - return ratio switch - { - IdeogramAspectRatio.ASPECT_10_16 => "10x16", - IdeogramAspectRatio.ASPECT_16_10 => "16x10", - IdeogramAspectRatio.ASPECT_9_16 => "9x16", - IdeogramAspectRatio.ASPECT_16_9 => "16x9", - IdeogramAspectRatio.ASPECT_3_2 => "3x2", - IdeogramAspectRatio.ASPECT_2_3 => "2x3", - IdeogramAspectRatio.ASPECT_4_3 => "4x3", - IdeogramAspectRatio.ASPECT_3_4 => "3x4", - IdeogramAspectRatio.ASPECT_1_1 => "1x1", - IdeogramAspectRatio.ASPECT_1_3 => "1x3", - IdeogramAspectRatio.ASPECT_3_1 => "3x1", - _ => throw new ArgumentOutOfRangeException(nameof(ratio), ratio, null), - }; - } - - - - } +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + + +namespace IdeogramAPIClient +{ + public static class IdeogramUtils + { + public static string StringifyAspectRatio(IdeogramAspectRatio? ratio) + { + if (!ratio.HasValue) + return ""; + return ratio switch + { + IdeogramAspectRatio.ASPECT_10_16 => "10x16", + IdeogramAspectRatio.ASPECT_16_10 => "16x10", + IdeogramAspectRatio.ASPECT_9_16 => "9x16", + IdeogramAspectRatio.ASPECT_16_9 => "16x9", + IdeogramAspectRatio.ASPECT_3_2 => "3x2", + IdeogramAspectRatio.ASPECT_2_3 => "2x3", + IdeogramAspectRatio.ASPECT_4_3 => "4x3", + IdeogramAspectRatio.ASPECT_3_4 => "3x4", + IdeogramAspectRatio.ASPECT_1_1 => "1x1", + IdeogramAspectRatio.ASPECT_1_3 => "1x3", + IdeogramAspectRatio.ASPECT_3_1 => "3x1", + _ => throw new ArgumentOutOfRangeException(nameof(ratio), ratio, null), + }; + } + + + + } } \ No newline at end of file diff --git a/IdeogramAPI/Program.cs b/IdeogramAPI/Program.cs deleted file mode 100644 index 107e6b1..0000000 --- a/IdeogramAPI/Program.cs +++ /dev/null @@ -1,113 +0,0 @@ -using CommandLine; -using System; -using System.Threading.Tasks; - -namespace IdeogramAPIClient -{ - public class Program - { - static async Task Main(string[] args) - { - // Hardcoded args for testing - args = new[] - { - "--api-key", "", - "--prompt", "A cute puppy playing in a garden", - "--aspect-ratio", "ASPECT_16_9", - "--model", "V_2", - "--magic-prompt", "ON", - "--negative-prompt", "cat, kitten" - }; - - try - { - var task = Parser.Default.ParseArguments(args) - .MapResult( - async (CommandLineOptions opts) => await RunAsync(opts), - _ => Task.FromResult(null) // Return null for parsing errors - ); - - var response = await task; // Wait for the task to complete - - if (response != null) - { - Console.WriteLine($"Image URL: {response.Data[0].Url}"); - Console.WriteLine($"Revised Prompt: {response.Data[0].Prompt}"); - return 0; // Success - } - else - { - Console.WriteLine("Failed to generate image or parse arguments."); - return 1; // Error - } - } - catch (Exception ex) - { - Console.Error.WriteLine($"Unhandled exception: {ex.Message}"); - return 2; // Return 2 for unhandled exceptions - } - finally - { - Console.WriteLine("Press Enter to exit..."); - Console.ReadLine(); - } - } - - private static async Task RunAsync(CommandLineOptions opts) - { - var client = new IdeogramClient(opts.ApiKey); - var ideogramDetails = new IdeogramDetails - { - AspectRatio = opts.AspectRatio, - Model = opts.Model, - MagicPromptOption = opts.MagicPromptOption, - StyleType = opts.StyleType, - NegativePrompt = opts.NegativePrompt - }; - var req = new IdeogramGenerateRequest(opts.Prompt, ideogramDetails); - - try - { - var response = await client.GenerateImageAsync(req); - if (response.Data != null && response.Data.Count > 0) - { - return response; - } - else - { - Console.WriteLine("No images generated."); - return null; - } - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return null; - } - } - } - - public class CommandLineOptions - { - [Option('k', "api-key", Required = true, HelpText = "API key for Ideogram.")] - public string ApiKey { get; set; } - - [Option('p', "prompt", Required = true, HelpText = "Prompt for image generation.")] - public string Prompt { get; set; } - - [Option('a', "aspect-ratio", Default = IdeogramAspectRatio.ASPECT_1_1, HelpText = "Aspect ratio of the generated image.")] - public IdeogramAspectRatio AspectRatio { get; set; } - - [Option('m', "model", Default = IdeogramModel.V_2, HelpText = "Model to use for image generation.")] - public IdeogramModel Model { get; set; } - - [Option("magic-prompt", Default = IdeogramMagicPromptOption.ON, HelpText = "Magic prompt option.")] - public IdeogramMagicPromptOption MagicPromptOption { get; set; } - - [Option('s', "style", Default = IdeogramStyleType.GENERAL, HelpText = "Style type for the generated image.")] - public IdeogramStyleType StyleType { get; set; } - - [Option('n', "negative-prompt", Default = null, HelpText = "Negative prompt for image generation.")] - public string NegativePrompt { get; set; } - } -} diff --git a/IdeogramAPI/request/IdeogramDescribeRequest.cs b/IdeogramAPI/request/IdeogramDescribeRequest.cs new file mode 100644 index 0000000..3c4b7b9 --- /dev/null +++ b/IdeogramAPI/request/IdeogramDescribeRequest.cs @@ -0,0 +1,12 @@ +using System; +using System.IO; +using Newtonsoft.Json; + +namespace IdeogramAPIClient +{ + public class IdeogramDescribeRequest + { + public byte[] ImageFile { get; set; } + public string DescribeModelVersion { get; set; } + } +} diff --git a/IdeogramAPI/request/IdeogramFile.cs b/IdeogramAPI/request/IdeogramFile.cs new file mode 100644 index 0000000..d02e0fa --- /dev/null +++ b/IdeogramAPI/request/IdeogramFile.cs @@ -0,0 +1,21 @@ +using System; + +namespace IdeogramAPIClient +{ + public class IdeogramFile + { + public IdeogramFile(byte[] content, string fileName, string contentType = "application/octet-stream") + { + Content = content ?? throw new ArgumentNullException(nameof(content)); + FileName = string.IsNullOrWhiteSpace(fileName) ? "file" : fileName; + ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType; + } + + public byte[] Content { get; } + + public string FileName { get; } + + public string ContentType { get; } + } +} + diff --git a/IdeogramAPI/request/IdeogramGenerateRequest.cs b/IdeogramAPI/request/IdeogramGenerateRequest.cs index 990f521..4078b7d 100644 --- a/IdeogramAPI/request/IdeogramGenerateRequest.cs +++ b/IdeogramAPI/request/IdeogramGenerateRequest.cs @@ -1,66 +1,66 @@ -using System; -using System.Collections.Generic; - -using Newtonsoft.Json; - -namespace IdeogramAPIClient -{ - public class IdeogramGenerateRequest - { - public IdeogramGenerateRequest(string prompt, IdeogramDetails ideogramDetails) - { - Prompt = prompt; - AspectRatio = ideogramDetails.AspectRatio; - Model = ideogramDetails.Model; - MagicPromptOption = ideogramDetails.MagicPromptOption; - StyleType = ideogramDetails.StyleType; - } - - /// The prompt which is actually used on ideogram. - [JsonProperty("prompt")] - public string Prompt { get; set; } - - private IdeogramAspectRatio? _aspectRatio; - - private IdeogramResolution? _resolution; - - [JsonProperty("aspect_ratio")] - public IdeogramAspectRatio? AspectRatio - { - get => _aspectRatio; - set - { - if (value.HasValue && _resolution.HasValue) - throw new InvalidOperationException("AspectRatio and Resolution cannot be used together."); - _aspectRatio = value; - } - } - - [JsonProperty("resolution")] - public IdeogramResolution? Resolution - { - get => _resolution; - set - { - if (value.HasValue && _aspectRatio.HasValue) - throw new InvalidOperationException("AspectRatio and Resolution cannot be used together."); - _resolution = value; - } - } - - [JsonProperty("model")] - public IdeogramModel? Model { get; set; } - - [JsonProperty("magic_prompt_option")] - public IdeogramMagicPromptOption? MagicPromptOption { get; set; } - - [JsonProperty("seed")] - public int? Seed { get; set; } - - [JsonProperty("style_type")] - public IdeogramStyleType? StyleType { get; set; } - - [JsonProperty("negative_prompt")] - public string NegativePrompt { get; set; } - } -} +using System; +using System.Collections.Generic; + +using Newtonsoft.Json; + +namespace IdeogramAPIClient +{ + public class IdeogramGenerateRequest + { + public IdeogramGenerateRequest(string prompt, IdeogramOptions ideogramDetails) + { + Prompt = prompt; + AspectRatio = ideogramDetails.AspectRatio; + Model = ideogramDetails.Model; + MagicPromptOption = ideogramDetails.MagicPromptOption; + StyleType = ideogramDetails.StyleType; + } + + /// The prompt which is actually used on ideogram. + [JsonProperty("prompt")] + public string Prompt { get; set; } + + private IdeogramAspectRatio? _aspectRatio; + + private IdeogramResolution? _resolution; + + [JsonProperty("aspect_ratio")] + public IdeogramAspectRatio? AspectRatio + { + get => _aspectRatio; + set + { + if (value.HasValue && _resolution.HasValue) + throw new InvalidOperationException("AspectRatio and Resolution cannot be used together."); + _aspectRatio = value; + } + } + + [JsonProperty("resolution")] + public IdeogramResolution? Resolution + { + get => _resolution; + set + { + if (value.HasValue && _aspectRatio.HasValue) + throw new InvalidOperationException("AspectRatio and Resolution cannot be used together."); + _resolution = value; + } + } + + [JsonProperty("model")] + public IdeogramModel? Model { get; set; } + + [JsonProperty("magic_prompt_option")] + public IdeogramMagicPromptOption? MagicPromptOption { get; set; } + + [JsonProperty("seed")] + public int? Seed { get; set; } + + [JsonProperty("style_type")] + public IdeogramStyleType? StyleType { get; set; } + + [JsonProperty("negative_prompt")] + public string NegativePrompt { get; set; } + } +} diff --git a/IdeogramAPI/request/IdeogramV3GenerateRequest.cs b/IdeogramAPI/request/IdeogramV3GenerateRequest.cs new file mode 100644 index 0000000..1db313e --- /dev/null +++ b/IdeogramAPI/request/IdeogramV3GenerateRequest.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + +namespace IdeogramAPIClient +{ + public class IdeogramV3GenerateRequest + { + public IdeogramV3GenerateRequest(string prompt) + { + if (string.IsNullOrWhiteSpace(prompt)) + throw new ArgumentException("Prompt is required.", nameof(prompt)); + + Prompt = prompt; + } + + public string Prompt { get; } + + public string AspectRatio { get; set; } + + + public IdeogramRenderingSpeed? RenderingSpeed { get; set; } + + public IdeogramMagicPromptOption? MagicPrompt { get; set; } + + public IdeogramV3StyleType StyleType { get; set; } + + public string? StylePreset { get; set; } + + public string? NegativePrompt { get; set; } + + public int? NumImages { get; set; } + + public int? Seed { get; set; } + + public IList StyleCodes { get; } = new List(); + + public IList StyleReferenceImages { get; } = new List(); + + public IList CharacterReferenceImages { get; } = new List(); + + public IList CharacterReferenceImageMasks { get; } = new List(); + } +} + diff --git a/IdeogramAPI/request/IdeogramV4GenerateRequest.cs b/IdeogramAPI/request/IdeogramV4GenerateRequest.cs new file mode 100644 index 0000000..b05c120 --- /dev/null +++ b/IdeogramAPI/request/IdeogramV4GenerateRequest.cs @@ -0,0 +1,43 @@ +using Newtonsoft.Json; + +namespace IdeogramAPIClient +{ + /// Request body for POST /v1/ideogram-v4/generate (Ideogram 4.0, + /// released 2026-06-03). Unlike v3 (multipart form), v4 takes a plain + /// JSON body. + /// + /// Only TextPrompt is required. We deliberately expose the simple + /// text_prompt path; the alternative structured `json_prompt` contract + /// (mutually exclusive with text_prompt, disables magic-prompt) can be + /// added later if we want compositional control. + /// + /// Docs: https://developer.ideogram.ai/api-reference/api-reference/generate-v4 + public class IdeogramV4GenerateRequest + { + public IdeogramV4GenerateRequest(string textPrompt) + { + TextPrompt = textPrompt; + } + + [JsonProperty("text_prompt")] + public string TextPrompt { get; set; } + + /// One of the documented 2K-class resolutions, e.g. "2048x2048" + /// (square, the default), "2304x1728" (4:3), "1728x2304" (3:4), + /// "2560x1440" (16:9), "1440x2560" (9:16), "2496x1664" (3:2), etc. + /// Null lets the API pick (2048x2048). + [JsonProperty("resolution", NullValueHandling = NullValueHandling.Ignore)] + public string Resolution { get; set; } + + /// FLASH | TURBO | DEFAULT | QUALITY. Null = DEFAULT. + [JsonProperty("rendering_speed", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public IdeogramRenderingSpeed? RenderingSpeed { get; set; } + + [JsonProperty("num_images", NullValueHandling = NullValueHandling.Ignore)] + public int? NumImages { get; set; } + + [JsonProperty("seed", NullValueHandling = NullValueHandling.Ignore)] + public int? Seed { get; set; } + } +} diff --git a/IdeogramAPI/request/options/IdeogramAspectRatio.cs b/IdeogramAPI/request/options/IdeogramAspectRatio.cs index 27c4a43..3102184 100644 --- a/IdeogramAPI/request/options/IdeogramAspectRatio.cs +++ b/IdeogramAPI/request/options/IdeogramAspectRatio.cs @@ -1,17 +1,17 @@ -namespace IdeogramAPIClient -{ - public enum IdeogramAspectRatio - { - ASPECT_10_16, - ASPECT_16_10, - ASPECT_9_16, - ASPECT_16_9, - ASPECT_3_2, - ASPECT_2_3, - ASPECT_4_3, - ASPECT_3_4, - ASPECT_1_1, - ASPECT_1_3, - ASPECT_3_1 - } -} +namespace IdeogramAPIClient +{ + public enum IdeogramAspectRatio + { + ASPECT_10_16, + ASPECT_16_10, + ASPECT_9_16, + ASPECT_16_9, + ASPECT_3_2, + ASPECT_2_3, + ASPECT_4_3, + ASPECT_3_4, + ASPECT_1_1, + ASPECT_1_3, + ASPECT_3_1 + } +} diff --git a/IdeogramAPI/request/options/IdeogramMagicPromptOption.cs b/IdeogramAPI/request/options/IdeogramMagicPromptOption.cs index aa1cd77..f529971 100644 --- a/IdeogramAPI/request/options/IdeogramMagicPromptOption.cs +++ b/IdeogramAPI/request/options/IdeogramMagicPromptOption.cs @@ -1,9 +1,9 @@ -namespace IdeogramAPIClient -{ - public enum IdeogramMagicPromptOption - { - AUTO, - ON, - OFF - } -} +namespace IdeogramAPIClient +{ + public enum IdeogramMagicPromptOption + { + AUTO, + ON, + OFF + } +} diff --git a/IdeogramAPI/request/options/IdeogramModel.cs b/IdeogramAPI/request/options/IdeogramModel.cs index b2d1755..f7da334 100644 --- a/IdeogramAPI/request/options/IdeogramModel.cs +++ b/IdeogramAPI/request/options/IdeogramModel.cs @@ -1,10 +1,13 @@ -namespace IdeogramAPIClient -{ - public enum IdeogramModel - { - V_1, - V_1_TURBO, - V_2, - V_2_TURBO - } -} +namespace IdeogramAPIClient +{ + public enum IdeogramModel + { + V_1, + V_1_TURBO, + V_2, + V_2_TURBO, + V_2A, + V_2A_TURBO, + // todo update to use ideogram 3! https://developer.ideogram.ai/api-reference/api-reference/generate-v3 + } +} diff --git a/IdeogramAPI/request/options/IdeogramRenderingSpeed.cs b/IdeogramAPI/request/options/IdeogramRenderingSpeed.cs new file mode 100644 index 0000000..d3b75b5 --- /dev/null +++ b/IdeogramAPI/request/options/IdeogramRenderingSpeed.cs @@ -0,0 +1,11 @@ +namespace IdeogramAPIClient +{ + public enum IdeogramRenderingSpeed + { + FLASH, + TURBO, + DEFAULT, + QUALITY + } +} + diff --git a/IdeogramAPI/request/options/IdeogramResolution.cs b/IdeogramAPI/request/options/IdeogramResolution.cs index 2344539..ca0eec0 100644 --- a/IdeogramAPI/request/options/IdeogramResolution.cs +++ b/IdeogramAPI/request/options/IdeogramResolution.cs @@ -1,84 +1,84 @@ -namespace IdeogramAPIClient -{ - public enum IdeogramResolution - { - RESOLUTION_512_1536, - RESOLUTION_576_1408, - RESOLUTION_576_1472, - RESOLUTION_576_1536, - RESOLUTION_640_1024, - RESOLUTION_640_1344, - RESOLUTION_640_1408, - RESOLUTION_640_1472, - RESOLUTION_640_1536, - RESOLUTION_704_1152, - RESOLUTION_704_1216, - RESOLUTION_704_1280, - RESOLUTION_704_1344, - RESOLUTION_704_1408, - RESOLUTION_704_1472, - RESOLUTION_720_1280, - RESOLUTION_736_1312, - RESOLUTION_768_1024, - RESOLUTION_768_1088, - RESOLUTION_768_1152, - RESOLUTION_768_1216, - RESOLUTION_768_1232, - RESOLUTION_768_1280, - RESOLUTION_768_1344, - RESOLUTION_832_960, - RESOLUTION_832_1024, - RESOLUTION_832_1088, - RESOLUTION_832_1152, - RESOLUTION_832_1216, - RESOLUTION_832_1248, - RESOLUTION_864_1152, - RESOLUTION_896_960, - RESOLUTION_896_1024, - RESOLUTION_896_1088, - RESOLUTION_896_1120, - RESOLUTION_896_1152, - RESOLUTION_960_832, - RESOLUTION_960_896, - RESOLUTION_960_1024, - RESOLUTION_960_1088, - RESOLUTION_1024_640, - RESOLUTION_1024_768, - RESOLUTION_1024_832, - RESOLUTION_1024_896, - RESOLUTION_1024_960, - RESOLUTION_1024_1024, - RESOLUTION_1088_768, - RESOLUTION_1088_832, - RESOLUTION_1088_896, - RESOLUTION_1088_960, - RESOLUTION_1120_896, - RESOLUTION_1152_704, - RESOLUTION_1152_768, - RESOLUTION_1152_832, - RESOLUTION_1152_864, - RESOLUTION_1152_896, - RESOLUTION_1216_704, - RESOLUTION_1216_768, - RESOLUTION_1216_832, - RESOLUTION_1232_768, - RESOLUTION_1248_832, - RESOLUTION_1280_704, - RESOLUTION_1280_720, - RESOLUTION_1280_768, - RESOLUTION_1280_800, - RESOLUTION_1312_736, - RESOLUTION_1344_640, - RESOLUTION_1344_704, - RESOLUTION_1344_768, - RESOLUTION_1408_576, - RESOLUTION_1408_640, - RESOLUTION_1408_704, - RESOLUTION_1472_576, - RESOLUTION_1472_640, - RESOLUTION_1472_704, - RESOLUTION_1536_512, - RESOLUTION_1536_576, - RESOLUTION_1536_640 - } -} +namespace IdeogramAPIClient +{ + public enum IdeogramResolution + { + RESOLUTION_512_1536, + RESOLUTION_576_1408, + RESOLUTION_576_1472, + RESOLUTION_576_1536, + RESOLUTION_640_1024, + RESOLUTION_640_1344, + RESOLUTION_640_1408, + RESOLUTION_640_1472, + RESOLUTION_640_1536, + RESOLUTION_704_1152, + RESOLUTION_704_1216, + RESOLUTION_704_1280, + RESOLUTION_704_1344, + RESOLUTION_704_1408, + RESOLUTION_704_1472, + RESOLUTION_720_1280, + RESOLUTION_736_1312, + RESOLUTION_768_1024, + RESOLUTION_768_1088, + RESOLUTION_768_1152, + RESOLUTION_768_1216, + RESOLUTION_768_1232, + RESOLUTION_768_1280, + RESOLUTION_768_1344, + RESOLUTION_832_960, + RESOLUTION_832_1024, + RESOLUTION_832_1088, + RESOLUTION_832_1152, + RESOLUTION_832_1216, + RESOLUTION_832_1248, + RESOLUTION_864_1152, + RESOLUTION_896_960, + RESOLUTION_896_1024, + RESOLUTION_896_1088, + RESOLUTION_896_1120, + RESOLUTION_896_1152, + RESOLUTION_960_832, + RESOLUTION_960_896, + RESOLUTION_960_1024, + RESOLUTION_960_1088, + RESOLUTION_1024_640, + RESOLUTION_1024_768, + RESOLUTION_1024_832, + RESOLUTION_1024_896, + RESOLUTION_1024_960, + RESOLUTION_1024_1024, + RESOLUTION_1088_768, + RESOLUTION_1088_832, + RESOLUTION_1088_896, + RESOLUTION_1088_960, + RESOLUTION_1120_896, + RESOLUTION_1152_704, + RESOLUTION_1152_768, + RESOLUTION_1152_832, + RESOLUTION_1152_864, + RESOLUTION_1152_896, + RESOLUTION_1216_704, + RESOLUTION_1216_768, + RESOLUTION_1216_832, + RESOLUTION_1232_768, + RESOLUTION_1248_832, + RESOLUTION_1280_704, + RESOLUTION_1280_720, + RESOLUTION_1280_768, + RESOLUTION_1280_800, + RESOLUTION_1312_736, + RESOLUTION_1344_640, + RESOLUTION_1344_704, + RESOLUTION_1344_768, + RESOLUTION_1408_576, + RESOLUTION_1408_640, + RESOLUTION_1408_704, + RESOLUTION_1472_576, + RESOLUTION_1472_640, + RESOLUTION_1472_704, + RESOLUTION_1536_512, + RESOLUTION_1536_576, + RESOLUTION_1536_640 + } +} diff --git a/IdeogramAPI/request/options/IdeogramStyleType.cs b/IdeogramAPI/request/options/IdeogramStyleType.cs index 77112b0..a050111 100644 --- a/IdeogramAPI/request/options/IdeogramStyleType.cs +++ b/IdeogramAPI/request/options/IdeogramStyleType.cs @@ -1,11 +1,11 @@ -namespace IdeogramAPIClient -{ - public enum IdeogramStyleType - { - GENERAL, - REALISTIC, - DESIGN, - RENDER_3D, - ANIME - } -} +namespace IdeogramAPIClient +{ + public enum IdeogramStyleType + { + GENERAL, + REALISTIC, + DESIGN, + RENDER_3D, + ANIME + } +} diff --git a/IdeogramAPI/request/options/IdeogramV3StyleType.cs b/IdeogramAPI/request/options/IdeogramV3StyleType.cs new file mode 100644 index 0000000..31108ca --- /dev/null +++ b/IdeogramAPI/request/options/IdeogramV3StyleType.cs @@ -0,0 +1,11 @@ +namespace IdeogramAPIClient +{ + public enum IdeogramV3StyleType + { + AUTO, + GENERAL, + REALISTIC, + DESIGN, + FICTION, + } +} diff --git a/IdeogramAPI/response/GenerateResponse.cs b/IdeogramAPI/response/GenerateResponse.cs index 31a4f79..fa74706 100644 --- a/IdeogramAPI/response/GenerateResponse.cs +++ b/IdeogramAPI/response/GenerateResponse.cs @@ -1,19 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; -using Newtonsoft.Json; -using System.IO; -using System.Net; - -namespace IdeogramAPIClient -{ - public class GenerateResponse - { - [JsonProperty("created")] - public DateTime Created { get; set; } - - [JsonProperty("data")] - public List Data { get; set; } - } -} +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using System.IO; +using System.Net; + +namespace IdeogramAPIClient +{ + public class GenerateResponse + { + [JsonProperty("created")] + public DateTime Created { get; set; } + + [JsonProperty("data")] + public List Data { get; set; } + } +} diff --git a/IdeogramAPI/response/IdeogramDescribeResponse.cs b/IdeogramAPI/response/IdeogramDescribeResponse.cs new file mode 100644 index 0000000..22bb885 --- /dev/null +++ b/IdeogramAPI/response/IdeogramDescribeResponse.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace IdeogramAPIClient +{ + public class IdeogramDescription + { + [JsonProperty("text")] + public string Text { get; set; } + } + + public class IdeogramDescribeResponse + { + [JsonProperty("descriptions")] + public List Descriptions { get; set; } + } +} diff --git a/IdeogramAPI/response/ImageObject.cs b/IdeogramAPI/response/ImageObject.cs index 82cdc8d..fb47833 100644 --- a/IdeogramAPI/response/ImageObject.cs +++ b/IdeogramAPI/response/ImageObject.cs @@ -1,23 +1,23 @@ -using Newtonsoft.Json; - -namespace IdeogramAPIClient -{ - public class ImageObject - { - [JsonProperty("url")] - public string Url { get; set; } - - [JsonProperty("prompt")] - public string Prompt { get; set; } - - [JsonProperty("resolution")] - public string Resolution { get; set; } - - [JsonProperty("is_image_safe")] - public bool IsImageSafe { get; set; } - - [JsonProperty("seed")] - public int Seed { get; set; } - } - -} +using Newtonsoft.Json; + +namespace IdeogramAPIClient +{ + public class ImageObject + { + [JsonProperty("url")] + public string Url { get; set; } + + [JsonProperty("prompt")] + public string Prompt { get; set; } + + [JsonProperty("resolution")] + public string Resolution { get; set; } + + [JsonProperty("is_image_safe")] + public bool IsImageSafe { get; set; } + + [JsonProperty("seed")] + public int Seed { get; set; } + } + +} diff --git a/IdeogramAPI/response/V3/IdeogramV3GenerateResponse.cs b/IdeogramAPI/response/V3/IdeogramV3GenerateResponse.cs new file mode 100644 index 0000000..2afbb2f --- /dev/null +++ b/IdeogramAPI/response/V3/IdeogramV3GenerateResponse.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace IdeogramAPIClient +{ + public class IdeogramV3GenerateResponse + { + [JsonProperty("created")] + public DateTime Created { get; set; } + + [JsonProperty("data")] + public List Data { get; set; } = new List(); + } +} + diff --git a/IdeogramAPI/response/V3/IdeogramV3ImageObject.cs b/IdeogramAPI/response/V3/IdeogramV3ImageObject.cs new file mode 100644 index 0000000..75fdb7a --- /dev/null +++ b/IdeogramAPI/response/V3/IdeogramV3ImageObject.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace IdeogramAPIClient +{ + public class IdeogramV3ImageObject + { + [JsonProperty("prompt")] + public string Prompt { get; set; } = string.Empty; + + [JsonProperty("resolution")] + public string Resolution { get; set; } = string.Empty; + + [JsonProperty("is_image_safe")] + public bool IsImageSafe { get; set; } + + [JsonProperty("seed")] + public int Seed { get; set; } + + [JsonProperty("url")] + public string Url { get; set; } = string.Empty; + + [JsonProperty("style_type")] + public string StyleType { get; set; } = string.Empty; + } +} + diff --git a/IdeogramAPI/response/V4/IdeogramV4GenerateResponse.cs b/IdeogramAPI/response/V4/IdeogramV4GenerateResponse.cs new file mode 100644 index 0000000..b433e04 --- /dev/null +++ b/IdeogramAPI/response/V4/IdeogramV4GenerateResponse.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace IdeogramAPIClient +{ + /// Response from POST /v1/ideogram-v4/generate. The per-image objects + /// share the v3 wire shape (url / prompt / resolution / is_image_safe / + /// seed) so we reuse IdeogramV3ImageObject. Note: in v4, `prompt` comes + /// back as the model's expanded STRUCTURED JSON prompt (a serialized + /// object), not a plain rewritten sentence. + public class IdeogramV4GenerateResponse + { + /// Always "url" for this endpoint shape. + [JsonProperty("response_type")] + public string ResponseType { get; set; } = string.Empty; + + [JsonProperty("created")] + public DateTime Created { get; set; } + + [JsonProperty("data")] + public List Data { get; set; } = new List(); + } +} diff --git a/IdeogramAPI/response/VisionResponse.cs b/IdeogramAPI/response/VisionResponse.cs index 989d2aa..fb3ef82 100644 --- a/IdeogramAPI/response/VisionResponse.cs +++ b/IdeogramAPI/response/VisionResponse.cs @@ -1,16 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; -using Newtonsoft.Json; -using System.IO; -using System.Net; - -namespace IdeogramAPIClient -{ - public class VisionResponse - { - [JsonProperty("descriptions")] - public List Descriptions { get; set; } - } +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using System.IO; +using System.Net; + +namespace IdeogramAPIClient +{ + public class VisionResponse + { + [JsonProperty("descriptions")] + public List Descriptions { get; set; } + } } \ No newline at end of file diff --git a/IdeogramHistoryExtractor b/IdeogramHistoryExtractor new file mode 160000 index 0000000..3b55e39 --- /dev/null +++ b/IdeogramHistoryExtractor @@ -0,0 +1 @@ +Subproject commit 3b55e399ec030c0d9a04a7a9dc3ea7f1c99c6f98 diff --git a/ImageGenerationClasses/ImageGenerationClasses.csproj b/ImageGenerationClasses/ImageGenerationClasses.csproj index d89d10c..58df766 100644 --- a/ImageGenerationClasses/ImageGenerationClasses.csproj +++ b/ImageGenerationClasses/ImageGenerationClasses.csproj @@ -1,26 +1,29 @@ - - - - Library - net6.0 - 10.0 - {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B} - AnyCPU;ARM64 - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - + + + + Library + net9.0 + 13.0 + {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B} + AnyCPU;ARM64 + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + \ No newline at end of file diff --git a/ImageGenerationClasses/Logger.cs b/ImageGenerationClasses/Logger.cs new file mode 100644 index 0000000..c894fe8 --- /dev/null +++ b/ImageGenerationClasses/Logger.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; + +namespace MultiImageClient +{ + /// Writes every line to both the console and a configured log file. + /// + /// Call once at startup with + /// Settings.LogFilePath before any call. The + /// stream auto-flushes so crashes (e.g. cancelled HTTP requests, + /// Ctrl+C) still leave a readable trail on disk. A + /// ProcessExit hook also flushes/closes defensively. + public static class Logger + { + private static readonly object _lock = new object(); + private static StreamWriter _logWriter; + private static string _logFilePath; + private static bool _warnedAboutUninitialized; + + public static void Initialize(string logFilePath) + { + if (string.IsNullOrWhiteSpace(logFilePath)) + { + throw new ArgumentException("logFilePath must be non-empty", nameof(logFilePath)); + } + + lock (_lock) + { + _logWriter?.Dispose(); + + var dir = Path.GetDirectoryName(logFilePath); + if (!string.IsNullOrEmpty(dir)) + { + Directory.CreateDirectory(dir); + } + + _logFilePath = logFilePath; + _logWriter = new StreamWriter(logFilePath, append: true) { AutoFlush = true }; + _logWriter.WriteLine($"{Timestamp()} --- Logger initialized, logging to {logFilePath}"); + } + + AppDomain.CurrentDomain.ProcessExit += (_, __) => + { + lock (_lock) + { + _logWriter?.Flush(); + _logWriter?.Dispose(); + _logWriter = null; + } + }; + } + + public static void Log(string message) + { + var line = $"{Timestamp()} {message}"; + Console.WriteLine(line); + + lock (_lock) + { + if (_logWriter == null) + { + if (!_warnedAboutUninitialized) + { + _warnedAboutUninitialized = true; + Console.Error.WriteLine( + "[Logger] Log() called before Initialize(); file logging disabled for this line and any prior lines."); + } + return; + } + _logWriter.WriteLine(line); + } + } + + private static string Timestamp() => DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + } +} diff --git a/ImageGenerationClasses/PromptHistoryStep.cs b/ImageGenerationClasses/PromptHistoryStep.cs index 77fd405..5696e92 100644 --- a/ImageGenerationClasses/PromptHistoryStep.cs +++ b/ImageGenerationClasses/PromptHistoryStep.cs @@ -1,36 +1,41 @@ -namespace MultiImageClient -{ - /// a step along the pipeline of generating the prompt. - /// The idea is that you generate these as you go along expanding/modifying the prompt, - /// and then at least have them to accompany the actual image output to know its history. - public class PromptHistoryStep - { - public PromptHistoryStep(string newPrompt, string humanExplanation, TransformationType transformationType) - { - Prompt = newPrompt; - Explanation = humanExplanation; - TransformationType = transformationType; - } - public PromptHistoryStep(PromptHistoryStep other) - { - Prompt = other.Prompt; - Explanation = other.Explanation; - TransformationType = other.TransformationType; - } - - /// You transformed the prompt somehow and got this new version! This is always internal; when annotating, always use the explanation. - public string Prompt { get; set; } - - /// Human description of what you atually did. For example, if you prompt a rewriter with an outer prompt + {prompt}, then put that text in, and the transformationType as Rewrite. - /// Question: should I do the replacement here? or do that live. For example, I need a step to track "figuring out the correct aspect ratio to request" e.g. with LLAMA or whatever. - public string Explanation { get; set; } - public TransformationType TransformationType { get; set; } - - //a method to make mousing over this object show the description and details: - public override string ToString() - { - return $"{TransformationType} {Explanation}"; - } - } -} - +namespace MultiImageClient +{ + /// a step along the pipeline of generating the prompt. + /// The idea is that you generate these as you go along expanding/modifying the prompt, + /// and then at least have them to accompany the actual image output to know its history. + public class PromptHistoryStep + { + public PromptHistoryStep() { } + public PromptHistoryStep(string newPrompt, string humanExplanation, TransformationType transformationType, PromptReplacementMetadata pmd = null) + { + Prompt = newPrompt; + Explanation = humanExplanation; + TransformationType = transformationType; + PromptReplacementMetadata = pmd; + } + public PromptHistoryStep(PromptHistoryStep other) + { + Prompt = other.Prompt; + Explanation = other.Explanation; + TransformationType = other.TransformationType; + PromptReplacementMetadata = other.PromptReplacementMetadata?.Copy(); + } + + public PromptReplacementMetadata PromptReplacementMetadata { get; set; } + + /// You transformed the prompt somehow and got this new version! This is always internal; when annotating, always use the explanation. + public string Prompt { get; set; } + + /// Human description of what you atually did. For example, if you prompt a rewriter with an outer prompt + {prompt}, then put that text in, and the transformationType as Rewrite. + /// Question: should I do the replacement here? or do that live. For example, I need a step to track "figuring out the correct aspect ratio to request" e.g. with LLAMA or whatever. + public string Explanation { get; set; } + public TransformationType TransformationType { get; set; } + + //a method to make mousing over this object show the description and details: + public override string ToString() + { + return $"{TransformationType} {Explanation}"; + } + } +} + diff --git a/ImageGenerationClasses/PromptReplacementMetadata.cs b/ImageGenerationClasses/PromptReplacementMetadata.cs new file mode 100644 index 0000000..078de65 --- /dev/null +++ b/ImageGenerationClasses/PromptReplacementMetadata.cs @@ -0,0 +1,14 @@ +namespace MultiImageClient +{ + public class PromptReplacementMetadata + { + public decimal ClaudeTemp { get; set; } + public PromptReplacementMetadata Copy() + { + return new PromptReplacementMetadata + { + ClaudeTemp = ClaudeTemp + }; + } + } +} diff --git a/ImageGenerationClasses/Settings.cs b/ImageGenerationClasses/Settings.cs index c63cc8a..d545bc9 100644 --- a/ImageGenerationClasses/Settings.cs +++ b/ImageGenerationClasses/Settings.cs @@ -1,64 +1,204 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace MultiImageClient -{ - public class Settings - { - public string IdeogramApiKey { get; set; } - public string OpenAIApiKey { get; set; } - public string BFLApiKey {get;set;} - public string AnthropicApiKey{ get; set; } - public string LoadPromptsFrom { get; set; } - public bool EnableLogging { get; set; } - public string LogFilePath { get; set; } - public bool SaveJsonLog { get; set; } - /// save just image.jpg or image.png etc. - - public string ImageDownloadBaseFolder { get; set; } - - /// unused yet we always do RIGHT - public string AnnotationSide { get; set; } = "bottom"; - - public static Settings LoadFromFile(string filePath) - { - - if (!File.Exists(filePath)) - { - throw new FileNotFoundException($"Settings file not found: {filePath}"); - } - - string json = File.ReadAllText(filePath); - var settings = JsonConvert.DeserializeObject(json); - settings.Validate(); - return settings; - } - - public void Validate() - { - if (string.IsNullOrWhiteSpace(IdeogramApiKey)) - { - throw new ArgumentException("IdeogramApiKey is required"); - } - - if (string.IsNullOrWhiteSpace(LogFilePath)) - { - throw new ArgumentException("LogFilePath is required"); - } - - var logDirectory = Path.GetDirectoryName(LogFilePath); - if (!string.IsNullOrEmpty(logDirectory)) - { - Directory.CreateDirectory(logDirectory); - } - - if (!Directory.Exists(ImageDownloadBaseFolder)) - { - Directory.CreateDirectory(ImageDownloadBaseFolder); - } - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace MultiImageClient +{ + + /// + /// Note: this file is for general confiruation for any use of the system. Don't put things like "preferredImageOutputType" gif/png/etc here + /// because those are things that might vary over time. This is only for global true stuff that'll be set one time and kinda done from that point. + /// If you have specific stuff like layout type horizontal/vertical etc, that should just be inlined in the way we create the generator at runtime + /// with edits in the program.cs file which is used by the person making the images. + /// + public class Settings + { + public string GoogleCloudProjectId { get; set; } + /// ffs google + public string GoogleServiceAccountKeyPath { get; set; } + + /// aka vertex + public string GoogleCloudApiKey { get; set; } + public string IdeogramApiKey { get; set; } + public string OpenAIApiKey { get; set; } + public string BFLApiKey { get; set; } + public string AnthropicApiKey { get; set; } + public string RecraftApiKey { get; set; } + + /// xAI (Grok) API key, format "xai-...". Required only when a Grok + /// image generator is active. Obtain one at https://console.x.ai/. + public string XAIGrokApiKey { get; set; } + public string GoogleGeminiApiKey { get; set; } + public string GoogleCloudLocation { get; set; } + /// List of prompt-source text files. Every listed file is read and the + /// lines are pooled together. All listed files must exist at run time; + /// missing files are a hard error. Prefer this field over the legacy + /// LoadPromptsFrom single-file setting. + public List PromptFiles { get; set; } = new List(); + + /// Legacy single-file prompt source. If set, appended to PromptFiles. + /// Kept for backward compatibility with older settings.json files. + public string LoadPromptsFrom { get; set; } + public bool EnableLogging { get; set; } + public string LogFilePath { get; set; } + public bool SaveJsonLog { get; set; } + /// save just image.jpg or image.png etc. + + public string ImageDownloadBaseFolder { get; set; } + /// unused yet we always do RIGHT + public string AnnotationSide { get; set; } = "bottom"; + + /// Optional flat-folder mirror. If non-empty, every saved raw, + /// annotated, and combined image is also copied (best-effort) into + /// this single folder so you don't have to navigate date folders to + /// grab the latest batch. Leave blank to disable; missing or + /// unreachable paths are logged and skipped, never fatal. + public string FlatImageMirrorPath { get; set; } = ""; + + /// Optional "prompts I typed by hand" capture file. When the user + /// types a free-form prompt at the interactive batch loop (anything + /// other than y/n/q), the typed text is also appended as a single + /// line to this file — handy for growing a personal prompt corpus + /// over time. Leave blank to disable; the existing JSON prompt_log + /// remains the machine-readable history regardless. Embedded + /// newlines in the typed prompt are collapsed to spaces so the file + /// is always one-prompt-per-line. + public string TypedPromptsAppendFile { get; set; } = ""; + + // Local FLUX.2 Klein / ComfyUI integration. The uncensored variant is + // workflow-driven: ComfyUI owns the model graph, and MultiImageClient + // only patches prompt/size/seed plus any explicit node input overrides. + public string LocalFlux2ComfyEndpoint { get; set; } = "http://127.0.0.1:8188"; + public bool LocalFlux2AllowRemoteEndpoint { get; set; } = false; + public string LocalFlux2WorkflowPath { get; set; } = ""; + public string LocalFlux2PositivePromptNodeId { get; set; } = ""; + public string LocalFlux2NegativePromptNodeId { get; set; } = ""; + public string LocalFlux2NegativePrompt { get; set; } = ""; + public string LocalFlux2ModelName { get; set; } = ""; + public string LocalFlux2TextEncoderName { get; set; } = ""; + public string LocalFlux2VaeName { get; set; } = ""; + public int LocalFlux2Width { get; set; } = 1024; + public int LocalFlux2Height { get; set; } = 1024; + public int LocalFlux2Steps { get; set; } = 28; + public double LocalFlux2Guidance { get; set; } = 1.0; + public int? LocalFlux2Seed { get; set; } + public int LocalFlux2TimeoutSeconds { get; set; } = 900; + public Dictionary> LocalFlux2WorkflowInputOverrides { get; set; } = + new Dictionary>(); + + public string ByteDanceArkApiKey { get; set; } = ""; + public string ByteDanceArkBaseUrl { get; set; } = "https://ark.ap-southeast.bytepluses.com/api/v3"; + public string SeedreamModel { get; set; } = "seedream-4-5-251128"; + public string SeedreamSize { get; set; } = "2K"; + public bool SeedreamWatermark { get; set; } = false; + + public string MiniMaxApiKey { get; set; } = ""; + public string HailuoModel { get; set; } = "image-01"; + public string HailuoAspectRatio { get; set; } = "1:1"; + public string HailuoResponseFormat { get; set; } = "base64"; + + public string KreaApiKey { get; set; } = ""; + public string KreaModelVariant { get; set; } = "medium"; + public string KreaAspectRatio { get; set; } = "1:1"; + public string KreaResolution { get; set; } = "1K"; + public string KreaCreativity { get; set; } = "medium"; + + public string BriaApiKey { get; set; } = ""; + public string BriaBaseUrl { get; set; } = "https://engine.prod.bria-api.com/v2"; + public string BriaAspectRatio { get; set; } = "1:1"; + public string BriaResolution { get; set; } = "1MP"; + public int BriaNumResults { get; set; } = 1; + + public string MagnificApiKey { get; set; } = ""; + public string MagnificAspectRatio { get; set; } = "square_1_1"; + public string MagnificModel { get; set; } = "realism"; + public string MagnificResolution { get; set; } = "1K"; + + public string LumaApiKey { get; set; } = ""; + public string LumaPhotonModel { get; set; } = "photon-1"; + public string LumaAspectRatio { get; set; } = "1:1"; + + public string RunwayApiKey { get; set; } = ""; + public string RunwayApiVersion { get; set; } = "2024-11-06"; + public string RunwayImageModel { get; set; } = "gen4_image"; + public string RunwayImageRatio { get; set; } = "1024:1024"; + + public string StabilityApiKey { get; set; } = ""; + public string StabilityModel { get; set; } = "sd3.5-large"; + public string StabilityAspectRatio { get; set; } = "1:1"; + public string StabilityOutputFormat { get; set; } = "png"; + public string StabilityNegativePrompt { get; set; } = ""; + + public int DirectImageApiTimeoutSeconds { get; set; } = 600; + + public static Settings LoadFromFile(string filePath) + { + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"Settings file not found: {filePath}"); + } + + string json = File.ReadAllText(filePath); + var settings = JsonConvert.DeserializeObject(json); + settings.Validate(); + Logger.Initialize(settings.LogFilePath); + Logger.Log("Current settings:"); + Logger.Log($"Image Download Base:\t{settings.ImageDownloadBaseFolder}"); + Logger.Log($"Save JSON Log:\t\t{settings.SaveJsonLog}"); + Logger.Log($"Enable Logging:\t\t{settings.EnableLogging}"); + Logger.Log($"Annotation Side:\t{settings.AnnotationSide}"); + if (!string.IsNullOrWhiteSpace(settings.FlatImageMirrorPath)) + { + Logger.Log($"Flat Mirror Path:\t{settings.FlatImageMirrorPath}"); + } + if (!string.IsNullOrWhiteSpace(settings.TypedPromptsAppendFile)) + { + Logger.Log($"Typed Prompts File:\t{settings.TypedPromptsAppendFile}"); + } + + return settings; + } + + /// Only validates things that EVERY run needs: the log file path and the + /// image download folder. Per-generator requirements (Google Cloud fields, + /// individual API keys, etc.) live in the generator that needs them. + public void Validate() + { + if (string.IsNullOrWhiteSpace(LogFilePath)) + { + throw new InvalidOperationException( + "settings.json: LogFilePath is required. Set it to a writable file path, e.g. \"C:\\\\proj\\\\multiImageClient\\\\ideogram.log\"."); + } + var logDirectory = Path.GetDirectoryName(LogFilePath); + if (!string.IsNullOrEmpty(logDirectory)) + { + try + { + Directory.CreateDirectory(logDirectory); + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"settings.json: LogFilePath='{LogFilePath}' — could not create directory '{logDirectory}': {ex.Message}. Fix the path in settings.json."); + } + } + + if (string.IsNullOrWhiteSpace(ImageDownloadBaseFolder)) + { + throw new InvalidOperationException( + "settings.json: ImageDownloadBaseFolder is required. Set it to a writable folder, e.g. \"C:\\\\proj\\\\multiImageClient\\\\saves\"."); + } + try + { + Directory.CreateDirectory(ImageDownloadBaseFolder); + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"settings.json: ImageDownloadBaseFolder='{ImageDownloadBaseFolder}' — could not create: {ex.Message}. Fix the path in settings.json."); + } + } + } +} diff --git a/ImageGenerationClasses/TransformationType.cs b/ImageGenerationClasses/TransformationType.cs index 7f2c9fb..1c5c282 100644 --- a/ImageGenerationClasses/TransformationType.cs +++ b/ImageGenerationClasses/TransformationType.cs @@ -1,31 +1,32 @@ -namespace MultiImageClient -{ - public enum TransformationType - { - InitialPrompt = 1, - Variants = 2, - InitialExpand = 3, - - - ClaudeRewrite = 20, - ClauedeRewriteRequest = 21, - ClaudeWouldRefuseRewrite = 22, - ClaudeDidRefuseRewrite = 23, - - TextToImage = 30, - ImageToText = 40, - FigureOutAR = 50, - IdeogramRewrite = 60, - BFLRewrite = 70, - LLAmARewrite = 80, - - Dalle3Rewrite = 90, - - Randomizer = 100, - - ManualSuffixation = 110, - - AddingArtistStyle = 120, - } -} - +namespace MultiImageClient +{ + public enum TransformationType + { + InitialPrompt = 1, + Variants = 2, + InitialExpand = 3, + + ClaudeRewrite = 20, + ClaudeRewriteRequest = 21, + ClaudeWouldRefuseRewrite = 22, + ClaudeDidRefuseRewrite = 23, + + TextToImage = 30, + ImageToText = 40, + FigureOutAR = 50, + IdeogramRewrite = 60, + BFLRewrite = 70, + LLAmARewrite = 80, + + Dalle3Rewrite = 90, + + Randomizer = 100, + + ManualSuffixation = 110, + + AddingArtistStyle = 120, + Imagen4Rewrite = 125, + GptImageOneRewrite = 130, + } +} + diff --git a/MultiImageClient.sln b/MultiImageClient.sln index d24b3ae..5dbfb24 100644 --- a/MultiImageClient.sln +++ b/MultiImageClient.sln @@ -1,61 +1,86 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.9.34728.123 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdeogramAPIClient", "IdeogramAPI\IdeogramAPIClient.csproj", "{3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BFLAPIClient", "BFLApi\BFLAPIClient.csproj", "{F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiImageClient", "MultiImageClient\MultiImageClient.csproj", "{0AB858B6-6624-452A-A126-9851F2552B0A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageGenerationClasses", "ImageGenerationClasses\ImageGenerationClasses.csproj", "{3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|ARM64 = Debug|ARM64 - Release|Any CPU = Release|Any CPU - Release|ARM64 = Release|ARM64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Debug|ARM64.Build.0 = Debug|ARM64 - {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Release|Any CPU.Build.0 = Release|Any CPU - {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Release|ARM64.ActiveCfg = Release|ARM64 - {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Release|ARM64.Build.0 = Release|ARM64 - {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Debug|ARM64.Build.0 = Debug|ARM64 - {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Release|Any CPU.Build.0 = Release|Any CPU - {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Release|ARM64.ActiveCfg = Release|ARM64 - {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Release|ARM64.Build.0 = Release|ARM64 - {0AB858B6-6624-452A-A126-9851F2552B0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0AB858B6-6624-452A-A126-9851F2552B0A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0AB858B6-6624-452A-A126-9851F2552B0A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {0AB858B6-6624-452A-A126-9851F2552B0A}.Debug|ARM64.Build.0 = Debug|ARM64 - {0AB858B6-6624-452A-A126-9851F2552B0A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0AB858B6-6624-452A-A126-9851F2552B0A}.Release|Any CPU.Build.0 = Release|Any CPU - {0AB858B6-6624-452A-A126-9851F2552B0A}.Release|ARM64.ActiveCfg = Release|ARM64 - {0AB858B6-6624-452A-A126-9851F2552B0A}.Release|ARM64.Build.0 = Release|ARM64 - {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Debug|ARM64.Build.0 = Debug|ARM64 - {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Release|Any CPU.Build.0 = Release|Any CPU - {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Release|ARM64.ActiveCfg = Release|ARM64 - {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Release|ARM64.Build.0 = Release|ARM64 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {9F186CA4-8488-45D0-8D14-EEB629EF46DE} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdeogramAPIClient", "IdeogramAPI\IdeogramAPIClient.csproj", "{3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BFLAPIClient", "BFLApi\BFLAPIClient.csproj", "{F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiImageClient", "MultiImageClient\MultiImageClient.csproj", "{0AB858B6-6624-452A-A126-9851F2552B0A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageGenerationClasses", "ImageGenerationClasses\ImageGenerationClasses.csproj", "{3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RecraftAPIClient", "RecraftAPI\RecraftAPIClient.csproj", "{5A29AA17-8556-4456-8FFC-E8E2AF015537}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XAIGrokAPIClient", "XAIGrokAPI\XAIGrokAPIClient.csproj", "{C6F1B6C2-3D41-4D9A-8F1E-5B7A2C3D4E5F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|ARM64 = Debug|ARM64 + Release|Any CPU = Release|Any CPU + Release|ARM64 = Release|ARM64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Debug|ARM64.Build.0 = Debug|ARM64 + {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Release|Any CPU.Build.0 = Release|Any CPU + {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Release|ARM64.ActiveCfg = Release|ARM64 + {3DFD03FC-9AAC-40A4-A883-AF9CA25D0055}.Release|ARM64.Build.0 = Release|ARM64 + {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Debug|ARM64.Build.0 = Debug|ARM64 + {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Release|Any CPU.Build.0 = Release|Any CPU + {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Release|ARM64.ActiveCfg = Release|ARM64 + {F68A7365-F772-4BD7-867B-A3E4A5D6DC5E}.Release|ARM64.Build.0 = Release|ARM64 + {0AB858B6-6624-452A-A126-9851F2552B0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0AB858B6-6624-452A-A126-9851F2552B0A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0AB858B6-6624-452A-A126-9851F2552B0A}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {0AB858B6-6624-452A-A126-9851F2552B0A}.Debug|ARM64.Build.0 = Debug|ARM64 + {0AB858B6-6624-452A-A126-9851F2552B0A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0AB858B6-6624-452A-A126-9851F2552B0A}.Release|Any CPU.Build.0 = Release|Any CPU + {0AB858B6-6624-452A-A126-9851F2552B0A}.Release|ARM64.ActiveCfg = Release|ARM64 + {0AB858B6-6624-452A-A126-9851F2552B0A}.Release|ARM64.Build.0 = Release|ARM64 + {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Debug|ARM64.Build.0 = Debug|ARM64 + {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Release|Any CPU.Build.0 = Release|Any CPU + {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Release|ARM64.ActiveCfg = Release|ARM64 + {3E7F378B-7AC6-4904-B775-E4B4CC69AB5B}.Release|ARM64.Build.0 = Release|ARM64 + {5A29AA17-8556-4456-8FFC-E8E2AF015537}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A29AA17-8556-4456-8FFC-E8E2AF015537}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A29AA17-8556-4456-8FFC-E8E2AF015537}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {5A29AA17-8556-4456-8FFC-E8E2AF015537}.Debug|ARM64.Build.0 = Debug|ARM64 + {5A29AA17-8556-4456-8FFC-E8E2AF015537}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A29AA17-8556-4456-8FFC-E8E2AF015537}.Release|Any CPU.Build.0 = Release|Any CPU + {5A29AA17-8556-4456-8FFC-E8E2AF015537}.Release|ARM64.ActiveCfg = Release|ARM64 + {5A29AA17-8556-4456-8FFC-E8E2AF015537}.Release|ARM64.Build.0 = Release|ARM64 + {C6F1B6C2-3D41-4D9A-8F1E-5B7A2C3D4E5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6F1B6C2-3D41-4D9A-8F1E-5B7A2C3D4E5F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6F1B6C2-3D41-4D9A-8F1E-5B7A2C3D4E5F}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {C6F1B6C2-3D41-4D9A-8F1E-5B7A2C3D4E5F}.Debug|ARM64.Build.0 = Debug|ARM64 + {C6F1B6C2-3D41-4D9A-8F1E-5B7A2C3D4E5F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6F1B6C2-3D41-4D9A-8F1E-5B7A2C3D4E5F}.Release|Any CPU.Build.0 = Release|Any CPU + {C6F1B6C2-3D41-4D9A-8F1E-5B7A2C3D4E5F}.Release|ARM64.ActiveCfg = Release|ARM64 + {C6F1B6C2-3D41-4D9A-8F1E-5B7A2C3D4E5F}.Release|ARM64.Build.0 = Release|ARM64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9F186CA4-8488-45D0-8D14-EEB629EF46DE} + EndGlobalSection +EndGlobal diff --git a/MultiImageClient/.editorconfig b/MultiImageClient/.editorconfig index fd70329..63ea57f 100644 --- a/MultiImageClient/.editorconfig +++ b/MultiImageClient/.editorconfig @@ -1,93 +1,96 @@ -[*.cs] - -# IDE0090: Use 'new(...)' -dotnet_diagnostic.IDE0090.severity = none -csharp_using_directive_placement = outside_namespace:silent -csharp_prefer_simple_using_statement = true:suggestion -csharp_prefer_braces = true:silent -csharp_style_namespace_declarations = block_scoped:silent -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_top_level_statements = true:silent -csharp_style_prefer_primary_constructors = true:suggestion -csharp_style_expression_bodied_methods = false:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_operators = false:silent -csharp_style_expression_bodied_properties = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_lambdas = true:silent -csharp_style_expression_bodied_local_functions = false:silent -csharp_style_throw_expression = true:suggestion -csharp_style_prefer_null_check_over_type_check = true:suggestion -csharp_prefer_simple_default_expression = true:suggestion -csharp_indent_labels = one_less_than_current -csharp_style_prefer_local_over_anonymous_function = true:suggestion -csharp_space_around_binary_operators = before_and_after - -[*.{cs,vb}] -#### Naming styles #### - -# Naming rules - -dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion -dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface -dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i - -dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.types_should_be_pascal_case.symbols = types -dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case - -dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members -dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case - -# Symbol specifications - -dotnet_naming_symbols.interface.applicable_kinds = interface -dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = - -dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum -dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = - -dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method -dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = - -# Naming styles - -dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = -dotnet_naming_style.begins_with_i.capitalization = pascal_case - -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case - -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_auto_properties = true:silent -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_prefer_simplified_boolean_expressions = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_style_prefer_conditional_expression_over_return = true:silent -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_compound_assignment = true:suggestion -dotnet_style_prefer_simplified_interpolation = true:suggestion -dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion -dotnet_style_namespace_match_folder = true:suggestion -dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 -indent_size = 4 -end_of_line = crlf +[*.cs] + +# IDE0090: Use 'new(...)' +dotnet_diagnostic.IDE0090.severity = none +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_indent_labels = one_less_than_current +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_space_around_binary_operators = before_and_after + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_namespace_match_folder = true:none +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf + +# IDE0130: Namespace does not match folder structure +dotnet_diagnostic.IDE0130.severity = none diff --git a/MultiImageClient/Describers/LocalInternVLClient.cs b/MultiImageClient/Describers/LocalInternVLClient.cs new file mode 100644 index 0000000..87fd6b2 --- /dev/null +++ b/MultiImageClient/Describers/LocalInternVLClient.cs @@ -0,0 +1,129 @@ + +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public class LocalInternVLClient : ILocalVisionModel + { + private static readonly HttpClient httpClient = new HttpClient(); + private readonly string _baseUrl; + private readonly float _temperature; + private readonly float _topP; + private readonly int _topK; + private readonly float _repetitionPenalty; + private readonly bool _doSample; + + public LocalInternVLClient( + string baseUrl = "http://127.0.0.1:11415", + float temperature = 0.8f, + float topP = 0.9f, + int topK = 50, + float repetitionPenalty = 1.1f, + bool doSample = true) + { + _baseUrl = baseUrl; + _temperature = temperature; + _topP = topP; + _topK = topK; + _repetitionPenalty = repetitionPenalty; + _doSample = doSample; + } + + public string GetModelName() => "InternVL3-1B-Pretrained"; + + public async Task DescribeImageAsync(byte[] imageBytes, string prompt, int maxTokens = 512, float temperature = 0.8f) + { + try + { + string base64Image = Convert.ToBase64String(imageBytes); + + var requestBody = new InternVLGenerateRequest + { + Image = $"data:image/png;base64,{base64Image}", + Prompt = prompt, + MaxTokens = maxTokens, + Temperature = temperature, + TopP = _topP, + TopK = _topK, + RepetitionPenalty = _repetitionPenalty, + DoSample = _doSample + }; + + var jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + string jsonBody = JsonSerializer.Serialize(requestBody, jsonOptions); + var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + + var response = await httpClient.PostAsync($"{_baseUrl}/generate", content); + var responseString = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + Logger.Log($"InternVL Flask endpoint returned error {response.StatusCode}: {responseString}"); + return string.Empty; + } + + var visionResponse = JsonSerializer.Deserialize(responseString, jsonOptions); + + return visionResponse?.Response ?? string.Empty; + } + catch (System.Net.Http.HttpRequestException hrex) + { + Logger.Log($": {hrex.Message}\r\n{hrex}"); + return string.Empty; + } + catch (Exception ex) + { + Logger.Log($"Error describing image with InternVL: {ex.Message}\r\n{ex}"); + return string.Empty; + } + } + + private class InternVLGenerateRequest + { + [JsonPropertyName("image")] + public required string Image { get; set; } + + [JsonPropertyName("prompt")] + public required string Prompt { get; set; } + + [JsonPropertyName("max_tokens")] + public int MaxTokens { get; set; } + + [JsonPropertyName("temperature")] + public float Temperature { get; set; } = 0.8f; + + [JsonPropertyName("top_p")] + public float TopP { get; set; } = 0.9f; + + [JsonPropertyName("top_k")] + public int TopK { get; set; } = 50; + + [JsonPropertyName("repetition_penalty")] + public float RepetitionPenalty { get; set; } = 1.1f; + + [JsonPropertyName("do_sample")] + public bool DoSample { get; set; } = true; + } + + private class InternVLGenerateResponse + { + [JsonPropertyName("response")] + public string Response { get; set; } = string.Empty; + + [JsonPropertyName("error")] + public string Error { get; set; } + } + } +} + + diff --git a/MultiImageClient/Describers/LocalQwenClient.cs b/MultiImageClient/Describers/LocalQwenClient.cs new file mode 100644 index 0000000..35d2fae --- /dev/null +++ b/MultiImageClient/Describers/LocalQwenClient.cs @@ -0,0 +1,158 @@ + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public class LocalQwenClient : ILocalVisionModel + { + private static readonly HttpClient httpClient = new HttpClient(); + private readonly string _baseUrl; + private readonly string _modelName; + private readonly float _temperature; + + public LocalQwenClient( + string baseUrl = "http://127.0.0.1:11434", + string modelName = "qwen2-vl:latest", + float temperature = 0.7f) + { + _baseUrl = baseUrl; + _modelName = modelName; + _temperature = temperature; + } + + public string GetModelName() => _modelName; + + public async Task DescribeImageAsync(byte[] imageBytes, string prompt, int maxTokens = 512, float temperature = 0.7f) + { + try + { + string base64Image = Convert.ToBase64String(imageBytes); + + var requestBody = new OllamaChatRequest + { + Model = _modelName, + Stream = false, + KeepAlive = "5m", + Options = new OllamaOptions + { + Temperature = temperature, + NumPredict = maxTokens + }, + Messages = new List + { + new OllamaMessage + { + Role = "user", + Content = prompt, + Images = new List { base64Image } + } + } + }; + + var jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + string jsonBody = JsonSerializer.Serialize(requestBody, jsonOptions); + var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + + var response = await httpClient.PostAsync($"{_baseUrl}/api/chat", content); + var responseString = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + Logger.Log($"Ollama endpoint returned error {response.StatusCode}: {responseString}"); + return string.Empty; + } + + var visionResponse = JsonSerializer.Deserialize(responseString, jsonOptions); + + return visionResponse?.Message?.Content ?? string.Empty; + } + catch (Exception ex) + { + Logger.Log($"Error describing image with Ollama: {ex.Message}\r\n{ex}"); + return string.Empty; + } + } + + private class OllamaChatRequest + { + [JsonPropertyName("model")] + public required string Model { get; set; } + + [JsonPropertyName("stream")] + public required bool Stream { get; set; } + + [JsonPropertyName("keep_alive")] + public required string KeepAlive { get; set; } + + [JsonPropertyName("options")] + public OllamaOptions? Options { get; set; } + + [JsonPropertyName("messages")] + public required List Messages { get; set; } = new(); + } + + private class OllamaMessage + { + [JsonPropertyName("role")] + public required string Role { get; set; } + + [JsonPropertyName("content")] + public required string Content { get; set; } + + [JsonPropertyName("images")] + public required List Images { get; set; } = new(); + } + + private class OllamaResponse + { + [JsonPropertyName("model")] + public required string Model { get; set; } + + [JsonPropertyName("created_at")] + public required DateTime CreatedAt { get; set; } + + [JsonPropertyName("message")] + public required OllamaResponseMessage Message { get; set; } = new() { Role = string.Empty, Content = string.Empty }; + + [JsonPropertyName("done")] + public required bool Done { get; set; } + } + + private class OllamaResponseMessage + { + [JsonPropertyName("role")] + public required string Role { get; set; } + + [JsonPropertyName("content")] + public required string Content { get; set; } = string.Empty; + } + + private class OllamaOptions + { + [JsonPropertyName("temperature")] + public float? Temperature { get; set; } + + [JsonPropertyName("num_predict")] + public int? NumPredict { get; set; } + + [JsonPropertyName("top_p")] + public float? TopP { get; set; } + + [JsonPropertyName("top_k")] + public int? TopK { get; set; } + } + } +} + + diff --git a/MultiImageClient/Enums/CombinationImageLayoutTypes.cs b/MultiImageClient/Enums/CombinationImageLayoutTypes.cs new file mode 100644 index 0000000..80fa1fe --- /dev/null +++ b/MultiImageClient/Enums/CombinationImageLayoutTypes.cs @@ -0,0 +1,9 @@ +namespace MultiImageClient +{ + public enum CombinedImageLayout + { + Horizontal = 1, + Square = 2, + ImageDescribeRendersHorizontally = 3, + } +} diff --git a/MultiImageClient/Enums/GeneralActionType.cs b/MultiImageClient/Enums/GeneralActionType.cs new file mode 100644 index 0000000..7ef3460 --- /dev/null +++ b/MultiImageClient/Enums/GeneralActionType.cs @@ -0,0 +1,9 @@ +namespace MultiImageClient +{ + public enum GeneralActionType + { + PromptToImageWithSteps = 1, + ImageToTextToImageWithSteps = 2, + SamePromptMultipleTargets = 3, + }; +} \ No newline at end of file diff --git a/MultiImageClient/Enums/GeneratorApiTypeExtensions.cs b/MultiImageClient/Enums/GeneratorApiTypeExtensions.cs index 8246f3e..f04038a 100644 --- a/MultiImageClient/Enums/GeneratorApiTypeExtensions.cs +++ b/MultiImageClient/Enums/GeneratorApiTypeExtensions.cs @@ -1,22 +1,56 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MultiImageClient.Enums -{ - public static class GeneratorApiTypeExtensions - { - public static string GetFileExtension(this ImageGeneratorApiType generator) - { - return generator switch - { - ImageGeneratorApiType.Ideogram => ".png", - ImageGeneratorApiType.BFL => ".jpg", - ImageGeneratorApiType.Dalle3 => ".png", - _ => throw new ArgumentException("Unknown generator type", nameof(generator)) - }; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public static class GeneratorApiTypeExtensions + { + public static string GetFileExtension(this ImageGeneratorApiType generator) + { + return generator switch + { + ImageGeneratorApiType.Ideogram => ".png", + ImageGeneratorApiType.BFLv11 => ".png", + ImageGeneratorApiType.BFLv11Ultra => ".png", + ImageGeneratorApiType.Dalle3 => ".png", + ImageGeneratorApiType.Recraft => ".png", //actually, I should use the value read from head since it sometimes shows up as .svg. + ImageGeneratorApiType.GptImage1 => ".png", + ImageGeneratorApiType.GptImage1Mini => ".png", + ImageGeneratorApiType.GptImage2 => ".png", + ImageGeneratorApiType.GoogleNanoBanana => ".png", + ImageGeneratorApiType.GoogleNanoBananaPro => ".png", + ImageGeneratorApiType.GoogleImagen4 => ".png", + ImageGeneratorApiType.IdeogramV3 => ".png", + ImageGeneratorApiType.IdeogramV4 => ".png", + ImageGeneratorApiType.BFLFlux2Pro => ".png", + ImageGeneratorApiType.BFLFlux2ProPreview => ".png", + ImageGeneratorApiType.BFLFlux2Max => ".png", + ImageGeneratorApiType.BFLFlux2Flex => ".png", + ImageGeneratorApiType.BFLFlux2Klein4b => ".png", + ImageGeneratorApiType.BFLFlux2Klein9b => ".png", + ImageGeneratorApiType.BFLFluxKontextPro => ".png", + ImageGeneratorApiType.BFLFluxKontextMax => ".png", + ImageGeneratorApiType.RecraftV4 => ".png", + ImageGeneratorApiType.RecraftV4Pro => ".png", + ImageGeneratorApiType.RecraftV41 => ".png", + ImageGeneratorApiType.RecraftV41Pro => ".png", + ImageGeneratorApiType.GrokImagine => ".png", + ImageGeneratorApiType.GrokImaginePro => ".png", + ImageGeneratorApiType.GrokImagineVideo => ".mp4", + ImageGeneratorApiType.LocalFlux2Uncensored => ".png", + ImageGeneratorApiType.ByteDanceSeedream => ".png", + ImageGeneratorApiType.MiniMaxHailuoImage => ".png", + ImageGeneratorApiType.KreaImage => ".png", + ImageGeneratorApiType.BriaImage => ".png", + ImageGeneratorApiType.MagnificMystic => ".png", + ImageGeneratorApiType.LumaPhoton => ".png", + ImageGeneratorApiType.RunwayGen4Image => ".png", + ImageGeneratorApiType.StabilityAi => ".png", + _ => throw new ArgumentException("Unknown image generator type while picking file extension:", nameof(generator)) + }; + } + } +} diff --git a/MultiImageClient/Enums/GenericImageGenerationErrorType.cs b/MultiImageClient/Enums/GenericImageGenerationErrorType.cs index 0f14262..c73977a 100644 --- a/MultiImageClient/Enums/GenericImageGenerationErrorType.cs +++ b/MultiImageClient/Enums/GenericImageGenerationErrorType.cs @@ -1,13 +1,13 @@ -namespace MultiImageClient.Enums -{ - public enum GenericImageGenerationErrorType - { - RequestModerated = 1, - ContentModerated = 2, - NoMoneyLeft = 3, - Unknown = 4, //refine this if it happens; this is just a catch-all. - NoImagesGenerated = 5, - } - - -} +namespace MultiImageClient +{ + public enum GenericImageGenerationErrorType + { + RequestModerated = 1, + ContentModerated = 2, + NoMoneyLeft = 3, + Unknown = 4, //refine this if it happens; this is just a catch-all. + NoImagesGenerated = 5, + } + + +} diff --git a/MultiImageClient/Enums/GenericTextGenerationErrorType.cs b/MultiImageClient/Enums/GenericTextGenerationErrorType.cs index 94b9a5c..e76fddf 100644 --- a/MultiImageClient/Enums/GenericTextGenerationErrorType.cs +++ b/MultiImageClient/Enums/GenericTextGenerationErrorType.cs @@ -1,16 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MultiImageClient.Enums -{ - public enum GenericTextGenerationErrorType - { - RequestModerated = 1, - ContentModerated = 2, - NoMoneyLeft = 3, - Unknown = 4, //refine this if it happens; this - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public enum GenericTextGenerationErrorType + { + RequestModerated = 1, + ContentModerated = 2, + NoMoneyLeft = 3, + Unknown = 4, //refine this if it happens; this + } +} diff --git a/MultiImageClient/Enums/ImageGeneratorApiType.cs b/MultiImageClient/Enums/ImageGeneratorApiType.cs index 67fab03..0575096 100644 --- a/MultiImageClient/Enums/ImageGeneratorApiType.cs +++ b/MultiImageClient/Enums/ImageGeneratorApiType.cs @@ -1,12 +1,78 @@ -using System; - -namespace MultiImageClient -{ - public enum ImageGeneratorApiType - { - Midjourney = 1, - Dalle3 = 2, - Ideogram = 3, - BFL = 4, - } -} \ No newline at end of file +using System; + +namespace MultiImageClient +{ + public enum ImageGeneratorApiType + { + Midjourney = 1, + Dalle3 = 2, + Ideogram = 3, + BFLv11 = 4, + Recraft = 5, + GptImage1 = 6, + BFLv11Ultra = 7, + GoogleNanoBanana = 8, + GoogleImagen4 = 9, + IdeogramV3 = 10, + GptImage1Mini = 11, + GptImage2 = 12, + + // FLUX.2 family (current BFL generation, launched 2025). Megapixel-priced. + BFLFlux2Pro = 13, + BFLFlux2Max = 14, + BFLFlux2Flex = 15, + BFLFlux2Klein4b = 16, + BFLFlux2Klein9b = 17, + + // FLUX.1 Kontext — text + image editing + BFLFluxKontextPro = 18, + BFLFluxKontextMax = 19, + + // Recraft V4 (drop-in upgrade over V3) + RecraftV4 = 20, + RecraftV4Pro = 21, + + // xAI Grok Imagine (launched 2026-01-28). Two tiers: + // GrokImagine -> grok-imagine-image ($0.02/image, 300 rpm) + // GrokImaginePro -> grok-imagine-image-pro ($0.07/image, 30 rpm) + GrokImagine = 22, + GrokImaginePro = 23, + + // xAI Grok Imagine VIDEO (grok-imagine-video). Asynchronous: start + + // poll. Produces mp4, not png — the generator saves the clip itself + // and returns a rendered "video card" for the combined grid. + GrokImagineVideo = 24, + + // Ideogram 4.0 (released 2026-06-03): POST /v1/ideogram-v4/generate, + // JSON body, 2K-native output, rendering_speed FLASH|TURBO|DEFAULT|QUALITY. + IdeogramV4 = 25, + + // Recraft V4.1 family (2026). API model strings recraftv4_1 / recraftv4_1_pro. + RecraftV41 = 26, + RecraftV41Pro = 27, + + // Google Gemini 3 Pro Image ("Nano Banana Pro") — professional tier, + // advanced reasoning, up to 4K. The flash tier (GoogleNanoBanana) + // now maps to gemini-3.1-flash-image ("Nano Banana 2"). + GoogleNanoBananaPro = 28, + + // BFL flux-2-pro-preview: where BFL lands the latest [pro] + // improvements first. Same API contract as flux-2-pro. + BFLFlux2ProPreview = 29, + + // Local ComfyUI-backed FLUX.2 Klein workflow using a user-supplied + // uncensored/ablated text encoder. No hosted API cost; requires a + // running local ComfyUI server and API-format workflow JSON. + LocalFlux2Uncensored = 30, + + // Direct first-party image APIs added after the aggregator pass. + ByteDanceSeedream = 31, + MiniMaxHailuoImage = 32, + KreaImage = 33, + BriaImage = 34, + MagnificMystic = 35, + LumaPhoton = 36, + RunwayGen4Image = 37, + StabilityAi = 38, + } +} diff --git a/MultiImageClient/Enums/OpenAIGPTImageOneQuality.cs b/MultiImageClient/Enums/OpenAIGPTImageOneQuality.cs new file mode 100644 index 0000000..3500831 --- /dev/null +++ b/MultiImageClient/Enums/OpenAIGPTImageOneQuality.cs @@ -0,0 +1,11 @@ +namespace MultiImageClient +{ + /// We only well-control the initial prompt text generation. The actual process of applying various steps, logging etc is all hardcoded in here which is not ideal. + public enum OpenAIGPTImageOneQuality + { + auto = 1, + low = 2, + medium = 3, + high = 4, + } +} diff --git a/MultiImageClient/Enums/TextGeneratorApiType.cs b/MultiImageClient/Enums/TextGeneratorApiType.cs index 1454434..31a97d8 100644 --- a/MultiImageClient/Enums/TextGeneratorApiType.cs +++ b/MultiImageClient/Enums/TextGeneratorApiType.cs @@ -1,14 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MultiImageClient -{ - public enum TextGeneratorApiType - { - Claude = 1, - LLAMA = 2, - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public enum TextGeneratorApiType + { + Claude = 1, + LLAMA = 2, + } +} diff --git a/MultiImageClient/ImageGenerators/BFLGenerator.cs b/MultiImageClient/ImageGenerators/BFLGenerator.cs new file mode 100644 index 0000000..282f984 --- /dev/null +++ b/MultiImageClient/ImageGenerators/BFLGenerator.cs @@ -0,0 +1,248 @@ +using BFLAPIClient; + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public class BFLGenerator : IImageGenerator + { + private SemaphoreSlim _bflSemaphore; + private BFLClient _bflClient; + private HttpClient _httpClient; + private MultiClientRunStats _stats; + private string _aspectRatio = "1:1"; + private bool _promptUpsampling = false; + private int _width { get; set; } + private int _height { get; set; } + private ImageGeneratorApiType _apiType { get; } + + public ImageGeneratorApiType ApiType => _apiType; + + private string _name; + + + public string GetGeneratorSpecPart() + { + if (string.IsNullOrEmpty(_name)) + { + return $"{_apiType}"; + } + else + { + return $"{_name}"; + } + } + + public string GetFilenamePart(PromptDetails pd) + { + var res = $"{_apiType}{_name}"; + var upsamplingPart = _promptUpsampling ? "_up" : ""; + switch (_apiType) + { + case ImageGeneratorApiType.BFLv11: + case ImageGeneratorApiType.BFLFlux2Pro: + case ImageGeneratorApiType.BFLFlux2ProPreview: + case ImageGeneratorApiType.BFLFlux2Max: + case ImageGeneratorApiType.BFLFlux2Flex: + case ImageGeneratorApiType.BFLFlux2Klein4b: + case ImageGeneratorApiType.BFLFlux2Klein9b: + res = $"{res}_{_height}x{_width}{upsamplingPart}"; + break; + case ImageGeneratorApiType.BFLv11Ultra: + res = $"{res}_{_aspectRatio}{upsamplingPart}"; + break; + default: + throw new Exception($"BFLGenerator: unhandled api type {_apiType}"); + } + + return res; + } + + public BFLGenerator(ImageGeneratorApiType apiType, string apiKey, int maxConcurrency, string aspectRatio, bool promptUpscaling, int width, int height, MultiClientRunStats stats, string name) + { + _apiType = apiType; + _bflClient = new BFLClient(apiKey); + _bflSemaphore = new SemaphoreSlim(maxConcurrency); + _httpClient = new HttpClient(); + + _aspectRatio = aspectRatio; + _promptUpsampling = promptUpscaling; + _width = width; + _height = height; + + _stats = stats; + _name = string.IsNullOrEmpty(name) ? "" : name; + } + public List GetRightParts() + { + var upsamplingPart = _promptUpsampling ? "prompt rewritten." : ""; + var rightsideContents = new List() { _apiType.ToString(), upsamplingPart}; + return rightsideContents; + } + + // https://docs.bfl.ai/quick_start/pricing + public decimal GetCost() + { + switch (_apiType) + { + case ImageGeneratorApiType.BFLv11: + return 0.04m; + case ImageGeneratorApiType.BFLv11Ultra: + return 0.06m; + // FLUX.2 is megapixel-priced; the numbers below are the headline + // rate at 1 MP output and will under-report for larger sizes. + case ImageGeneratorApiType.BFLFlux2Pro: + case ImageGeneratorApiType.BFLFlux2ProPreview: + return 0.03m; + case ImageGeneratorApiType.BFLFlux2Max: + return 0.07m; + case ImageGeneratorApiType.BFLFlux2Flex: + return 0.06m; + case ImageGeneratorApiType.BFLFlux2Klein4b: + return 0.014m; + case ImageGeneratorApiType.BFLFlux2Klein9b: + return 0.015m; + default: + throw new Exception($"BFLGenerator: no cost entry for {_apiType}"); + } + } + public async Task ProcessPromptAsync(IImageGenerator generator, PromptDetails promptDetails) + { + await _bflSemaphore.WaitAsync(); + + try + { + GenerationResponse generationResponse = null; + // BFL rejects safety_tolerance 6 with a 403 ("safety_tolerance > 5 + // requires authorization") on normal accounts; 5 is the max + // permissive value available to us. + const int MaxPermissiveSafetyTolerance = 5; + switch (_apiType) + { + case ImageGeneratorApiType.BFLv11: + { + var request = new FluxPro11Request + { + Prompt = promptDetails.Prompt, + Width = _width, + Height = _height, + PromptUpsampling = _promptUpsampling, + SafetyTolerance = MaxPermissiveSafetyTolerance + }; + generationResponse = await _bflClient.GenerateFluxPro11Async(request); + break; + } + case ImageGeneratorApiType.BFLv11Ultra: + { + var request = new FluxPro11UltraRequest + { + Prompt = promptDetails.Prompt, + AspectRatio = _aspectRatio, + PromptUpsampling = _promptUpsampling, + Width = _width, + Height = _height, + SafetyTolerance = MaxPermissiveSafetyTolerance + }; + generationResponse = await _bflClient.GenerateFluxPro11UltraAsync(request); + break; + } + case ImageGeneratorApiType.BFLFlux2Pro: + case ImageGeneratorApiType.BFLFlux2ProPreview: + case ImageGeneratorApiType.BFLFlux2Max: + case ImageGeneratorApiType.BFLFlux2Flex: + case ImageGeneratorApiType.BFLFlux2Klein4b: + case ImageGeneratorApiType.BFLFlux2Klein9b: + { + var request = new Flux2Request + { + Prompt = promptDetails.Prompt, + Width = _width, + Height = _height, + PromptUpsampling = _promptUpsampling, + SafetyTolerance = MaxPermissiveSafetyTolerance, + }; + // flex lets you steer denoising; keep it permissive by default. + if (_apiType == ImageGeneratorApiType.BFLFlux2Flex) + { + request.Steps = 40; + request.Guidance = 4.5f; + } + + generationResponse = _apiType switch + { + ImageGeneratorApiType.BFLFlux2Pro => await _bflClient.GenerateFlux2ProAsync(request), + ImageGeneratorApiType.BFLFlux2ProPreview => await _bflClient.GenerateFlux2ProPreviewAsync(request), + ImageGeneratorApiType.BFLFlux2Max => await _bflClient.GenerateFlux2MaxAsync(request), + ImageGeneratorApiType.BFLFlux2Flex => await _bflClient.GenerateFlux2FlexAsync(request), + ImageGeneratorApiType.BFLFlux2Klein4b => await _bflClient.GenerateFlux2Klein4bAsync(request), + ImageGeneratorApiType.BFLFlux2Klein9b => await _bflClient.GenerateFlux2Klein9bAsync(request), + _ => throw new Exception("unreachable"), + }; + break; + } + default: + throw new Exception($"BFLGenerator: unsupported api type {_apiType}"); + } + + + _stats.BFLImageGenerationRequestCount++; + + Logger.Log($"{promptDetails} From BFL ({_apiType}): '{generationResponse.Status}'"); + + if (generationResponse.Status != "Ready") + { + var baseResponse = new TaskProcessResult { IsSuccess = false, PromptDetails = promptDetails, ImageGeneratorDescription = generator.GetGeneratorSpecPart(), ImageGenerator = _apiType, ErrorMessage = generationResponse.Status }; + if (generationResponse.Status == "Content Moderated") + { + _stats.BFLImageGenerationErrorCount++; + baseResponse.GenericImageErrorType = GenericImageGenerationErrorType.ContentModerated; + return baseResponse; + } + else if (generationResponse.Status == "Request Moderated") + { + _stats.BFLImageGenerationErrorCount++; + baseResponse.GenericImageErrorType = GenericImageGenerationErrorType.RequestModerated; + return baseResponse; + + } + else + { + _stats.BFLImageGenerationErrorCount++; + baseResponse.GenericImageErrorType = GenericImageGenerationErrorType.Unknown; + return baseResponse; + } + + } + else + { + Logger.Log($"{promptDetails} BFL image generated: {generationResponse.Result.Sample}"); + _stats.BFLImageGenerationSuccessCount++; + var returnedPrompt = generationResponse.Result.Prompt?.Trim(); + if (!string.IsNullOrEmpty(returnedPrompt) && returnedPrompt != promptDetails.Prompt.Trim()) + { + // BFL rewrote the prompt. It actually happens (prompt upsampling, safety, etc.). + promptDetails.ReplacePrompt(returnedPrompt, returnedPrompt, TransformationType.BFLRewrite); + } + + var headResponse = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, generationResponse.Result.Sample)); + var contentType = headResponse.Content.Headers.ContentType?.MediaType; + return new TaskProcessResult { IsSuccess = true, Url = generationResponse.Result.Sample, ContentType = contentType, ImageGeneratorDescription = generator.GetGeneratorSpecPart(), PromptDetails = promptDetails, ImageGenerator = _apiType }; + } + + } + catch (Exception ex) + { + Logger.Log($"{promptDetails} BFL error: {ex.Message}"); + return new TaskProcessResult { IsSuccess = false, ErrorMessage = ex.Message, PromptDetails = promptDetails, ImageGeneratorDescription = generator.GetGeneratorSpecPart(), ImageGenerator = _apiType }; + } + finally + { + _bflSemaphore.Release(); + } + } + } +} diff --git a/MultiImageClient/ImageGenerators/Dalle3Generator.cs b/MultiImageClient/ImageGenerators/Dalle3Generator.cs new file mode 100644 index 0000000..b992c3b --- /dev/null +++ b/MultiImageClient/ImageGenerators/Dalle3Generator.cs @@ -0,0 +1,137 @@ +using OpenAI; +using OpenAI.Images; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public class Dalle3Generator : IImageGenerator + { + private SemaphoreSlim _dalle3Semaphore; + private ImageClient _openAIImageClient; + private GeneratedImageQuality _quality; + private GeneratedImageSize _size; + private MultiClientRunStats _stats; + private string _name; + + public ImageGeneratorApiType ApiType => ImageGeneratorApiType.Dalle3; + + public Dalle3Generator(string apiKey, int maxConcurrency, + GeneratedImageQuality quality, + GeneratedImageSize size, + MultiClientRunStats stats, string name = "") + { + var openAIClient = new OpenAIClient(apiKey); + _openAIImageClient = openAIClient.GetImageClient("dall-e-3"); + _dalle3Semaphore = new SemaphoreSlim(maxConcurrency); + _name = string.IsNullOrEmpty(name) ? "" : name; + _quality = quality; + _size = size; + _stats = stats; + } + + public string GetFilenamePart(PromptDetails pd) + { + var qualpt = ""; + if (_quality != GeneratedImageQuality.High) + { + qualpt = $"_{_quality.ToString().ToLower()}"; + } + var res = $"dalle3-{_name}{qualpt}"; + return res; + } + + public decimal GetCost() + { + var ss = _size.ToString(); + switch (ss) + { + case "1024x1024": + return 0.08m; + case "1792x1024": + return 0.12m; + case "1024x1792": + return 0.12m; + default: + throw new Exception("few"); + } + } + + public List GetRightParts() + { + var qualpt = ""; + if (_quality != GeneratedImageQuality.High) + { + qualpt = $"_{_quality.ToString().ToLower()}"; + } + var res = $"dalle3-{_name}{qualpt}"; + + var rightsideContents = new List() { "dall-e-3", _name, qualpt}; + + return rightsideContents; + } + + /// it's rather annoying that we still have to send in the original pd. we do that because here is where we notice things like "de3 modified or banned the text, etc." + public async Task ProcessPromptAsync(IImageGenerator generator, PromptDetails promptDetails) + { + await _dalle3Semaphore.WaitAsync(); + try + { + _stats.Dalle3RequestCount++; + //det.Size = GeneratedImageSize.W1024xH1024; + + var det = new ImageGenerationOptions() + { + Quality = GeneratedImageQuality.High, + Size = GeneratedImageSize.W1024xH1024 + }; + + var cappedSizePrompt = promptDetails.Prompt; + if (cappedSizePrompt.Length > 4000) + { + cappedSizePrompt = cappedSizePrompt.Substring(0, 4000); + } + + var res = await _openAIImageClient.GenerateImageAsync(cappedSizePrompt, det); + var uri = res.Value.ImageUri; + var revisedPrompt = res.Value.RevisedPrompt; + if (revisedPrompt != cappedSizePrompt) + { + promptDetails.ReplacePrompt(revisedPrompt, revisedPrompt, TransformationType.Dalle3Rewrite); + } + return new TaskProcessResult { IsSuccess = true, Url = uri.ToString(), ErrorMessage = "", PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.Dalle3, ImageGeneratorDescription = generator.GetGeneratorSpecPart() }; + } + catch (Exception ex) + { + var ms = ex.Message.Split("\r\n\r\n"); + var errorMessage = "Error."; + if (ms.Length > 1) + { + errorMessage = ms.Last(); + } + + return new TaskProcessResult { IsSuccess = false, ErrorMessage = errorMessage, PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.Dalle3, ImageGeneratorDescription = generator.GetGeneratorSpecPart() }; + } + finally + { + _dalle3Semaphore.Release(); + } + } + + public string GetGeneratorSpecPart() + { + if (string.IsNullOrEmpty(_name)) + { + return $"dall-e-3"; + } + else + { + return $"{_name}"; + } + } + } +} \ No newline at end of file diff --git a/MultiImageClient/ImageGenerators/DirectImageApiGenerator.cs b/MultiImageClient/ImageGenerators/DirectImageApiGenerator.cs new file mode 100644 index 0000000..7f4b4c8 --- /dev/null +++ b/MultiImageClient/ImageGenerators/DirectImageApiGenerator.cs @@ -0,0 +1,756 @@ +#nullable enable +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public enum DirectImageProvider + { + ByteDanceSeedream, + MiniMaxHailuo, + Krea, + Bria, + Magnific, + LumaPhoton, + Runway, + StabilityAi, + } + + public class DirectImageApiGenerator : IImageGenerator + { + private readonly DirectImageProvider _provider; + private readonly Settings _settings; + private readonly MultiClientRunStats _stats; + private readonly SemaphoreSlim _semaphore; + private readonly HttpClient _httpClient; + private readonly string _name; + + public ImageGeneratorApiType ApiType => ApiTypeFor(_provider); + + public DirectImageApiGenerator( + DirectImageProvider provider, + Settings settings, + int maxConcurrency, + MultiClientRunStats stats, + string name = "") + { + _provider = provider; + _settings = settings; + _stats = stats; + _name = name ?? string.Empty; + _semaphore = new SemaphoreSlim(Math.Max(1, maxConcurrency)); + var timeout = settings.DirectImageApiTimeoutSeconds <= 0 ? 600 : settings.DirectImageApiTimeoutSeconds; + _httpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(timeout + 30) + }; + } + + public string GetFilenamePart(PromptDetails pd) + { + var model = ModelLabel().Replace('/', '_').Replace(':', 'x'); + var size = SizeLabel().Replace(':', 'x').Replace('/', '_'); + var suffix = string.IsNullOrWhiteSpace(_name) ? "" : $"_{_name}"; + return $"{ApiType}_{model}_{size}{suffix}"; + } + + public List GetRightParts() + { + var parts = new List + { + ProviderLabel(), + ModelLabel(), + SizeLabel(), + }; + if (!string.IsNullOrWhiteSpace(_name)) + { + parts.Add(_name); + } + return parts; + } + + public string GetGeneratorSpecPart() + { + if (!string.IsNullOrWhiteSpace(_name)) + { + return _name; + } + return $"{ProviderLabel()} {ModelLabel()} {SizeLabel()}"; + } + + public decimal GetCost() + { + return _provider switch + { + DirectImageProvider.ByteDanceSeedream => 0.04m, + DirectImageProvider.MiniMaxHailuo => 0.03m, + DirectImageProvider.Krea => 0.04m, + DirectImageProvider.Bria => 0.05m, + DirectImageProvider.Magnific => 0.08m, + DirectImageProvider.LumaPhoton => _settings.LumaPhotonModel == "photon-flash-1" ? 0.004m : 0.015m, + DirectImageProvider.Runway => _settings.RunwayImageModel == "gen4_image_turbo" ? 0.02m : 0.08m, + DirectImageProvider.StabilityAi => _settings.StabilityModel.Contains("turbo", StringComparison.OrdinalIgnoreCase) ? 0.04m : 0.08m, + _ => 0m, + }; + } + + public async Task ProcessPromptAsync(IImageGenerator generator, PromptDetails promptDetails) + { + await _semaphore.WaitAsync(); + var sw = Stopwatch.StartNew(); + _stats.DirectImageGenerationRequestCount++; + + try + { + var submit = await SubmitAsync(promptDetails.Prompt ?? string.Empty); + var completed = submit; + if (submit.PollUrl != null) + { + completed = await PollAsync(submit); + } + + var urls = ExtractImageUrls(completed.Json); + var base64Images = ExtractBase64Images(completed.Json); + + if (completed.BinaryImageBytes != null && completed.BinaryImageBytes.Length > 0) + { + base64Images.Add(Convert.ToBase64String(completed.BinaryImageBytes)); + } + + if (urls.Count == 0 && base64Images.Count == 0) + { + throw new InvalidOperationException( + $"{ProviderLabel()} completed but no image URL or base64 image was found in response: {Truncate(completed.RawBody, 700)}"); + } + + _stats.DirectImageGenerationSuccessCount++; + sw.Stop(); + + if (urls.Count > 0) + { + var firstUrl = urls[0]; + var contentType = await TryGetContentTypeAsync(firstUrl); + Logger.Log($"\t<- {ProviderLabel()} OK url in {sw.ElapsedMilliseconds} ms"); + return new TaskProcessResult + { + IsSuccess = true, + Url = firstUrl, + ContentType = contentType, + PromptDetails = promptDetails, + ImageGenerator = ApiType, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + CreateTotalMs = sw.ElapsedMilliseconds, + }; + } + + Logger.Log($"\t<- {ProviderLabel()} OK {base64Images.Count} inline image(s) in {sw.ElapsedMilliseconds} ms"); + return new TaskProcessResult + { + IsSuccess = true, + Base64ImageDatas = base64Images.Select(b => new CreatedBase64Image + { + bytesBase64 = b, + newPrompt = promptDetails.Prompt, + }).ToList(), + ContentType = completed.ContentType ?? ContentTypeFromOutputFormat(), + PromptDetails = promptDetails, + ImageGenerator = ApiType, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + CreateTotalMs = sw.ElapsedMilliseconds, + }; + } + catch (Exception ex) + { + sw.Stop(); + _stats.DirectImageGenerationErrorCount++; + Logger.Log($"\t<- {ProviderLabel()} FAIL: {ex.Message}"); + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = ex.Message, + PromptDetails = promptDetails, + ImageGenerator = ApiType, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + CreateTotalMs = sw.ElapsedMilliseconds, + }; + } + finally + { + _semaphore.Release(); + } + } + + private async Task SubmitAsync(string prompt) + { + return _provider switch + { + DirectImageProvider.ByteDanceSeedream => await SubmitSeedreamAsync(prompt), + DirectImageProvider.MiniMaxHailuo => await SubmitHailuoAsync(prompt), + DirectImageProvider.Krea => await SubmitKreaAsync(prompt), + DirectImageProvider.Bria => await SubmitBriaAsync(prompt), + DirectImageProvider.Magnific => await SubmitMagnificAsync(prompt), + DirectImageProvider.LumaPhoton => await SubmitLumaAsync(prompt), + DirectImageProvider.Runway => await SubmitRunwayAsync(prompt), + DirectImageProvider.StabilityAi => await SubmitStabilityAsync(prompt), + _ => throw new ArgumentOutOfRangeException(nameof(_provider), _provider, null), + }; + } + + private async Task SubmitSeedreamAsync(string prompt) + { + Require(_settings.ByteDanceArkApiKey, "ByteDanceArkApiKey"); + var body = new JObject + { + ["model"] = _settings.SeedreamModel, + ["prompt"] = prompt, + ["size"] = _settings.SeedreamSize, + ["watermark"] = _settings.SeedreamWatermark, + ["response_format"] = "url", + }; + var url = $"{TrimSlash(_settings.ByteDanceArkBaseUrl)}/images/generations"; + var request = JsonPost(url, body); + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _settings.ByteDanceArkApiKey); + return await SendJsonAsync(request); + } + + private async Task SubmitHailuoAsync(string prompt) + { + Require(_settings.MiniMaxApiKey, "MiniMaxApiKey"); + var body = new JObject + { + ["model"] = _settings.HailuoModel, + ["prompt"] = prompt, + ["aspect_ratio"] = _settings.HailuoAspectRatio, + ["response_format"] = string.IsNullOrWhiteSpace(_settings.HailuoResponseFormat) ? "base64" : _settings.HailuoResponseFormat, + }; + var request = JsonPost("https://api.minimax.io/v1/image_generation", body); + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _settings.MiniMaxApiKey); + return await SendJsonAsync(request); + } + + private async Task SubmitKreaAsync(string prompt) + { + Require(_settings.KreaApiKey, "KreaApiKey"); + var variant = string.Equals(_settings.KreaModelVariant, "large", StringComparison.OrdinalIgnoreCase) + ? "large" + : "medium"; + var body = new JObject + { + ["prompt"] = prompt, + ["aspect_ratio"] = _settings.KreaAspectRatio, + ["resolution"] = _settings.KreaResolution, + ["creativity"] = _settings.KreaCreativity, + }; + var request = JsonPost($"https://api.krea.ai/generate/image/krea/krea-2/{variant}", body); + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _settings.KreaApiKey); + var result = await SendJsonAsync(request); + var jobId = StringAt(result.Json, "job_id") ?? StringAt(result.Json, "id"); + if (string.IsNullOrWhiteSpace(jobId)) + { + throw new InvalidOperationException($"Krea did not return job_id: {Truncate(result.RawBody, 500)}"); + } + result.PollUrl = $"https://api.krea.ai/jobs/{Uri.EscapeDataString(jobId)}"; + return result; + } + + private async Task SubmitBriaAsync(string prompt) + { + Require(_settings.BriaApiKey, "BriaApiKey"); + var body = new JObject + { + ["prompt"] = prompt, + ["aspect_ratio"] = _settings.BriaAspectRatio, + ["resolution"] = _settings.BriaResolution, + ["num_results"] = Math.Max(1, _settings.BriaNumResults), + }; + var request = JsonPost($"{TrimSlash(_settings.BriaBaseUrl)}/image/generate", body); + request.Headers.Add("api_token", _settings.BriaApiKey); + request.Headers.UserAgent.ParseAdd("MultiImageClient/1.0"); + var result = await SendJsonAsync(request); + result.PollUrl = StringAt(result.Json, "status_url"); + return result; + } + + private async Task SubmitMagnificAsync(string prompt) + { + Require(_settings.MagnificApiKey, "MagnificApiKey"); + var body = new JObject + { + ["prompt"] = prompt, + ["aspect_ratio"] = _settings.MagnificAspectRatio, + ["model"] = _settings.MagnificModel, + ["resolution"] = _settings.MagnificResolution, + }; + var request = JsonPost("https://api.magnific.com/v1/ai/mystic", body); + request.Headers.Add("x-magnific-api-key", _settings.MagnificApiKey); + var result = await SendJsonAsync(request); + var taskId = StringAt(result.Json, "task_id") ?? StringAt(result.Json, "taskId") ?? StringAt(result.Json, "id"); + if (!string.IsNullOrWhiteSpace(taskId)) + { + result.PollUrl = $"https://api.magnific.com/v1/ai/mystic/{Uri.EscapeDataString(taskId)}"; + } + return result; + } + + private async Task SubmitLumaAsync(string prompt) + { + Require(_settings.LumaApiKey, "LumaApiKey"); + var body = new JObject + { + ["prompt"] = prompt, + ["model"] = _settings.LumaPhotonModel, + ["aspect_ratio"] = _settings.LumaAspectRatio, + }; + var request = JsonPost("https://api.lumalabs.ai/dream-machine/v1/generations/image", body); + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _settings.LumaApiKey); + var result = await SendJsonAsync(request); + var id = StringAt(result.Json, "id"); + if (string.IsNullOrWhiteSpace(id)) + { + throw new InvalidOperationException($"Luma did not return id: {Truncate(result.RawBody, 500)}"); + } + result.PollUrl = $"https://api.lumalabs.ai/dream-machine/v1/generations/{Uri.EscapeDataString(id)}"; + return result; + } + + private async Task SubmitRunwayAsync(string prompt) + { + Require(_settings.RunwayApiKey, "RunwayApiKey"); + var body = new JObject + { + ["model"] = _settings.RunwayImageModel, + ["promptText"] = prompt, + ["ratio"] = _settings.RunwayImageRatio, + }; + var request = JsonPost("https://api.dev.runwayml.com/v1/text_to_image", body); + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _settings.RunwayApiKey); + request.Headers.Add("X-Runway-Version", _settings.RunwayApiVersion); + var result = await SendJsonAsync(request); + var id = StringAt(result.Json, "id"); + if (string.IsNullOrWhiteSpace(id)) + { + throw new InvalidOperationException($"Runway did not return id: {Truncate(result.RawBody, 500)}"); + } + result.PollUrl = $"https://api.dev.runwayml.com/v1/tasks/{Uri.EscapeDataString(id)}"; + return result; + } + + private async Task SubmitStabilityAsync(string prompt) + { + Require(_settings.StabilityApiKey, "StabilityApiKey"); + var form = new MultipartFormDataContent + { + { new StringContent(prompt), "prompt" }, + { new StringContent("text-to-image"), "mode" }, + { new StringContent(_settings.StabilityModel), "model" }, + { new StringContent(_settings.StabilityAspectRatio), "aspect_ratio" }, + { new StringContent(_settings.StabilityOutputFormat), "output_format" }, + }; + if (!string.IsNullOrWhiteSpace(_settings.StabilityNegativePrompt)) + { + form.Add(new StringContent(_settings.StabilityNegativePrompt), "negative_prompt"); + } + + var request = new HttpRequestMessage(HttpMethod.Post, "https://api.stability.ai/v2beta/stable-image/generate/sd3") + { + Content = form + }; + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _settings.StabilityApiKey); + request.Headers.Accept.ParseAdd("image/*"); + + using var response = await _httpClient.SendAsync(request); + var bytes = await response.Content.ReadAsByteArrayAsync(); + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"Stability AI failed {(int)response.StatusCode} {response.ReasonPhrase}: {BytesToErrorString(bytes)}"); + } + + var contentType = response.Content.Headers.ContentType?.MediaType ?? ContentTypeFromOutputFormat(); + return new ProviderResult + { + Json = new JObject(), + RawBody = $"", + BinaryImageBytes = bytes, + ContentType = contentType, + }; + } + + private async Task PollAsync(ProviderResult submit) + { + var deadline = DateTime.UtcNow.AddSeconds(_settings.DirectImageApiTimeoutSeconds <= 0 ? 600 : _settings.DirectImageApiTimeoutSeconds); + var delay = TimeSpan.FromSeconds(2); + var pollUrl = submit.PollUrl!; + + while (DateTime.UtcNow < deadline) + { + var request = new HttpRequestMessage(HttpMethod.Get, pollUrl); + AddPollingHeaders(request); + var current = await SendJsonAsync(request); + var state = PollState(current.Json); + if (state == ProviderPollState.Completed) + { + return current; + } + if (state == ProviderPollState.Failed) + { + throw new InvalidOperationException($"{ProviderLabel()} generation failed: {Truncate(current.RawBody, 700)}"); + } + + await Task.Delay(delay); + } + + throw new TimeoutException($"{ProviderLabel()} did not complete within {_settings.DirectImageApiTimeoutSeconds} seconds."); + } + + private void AddPollingHeaders(HttpRequestMessage request) + { + switch (_provider) + { + case DirectImageProvider.Krea: + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _settings.KreaApiKey); + break; + case DirectImageProvider.Bria: + request.Headers.Add("api_token", _settings.BriaApiKey); + request.Headers.UserAgent.ParseAdd("MultiImageClient/1.0"); + break; + case DirectImageProvider.Magnific: + request.Headers.Add("x-magnific-api-key", _settings.MagnificApiKey); + break; + case DirectImageProvider.LumaPhoton: + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _settings.LumaApiKey); + break; + case DirectImageProvider.Runway: + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _settings.RunwayApiKey); + request.Headers.Add("X-Runway-Version", _settings.RunwayApiVersion); + break; + } + } + + private ProviderPollState PollState(JObject json) + { + var status = StringAt(json, "status") ?? StringAt(json, "state"); + if (string.IsNullOrWhiteSpace(status)) + { + return HasImage(json) ? ProviderPollState.Completed : ProviderPollState.Pending; + } + + status = status.Trim(); + if (status.Equals("completed", StringComparison.OrdinalIgnoreCase) || + status.Equals("complete", StringComparison.OrdinalIgnoreCase) || + status.Equals("succeeded", StringComparison.OrdinalIgnoreCase) || + status.Equals("success", StringComparison.OrdinalIgnoreCase) || + status.Equals("done", StringComparison.OrdinalIgnoreCase) || + status.Equals("SUCCEEDED", StringComparison.OrdinalIgnoreCase)) + { + return ProviderPollState.Completed; + } + + if (status.Equals("failed", StringComparison.OrdinalIgnoreCase) || + status.Equals("failure", StringComparison.OrdinalIgnoreCase) || + status.Equals("cancelled", StringComparison.OrdinalIgnoreCase) || + status.Equals("canceled", StringComparison.OrdinalIgnoreCase) || + status.Equals("FAILED", StringComparison.OrdinalIgnoreCase)) + { + return ProviderPollState.Failed; + } + + return ProviderPollState.Pending; + } + + private async Task SendJsonAsync(HttpRequestMessage request) + { + using var response = await _httpClient.SendAsync(request); + var text = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"{ProviderLabel()} failed {(int)response.StatusCode} {response.ReasonPhrase}: {Truncate(text, 1000)}"); + } + var json = string.IsNullOrWhiteSpace(text) ? new JObject() : JObject.Parse(text); + return new ProviderResult + { + Json = json, + RawBody = text, + ContentType = response.Content.Headers.ContentType?.MediaType, + }; + } + + private static HttpRequestMessage JsonPost(string url, JObject body) + { + return new HttpRequestMessage(HttpMethod.Post, url) + { + Content = new StringContent( + JsonConvert.SerializeObject(body, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }), + Encoding.UTF8, + "application/json") + }; + } + + private List ExtractImageUrls(JObject json) + { + var urls = new List(); + + if (_provider == DirectImageProvider.Krea && json["result"]?["urls"] is JArray kreaUrls) + { + urls.AddRange(kreaUrls.Values().Where(IsHttpUrl)); + } + if (_provider == DirectImageProvider.LumaPhoton) + { + var luma = json["assets"]?["image"]?.Value(); + if (IsHttpUrl(luma)) urls.Add(luma!); + } + if (_provider == DirectImageProvider.Runway && json["output"] is JArray output) + { + urls.AddRange(output.Values().Where(IsHttpUrl)); + } + + foreach (var key in new[] { "url", "image_url", "imageUrl", "imageURL", "image" }) + { + urls.AddRange(FindStringsByKey(json, key).Where(IsHttpUrl)); + } + + return urls.Distinct().ToList(); + } + + private static List ExtractBase64Images(JObject json) + { + var images = new List(); + foreach (var key in new[] { "b64_json", "image_base64", "base64", "imageBase64", "image_base64_data" }) + { + images.AddRange(FindStringsByKey(json, key).Where(LooksLikeBase64Image).Select(StripDataUriPrefix)); + } + + if (json["data"]?["image_base64"] is JArray minimaxArray) + { + images.AddRange(minimaxArray.Values().Where(LooksLikeBase64Image).Select(StripDataUriPrefix)); + } + if (json["data"]?["image_base64"] is JValue minimaxSingle) + { + var s = minimaxSingle.Value(); + if (LooksLikeBase64Image(s)) images.Add(StripDataUriPrefix(s!)); + } + + return images.Distinct().ToList(); + } + + private static IEnumerable FindStringsByKey(JToken token, string key) + { + if (token is JObject obj) + { + foreach (var prop in obj.Properties()) + { + if (prop.Name.Equals(key, StringComparison.OrdinalIgnoreCase)) + { + if (prop.Value.Type == JTokenType.String) + { + var value = prop.Value.Value(); + if (!string.IsNullOrWhiteSpace(value)) yield return value!; + } + else if (prop.Value is JArray arr) + { + foreach (var value in arr.Values()) + { + if (!string.IsNullOrWhiteSpace(value)) yield return value!; + } + } + } + + foreach (var nested in FindStringsByKey(prop.Value, key)) + { + yield return nested; + } + } + } + else if (token is JArray arr) + { + foreach (var child in arr) + { + foreach (var nested in FindStringsByKey(child, key)) + { + yield return nested; + } + } + } + } + + private bool HasImage(JObject json) + { + return ExtractImageUrls(json).Count > 0 || ExtractBase64Images(json).Count > 0; + } + + private async Task TryGetContentTypeAsync(string url) + { + try + { + using var response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, url)); + return response.Content.Headers.ContentType?.MediaType; + } + catch + { + return null; + } + } + + private string ProviderLabel() + { + return _provider switch + { + DirectImageProvider.ByteDanceSeedream => "ByteDance Seedream", + DirectImageProvider.MiniMaxHailuo => "MiniMax Hailuo", + DirectImageProvider.Krea => "Krea", + DirectImageProvider.Bria => "BRIA", + DirectImageProvider.Magnific => "Magnific Mystic", + DirectImageProvider.LumaPhoton => "Luma Photon", + DirectImageProvider.Runway => "Runway Gen-4 Image", + DirectImageProvider.StabilityAi => "Stability AI", + _ => _provider.ToString(), + }; + } + + private string ModelLabel() + { + return _provider switch + { + DirectImageProvider.ByteDanceSeedream => _settings.SeedreamModel, + DirectImageProvider.MiniMaxHailuo => _settings.HailuoModel, + DirectImageProvider.Krea => $"krea-2-{_settings.KreaModelVariant}", + DirectImageProvider.Bria => "bria-fibo", + DirectImageProvider.Magnific => $"mystic-{_settings.MagnificModel}", + DirectImageProvider.LumaPhoton => _settings.LumaPhotonModel, + DirectImageProvider.Runway => _settings.RunwayImageModel, + DirectImageProvider.StabilityAi => _settings.StabilityModel, + _ => _provider.ToString(), + }; + } + + private string SizeLabel() + { + return _provider switch + { + DirectImageProvider.ByteDanceSeedream => _settings.SeedreamSize, + DirectImageProvider.MiniMaxHailuo => _settings.HailuoAspectRatio, + DirectImageProvider.Krea => $"{_settings.KreaAspectRatio} {_settings.KreaResolution}", + DirectImageProvider.Bria => $"{_settings.BriaAspectRatio} {_settings.BriaResolution}", + DirectImageProvider.Magnific => $"{_settings.MagnificAspectRatio} {_settings.MagnificResolution}", + DirectImageProvider.LumaPhoton => _settings.LumaAspectRatio, + DirectImageProvider.Runway => _settings.RunwayImageRatio, + DirectImageProvider.StabilityAi => _settings.StabilityAspectRatio, + _ => "", + }; + } + + private string ContentTypeFromOutputFormat() + { + var format = _provider == DirectImageProvider.StabilityAi ? _settings.StabilityOutputFormat : "png"; + return format?.ToLowerInvariant() switch + { + "jpg" => "image/jpeg", + "jpeg" => "image/jpeg", + "webp" => "image/webp", + _ => "image/png", + }; + } + + private static ImageGeneratorApiType ApiTypeFor(DirectImageProvider provider) + { + return provider switch + { + DirectImageProvider.ByteDanceSeedream => ImageGeneratorApiType.ByteDanceSeedream, + DirectImageProvider.MiniMaxHailuo => ImageGeneratorApiType.MiniMaxHailuoImage, + DirectImageProvider.Krea => ImageGeneratorApiType.KreaImage, + DirectImageProvider.Bria => ImageGeneratorApiType.BriaImage, + DirectImageProvider.Magnific => ImageGeneratorApiType.MagnificMystic, + DirectImageProvider.LumaPhoton => ImageGeneratorApiType.LumaPhoton, + DirectImageProvider.Runway => ImageGeneratorApiType.RunwayGen4Image, + DirectImageProvider.StabilityAi => ImageGeneratorApiType.StabilityAi, + _ => throw new ArgumentOutOfRangeException(nameof(provider), provider, null), + }; + } + + private static string? StringAt(JObject json, string key) + { + return FindStringsByKey(json, key).FirstOrDefault(); + } + + private static bool IsHttpUrl(string? value) + { + return !string.IsNullOrWhiteSpace(value) + && (value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + || value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)); + } + + private static bool LooksLikeBase64Image(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return false; + if (value.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase)) return true; + return value.Length > 100 && !value.Contains("://") && value.All(c => + char.IsLetterOrDigit(c) || c == '+' || c == '/' || c == '=' || c == '\r' || c == '\n'); + } + + private static string StripDataUriPrefix(string value) + { + var comma = value.IndexOf(','); + if (value.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase) && comma >= 0) + { + return value.Substring(comma + 1); + } + return value; + } + + private static string TrimSlash(string value) + { + return (value ?? string.Empty).Trim().TrimEnd('/'); + } + + private static void Require(string value, string settingName) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidOperationException($"Settings.{settingName} is not set."); + } + } + + private static string Truncate(string? value, int max) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + return value.Length <= max ? value : value.Substring(0, max) + "..."; + } + + private static string BytesToErrorString(byte[] bytes) + { + if (bytes.Length == 0) return ""; + try + { + return Truncate(Encoding.UTF8.GetString(bytes), 1000); + } + catch + { + return $""; + } + } + + private enum ProviderPollState + { + Pending, + Completed, + Failed, + } + + private class ProviderResult + { + public JObject Json { get; set; } = new JObject(); + public string RawBody { get; set; } = ""; + public string? PollUrl { get; set; } + public byte[]? BinaryImageBytes { get; set; } + public string? ContentType { get; set; } + } + } +} diff --git a/MultiImageClient/ImageGenerators/GoogleGenerator.cs b/MultiImageClient/ImageGenerators/GoogleGenerator.cs new file mode 100644 index 0000000..c7229bd --- /dev/null +++ b/MultiImageClient/ImageGenerators/GoogleGenerator.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public class GoogleGenerator : IImageGenerator + { + private SemaphoreSlim _googleSemaphore; + private HttpClient _httpClient; + private string _apiKey; + private MultiClientRunStats _stats; + private string _name; + private ImageGeneratorApiType _apiType; + private string _aspectRatio; + private string _imageSize; + + public ImageGeneratorApiType ApiType => _apiType; + + /// Gemini image-model slug per tier. June 2026 status: all dedicated + /// Imagen endpoints shut down 2026-06-24..30; Gemini Image ("Nano + /// Banana") models are Google's official replacement. + /// GoogleNanoBanana -> gemini-3.1-flash-image ("Nano Banana 2", + /// fast/cheap tier, successor to 2.5-flash-image) + /// GoogleNanoBananaPro -> gemini-3-pro-image ("Nano Banana Pro", + /// reasoning/"thinking" tier, hi-fi text, up to 4K) + private static string ModelFor(ImageGeneratorApiType apiType) => apiType switch + { + ImageGeneratorApiType.GoogleNanoBananaPro => "gemini-3-pro-image", + _ => "gemini-3.1-flash-image", + }; + + /// aspectRatio: "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", + /// "9:16", "16:9", "21:9" — or null to let the model decide (1:1 default). + /// imageSize: "512" (3.1-flash only), "1K", "2K", "4K" (K must be + /// uppercase) — or null for the API default (1K). Per the token + /// table, 2K costs the same tokens as 1K; only 4K is pricier. + public GoogleGenerator(ImageGeneratorApiType apiType, string apiKey, int maxConcurrency, + MultiClientRunStats stats, string name = "", + string aspectRatio = null, string imageSize = null) + { + if (apiType != ImageGeneratorApiType.GoogleNanoBanana && apiType != ImageGeneratorApiType.GoogleNanoBananaPro) + { + throw new ArgumentException( + $"GoogleGenerator only supports GoogleNanoBanana or GoogleNanoBananaPro, got {apiType}.", + nameof(apiType)); + } + _apiKey = apiKey; + _googleSemaphore = new SemaphoreSlim(maxConcurrency); + _httpClient = new HttpClient(); + _name = string.IsNullOrEmpty(name) ? "" : name; + _stats = stats; + _apiType = apiType; + _aspectRatio = aspectRatio; + _imageSize = imageSize; + } + + public string GetFilenamePart(PromptDetails pd) + { + return $"{_apiType}"; + } + + public decimal GetCost() + { + // Gemini image models use token-based pricing ($30/1M output tokens). + // Per the docs' token table, 1K and 2K outputs both cost ~1120 + // tokens; 4K costs ~2000. Pro additionally burns "thinking" tokens; + // ~$0.13/image is a reasonable 1K/2K estimate until we wire usage + // parsing. + var sizeMultiplier = _imageSize == "4K" ? (2000m / 1120m) : 1m; + if (_apiType == ImageGeneratorApiType.GoogleNanoBanana) + { + return (30m / 1000000m) * 1120m * sizeMultiplier; + } + else if (_apiType == ImageGeneratorApiType.GoogleNanoBananaPro) + { + return 0.13m * sizeMultiplier; + } + else + { + throw new Exception("E"); + } + } + + public List GetRightParts() + { + var parts = new List { _apiType.ToString() }; + if (!string.IsNullOrEmpty(_aspectRatio)) + { + parts.Add(_aspectRatio); + } + if (!string.IsNullOrEmpty(_imageSize)) + { + parts.Add(_imageSize); + } + return parts; + } + + public string GetGeneratorSpecPart() + { + if (string.IsNullOrEmpty(_name)) + { + return $"google-{_apiType.ToString()}"; + } + else + { + return _name; + } + } + + public async Task ProcessPromptAsync(IImageGenerator generator, PromptDetails promptDetails) + { + await _googleSemaphore.WaitAsync(); + try + { + _stats.GoogleRequestCount++; + + // Google Gemini API endpoint for native image generation (Nano Banana). + // Model slug per tier — see ModelFor(). The old + // gemini-2.5-flash-image-preview slug was retired with the + // June 2026 Imagen/preview shutdown wave. + var apiUrl = $"https://generativelanguage.googleapis.com/v1beta/models/{ModelFor(_apiType)}:generateContent"; + + var generationConfig = new Dictionary + { + ["responseModalities"] = new[] { "TEXT", "IMAGE" } + }; + var imageConfig = new Dictionary(); + if (!string.IsNullOrEmpty(_aspectRatio)) + { + imageConfig["aspectRatio"] = _aspectRatio; + } + if (!string.IsNullOrEmpty(_imageSize)) + { + imageConfig["imageSize"] = _imageSize; + } + if (imageConfig.Count > 0) + { + generationConfig["imageConfig"] = imageConfig; + } + + var requestBody = new + { + contents = new[] + { + new + { + parts = new[] + { + new { text = promptDetails.Prompt } + } + } + }, + generationConfig + }; + + var json = JsonSerializer.Serialize(requestBody); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + // Set API key in header as required by Gemini API + var request = new HttpRequestMessage(HttpMethod.Post, apiUrl) + { + Content = content + }; + request.Headers.Add("x-goog-api-key", _apiKey); + + + var response = await _httpClient.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + var errorMessage = $"Google Gemini API error: {response.StatusCode} - {responseContent}"; + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = errorMessage, + PromptDetails = promptDetails, + ImageGenerator = GetImageGeneratorType(), + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + + // Parse Gemini native image generation response + var responseData = JsonSerializer.Deserialize(responseContent); + + if (responseData?.candidates?.Length > 0) + { + var base64Images = new List(); + // Gemini image models typically return image/jpeg; trust the + // declared mime type so downstream conversion-to-png triggers. + string contentType = null; + + foreach (var candidate in responseData.candidates) + { + if (candidate?.content?.parts != null) + { + foreach (var part in candidate.content.parts) + { + // Check for image data in inline_data + if (part.inlineData != null && !string.IsNullOrEmpty(part.inlineData.data)) + { + var bd = new CreatedBase64Image + { + bytesBase64 = part.inlineData.data, + newPrompt = promptDetails.Prompt + }; + base64Images.Add(bd); + contentType ??= part.inlineData.mimeType; + } + // Log any text responses for debugging + else if (!string.IsNullOrEmpty(part.text)) + { + Console.WriteLine($"Gemini text response: {part.text}"); + //throw new Exception("qq"); + } + } + } + } + + if (base64Images.Count > 0) + { + return new TaskProcessResult + { + IsSuccess = true, + Base64ImageDatas = base64Images, + ContentType = contentType ?? "image/png", + ErrorMessage = "", + PromptDetails = promptDetails, + ImageGenerator = GetImageGeneratorType(), + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + } + + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = "No image data returned from Google Gemini API", + PromptDetails = promptDetails, + ImageGenerator = GetImageGeneratorType(), + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + catch (Exception ex) + { + var errorMessage = $"Google Gemini image generator error: {ex.Message}"; + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = errorMessage, + PromptDetails = promptDetails, + ImageGenerator = GetImageGeneratorType(), + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + finally + { + _googleSemaphore.Release(); + } + } + + private ImageGeneratorApiType GetImageGeneratorType() + { + return _apiType; + } + + public void Dispose() + { + _httpClient?.Dispose(); + _googleSemaphore?.Dispose(); + } + } + +} \ No newline at end of file diff --git a/MultiImageClient/ImageGenerators/GoogleImagen4Generator.cs b/MultiImageClient/ImageGenerators/GoogleImagen4Generator.cs new file mode 100644 index 0000000..589321c --- /dev/null +++ b/MultiImageClient/ImageGenerators/GoogleImagen4Generator.cs @@ -0,0 +1,296 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Google.Apis.Auth.OAuth2; +using System.IO; +using Google.Cloud.AIPlatform.V1; +using Google.Protobuf.WellKnownTypes; +using System.Linq; + +namespace MultiImageClient +{ + /// DEPRECATED — Google is shutting down ALL Imagen endpoints (including + /// imagen-4.0-generate-001 used here) on 2026-06-24..30. This generator + /// will stop working then. Migrate to GoogleGenerator with + /// GoogleNanoBanana (gemini-3.1-flash-image) or GoogleNanoBananaPro + /// (gemini-3-pro-image), which is Google's official replacement path. + /// Kept for reference until the shutdown actually lands. + [Obsolete("All Google Imagen endpoints shut down 2026-06-24..30; use GoogleGenerator (Gemini image models) instead.")] + public class GoogleImagen4Generator : IImageGenerator + { + private readonly SemaphoreSlim _googleSemaphore; + private readonly string _apiKey; + private readonly MultiClientRunStats _stats; + private readonly string _name; + private readonly string _aspectRatio; + private readonly string _safetyFilterLevel; + private readonly string _location; + private readonly string _projectId; + private readonly string _googleServiceAccountKeyPath; + + private PredictionServiceClient _predictionServiceClient; + private GoogleCredential _credential; + private readonly object _clientLock = new object(); + + public ImageGeneratorApiType ApiType => ImageGeneratorApiType.GoogleImagen4; + + /// Constructor is intentionally cheap: it only stores fields. Credential + /// loading and client construction happen lazily on first use so that + /// users who never call Imagen 4 don't need any Google Cloud setup. + public GoogleImagen4Generator(string apiKey, int maxConcurrency, + MultiClientRunStats stats, string name, + string aspectRatio, + string safetyFilterLevel, + string location, + string projectId, + string googleServiceAccountKeyPath) + { + _apiKey = apiKey; + _googleSemaphore = new SemaphoreSlim(maxConcurrency); + _location = location; + _projectId = projectId; + _googleServiceAccountKeyPath = googleServiceAccountKeyPath; + _name = string.IsNullOrEmpty(name) ? "" : name; + _stats = stats; + _aspectRatio = aspectRatio; + _safetyFilterLevel = safetyFilterLevel; + } + + private PredictionServiceClient EnsureClient() + { + if (_predictionServiceClient != null) return _predictionServiceClient; + lock (_clientLock) + { + if (_predictionServiceClient != null) return _predictionServiceClient; + + if (string.IsNullOrWhiteSpace(_location)) + { + throw new InvalidOperationException( + "settings.json: GoogleCloudLocation is required to use the Imagen 4 generator (e.g. \"us-central1\")."); + } + if (string.IsNullOrWhiteSpace(_projectId)) + { + throw new InvalidOperationException( + "settings.json: GoogleCloudProjectId is required to use the Imagen 4 generator."); + } + if (string.IsNullOrWhiteSpace(_googleServiceAccountKeyPath)) + { + throw new InvalidOperationException( + "settings.json: GoogleServiceAccountKeyPath is required to use the Imagen 4 generator (absolute path to a service-account JSON key file)."); + } + if (!File.Exists(_googleServiceAccountKeyPath)) + { + throw new InvalidOperationException( + $"settings.json: GoogleServiceAccountKeyPath='{_googleServiceAccountKeyPath}' does not exist. Point it at a real service-account JSON file."); + } + + _credential = GoogleCredential.FromFile(_googleServiceAccountKeyPath) + .CreateScoped("https://www.googleapis.com/auth/cloud-platform"); + + _predictionServiceClient = new PredictionServiceClientBuilder + { + Endpoint = $"{_location}-aiplatform.googleapis.com", + Credential = _credential + }.Build(); + + return _predictionServiceClient; + } + } + + public string GetFilenamePart(PromptDetails pd) + { + var namePart = string.IsNullOrEmpty(_name) ? "" : $"-{_name}"; + return $"google-imagen4{namePart}"; + } + + public decimal GetCost() + { + // Imagen 4 pricing (higher than Imagen 3) + return 0.04m; + } + + public List GetRightParts() + { + var namePart = string.IsNullOrEmpty(_name) ? "" : _name; + return new List { "imagen4", namePart }; + } + + public string GetGeneratorSpecPart() + { + if (string.IsNullOrEmpty(_name)) + { + return "google-imagen4"; + } + else + { + return _name; + } + } + + public async Task ProcessPromptAsync(IImageGenerator generator, PromptDetails promptDetails) + { + await _googleSemaphore.WaitAsync(); + try + { + var client = EnsureClient(); + _stats.GoogleRequestCount++; + + // Google Gemini API endpoint for Imagen 4 + var apiUrl = $"https://{_location}-aiplatform.googleapis.com/v1/projects/{_projectId}/locations/{_location}/publishers/google/models/imagen-4.0-generate-001:predict"; + + // Construct the instance for the predict request + var instance = new Google.Protobuf.WellKnownTypes.Value + { + StructValue = new Google.Protobuf.WellKnownTypes.Struct + { + Fields = + { + { "prompt", Google.Protobuf.WellKnownTypes.Value.ForString(promptDetails.Prompt) }, + { "numberOfImages", Google.Protobuf.WellKnownTypes.Value.ForNumber(1) }, + { "aspectRatio", Google.Protobuf.WellKnownTypes.Value.ForString(_aspectRatio) }, + { "enhancePrompt", Google.Protobuf.WellKnownTypes.Value.ForBool(false) }, + { "includeRaiReason", Google.Protobuf.WellKnownTypes.Value.ForBool(true) }, + { "safetyFilterLevel", Google.Protobuf.WellKnownTypes.Value.ForString(_safetyFilterLevel) }, + { "safetySetting", Google.Protobuf.WellKnownTypes.Value.ForString("block_only_high") }, + { "personGeneration", Google.Protobuf.WellKnownTypes.Value.ForString("ALLOW_ALL") }, + { "addWatermark", Google.Protobuf.WellKnownTypes.Value.ForBool(false) } + } + } + }; + + var instances = new List { instance }; + + // Imagen 4 does not use a separate 'config' field in parameters. + // Parameters are directly specified in the instance. + var parameters = new Google.Protobuf.WellKnownTypes.Value(); // No parameters needed for now. + + var endpoint = EndpointName.FromProjectLocationPublisherModel(_projectId, _location, "google", "imagen-4.0-generate-001"); + + var response = await client.PredictAsync(endpoint, instances, parameters); + + var base64Images = new List(); + string commonMimeType = "image/png"; // Default or first detected mime type + + if (response?.Predictions != null && response.Predictions.Any()) + { + foreach (var prediction in response.Predictions) + { + if (prediction?.StructValue?.Fields != null) + { + var predictionFields = prediction.StructValue.Fields; + if (predictionFields.ContainsKey("bytesBase64Encoded") && predictionFields.ContainsKey("mimeType")) + { + var imageData = predictionFields["bytesBase64Encoded"].StringValue; + var newPrompt = predictionFields["prompt"].StringValue; + var currentMimeType = predictionFields["mimeType"].StringValue; + + if (!string.IsNullOrEmpty(imageData)) + { + var bd = new CreatedBase64Image + { + bytesBase64 = imageData, + newPrompt = newPrompt, + }; + + base64Images.Add(bd); + if (!string.IsNullOrEmpty(currentMimeType) && (commonMimeType == "image/png")) + { + commonMimeType = currentMimeType; // Use the first valid mime type found if default + } + } + } + } + } + } + + if (base64Images.Count == 0) + { + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = "No image data returned from Google Imagen 4 API", + PromptDetails = promptDetails, + ImageGenerator = ImageGeneratorApiType.GoogleImagen4, + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + + return new TaskProcessResult + { + IsSuccess = true, + Base64ImageDatas = base64Images, + ContentType = commonMimeType, + ErrorMessage = "", + PromptDetails = promptDetails, + ImageGenerator = ImageGeneratorApiType.GoogleImagen4, + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + catch (Exception ex) + { + var errorMessage = $"Google Imagen 4 Generator error: {ex.Message}"; + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = errorMessage.Split("Support").FirstOrDefault(), + PromptDetails = promptDetails, + ImageGenerator = ImageGeneratorApiType.GoogleImagen4, + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + finally + { + _googleSemaphore.Release(); + } + } + + public void Dispose() + { + _googleSemaphore?.Dispose(); + } + } + + // Shared response models for Google Imagen API + public class GoogleImagenResponse + { + public GoogleImagenGeneratedImage[] GeneratedImages { get; set; } + } + + public class GoogleImagenGeneratedImage + { + public string BytesBase64Encoded { get; set; } + public string MimeType { get; set; } + } + + // Response models for Gemini native image generation (Nano Banana) + public class GeminiGenerateContentResponse + { + public GeminiCandidate[] candidates { get; set; } + } + + public class GeminiCandidate + { + public GeminiContent content { get; set; } + } + + public class GeminiContent + { + public GeminiPart[] parts { get; set; } + } + + public class GeminiPart + { + public string text { get; set; } + public GeminiInlineData inlineData { get; set; } + } + + public class GeminiInlineData + { + public string mimeType { get; set; } + public string data { get; set; } + } +} diff --git a/MultiImageClient/ImageGenerators/GptImage2Generator.cs b/MultiImageClient/ImageGenerators/GptImage2Generator.cs new file mode 100644 index 0000000..29c326e --- /dev/null +++ b/MultiImageClient/ImageGenerators/GptImage2Generator.cs @@ -0,0 +1,688 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + // OpenAI `gpt-image-2` — released 2026-04-21. Uses the standard + // Images API at /v1/images/generations. Accepts `size`, `quality` + // (low/medium/high/auto), `n`, and `moderation` (auto/low). + // + // On THIS endpoint (/generations) the `input_fidelity` parameter must + // not be sent — gpt-image-2 always renders at high fidelity and the + // generations endpoint rejects the field. Note the OpenAI cookbook's + // gpt-image-2 edits examples do pass `input_fidelity="high"`, so the + // restriction may be generations-specific; re-test when wiring the + // /edits endpoint. Transparent backgrounds are not supported. Pricing is + // token-based ($30/1M output tokens); GetCost() returns a rough + // per-quality estimate for reporting only. + // + // Popular sizes: 1024x1024, 1536x1024, 1024x1536, 2048x2048, 2048x1152, + // 2560x1440 (QHD / 2K — cookbook's "recommended upper reliability + // boundary"), 3824x2144 (near-4K; see below), or "auto". Arbitrary + // resolutions are also allowed under the constraints: edges multiple of + // 16, max edge STRICTLY less than 3840 (cookbook 1.1), total pixels in + // [655360, 8294400], long:short edge ratio <= 3:1. 3840x2160 is listed + // in some places as "experimental" and may be rejected on some accounts + // — 3824x2144 is the safe canonical near-4K. + public class GptImage2Generator : IImageGenerator + { + private const string ModelId = "gpt-image-2"; + + private readonly SemaphoreSlim _semaphore; + // Typical gpt-image-2 latency is 10-60s, but OpenAI's own docs warn + // "complex prompts may take up to 2 minutes" and the launch-day tail + // can be worse. Default HttpClient.Timeout is 100s, which cuts into + // that envelope. 10 min is a safety buffer, not a claim about normal + // behavior. + private static readonly HttpClient _http = new HttpClient + { + Timeout = TimeSpan.FromMinutes(10) + }; + private readonly MultiClientRunStats _stats; + private readonly string _moderation; + private readonly string _name; + // Pools from which size and quality are randomly chosen per call. Single- + // element pools behave exactly like a fixed size/quality generator, so + // the old "one fixed variant" usage still works without special cases. + private readonly string[] _sizePool; + private readonly OpenAIGPTImageOneQuality[] _qualityPool; + + // How many images to request per call (`n` in the request body). 1 is + // the common case; >1 is useful for logo/variant exploration (cookbook + // 4.5) where a single round-trip returns multiple candidates. The + // streaming handler collects all N images and returns them as separate + // CreatedBase64Image entries so ImageManager's per-index save path + // ("...img0", "...img1", ...) produces distinct files automatically. + private readonly int _imageCount; + + // When non-empty, each streamed partial PNG is written under this + // folder (in a per-day "PartialsLive" subfolder) as it arrives. When + // _popUpPartials is also true, each saved partial is opened in the + // system default image viewer via Process.Start. This is the + // --quick-test interactive-feedback path; for normal runs both are + // off and partials are logged but not persisted. + private readonly string _partialSaveFolder; + private readonly bool _popUpPartials; + + public ImageGeneratorApiType ApiType => ImageGeneratorApiType.GptImage2; + + public GptImage2Generator(string apiKey, int maxConcurrency, string size, string moderation, OpenAIGPTImageOneQuality quality, MultiClientRunStats stats, string name) + : this(apiKey, maxConcurrency, new[] { size }, moderation, new[] { quality }, stats, name) + { + } + + public GptImage2Generator( + string apiKey, + int maxConcurrency, + string[] sizePool, + string moderation, + OpenAIGPTImageOneQuality[] qualityPool, + MultiClientRunStats stats, + string name, + string partialSaveFolder = null, + bool popUpPartials = false, + int imageCount = 1) + { + if (sizePool == null || sizePool.Length == 0) throw new ArgumentException("sizePool must be non-empty", nameof(sizePool)); + if (qualityPool == null || qualityPool.Length == 0) throw new ArgumentException("qualityPool must be non-empty", nameof(qualityPool)); + if (imageCount < 1) throw new ArgumentException("imageCount must be >= 1", nameof(imageCount)); + _semaphore = new SemaphoreSlim(maxConcurrency); + _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + _sizePool = sizePool; + _moderation = moderation; + _qualityPool = qualityPool; + _name = name ?? ""; + _stats = stats; + _partialSaveFolder = partialSaveFolder ?? ""; + _popUpPartials = popUpPartials; + _imageCount = imageCount; + } + + // Log tag + fallback label if an outer exception prevents us from + // building the richer per-call label. Always leads with ModelId so + // "gpt-image-2" is present even for named variants like "fast". + public string GetGeneratorSpecPart() => string.IsNullOrEmpty(_name) ? ModelId : $"{ModelId} {_name}"; + + public string GetFilenamePart(PromptDetails pd) + { + // Prefer the per-call choices written into PromptDetails.RuntimeMeta by + // ProcessPromptAsync so the filename reflects the actual request. + var size = _sizePool[0]; + var quality = _qualityPool[0].ToString(); + if (pd?.RuntimeMeta != null) + { + if (pd.RuntimeMeta.TryGetValue("size", out var s) && !string.IsNullOrEmpty(s)) size = s; + if (pd.RuntimeMeta.TryGetValue("quality", out var q) && !string.IsNullOrEmpty(q)) quality = q; + } + var modpt = string.IsNullOrEmpty(_moderation) || _moderation == "low" ? "" : $" mod{_moderation}"; + return $"gpt-2_{_name}{modpt}{size} qual{quality}"; + } + + // Token-based pricing. Until per-image averages are published this + // returns a conservative estimate derived from the documented + // $30/1M output-token rate and typical token counts seen for + // gpt-image-1 at the same size; treat as a ceiling, not a bill. + public decimal GetCost() + { + // Random pools make per-call cost unknowable at instance level; report + // the ceiling of the active quality pool, scaled by `n` since we + // always return exactly `n` images per call. + var worst = _qualityPool.Max(); + var perImage = worst switch + { + OpenAIGPTImageOneQuality.low => 0.02m, + OpenAIGPTImageOneQuality.medium => 0.08m, + OpenAIGPTImageOneQuality.high => 0.25m, + _ => 0.25m, + }; + return perImage * _imageCount; + } + + public List GetRightParts() + { + var modpt = $" moderation {_moderation}"; + var qualitypt = _qualityPool.Length == 1 + ? $"quality {_qualityPool[0]}" + : $"quality RANDOM({string.Join("/", _qualityPool)})"; + var sizept = _sizePool.Length == 1 + ? $"size {_sizePool[0]}" + : $"size RANDOM({string.Join("/", _sizePool)})"; + var parts = new List { ModelId, _name, sizept, qualitypt, modpt }; + if (_imageCount != 1) parts.Add($"n {_imageCount}"); + return parts; + } + + // How many partial images to request mid-stream. 0-3. Each partial + // costs +100 output tokens (~fractions of a cent), cheap relative to + // the value of seeing progress. We pick 2 so we get ~33% and ~66% + // snapshots along the way. + private const int PartialImageCount = 2; + + // Log a "still waiting..." line this often when the stream is quiet. + // The server does emit partials, but the pre-first-partial gap can + // be 10-30s and the final gap before `completed` can be similar. + private static readonly TimeSpan HeartbeatInterval = TimeSpan.FromSeconds(15); + + public async Task ProcessPromptAsync(IImageGenerator generator, PromptDetails promptDetails) + { + await _semaphore.WaitAsync(); + var sw = Stopwatch.StartNew(); + + // Pick size + quality for this single call from the configured pools. + // For single-element pools this is a deterministic pick and behaves + // just like the fixed-variant generator did previously. + var chosenSize = _sizePool[Random.Shared.Next(_sizePool.Length)]; + var chosenQuality = _qualityPool[Random.Shared.Next(_qualityPool.Length)]; + var arKeyword = SizeToAspectKeyword(chosenSize); + // Rich per-call label — ends up in the combined-image overlay and in + // per-save filenames via RuntimeMeta. GetGeneratorSpecPart() still + // returns just ModelId for top-level log lines, so the log stays terse. + // Always lead with the model id so the combined-image label makes + // it obvious which API produced this panel. Any per-variant `_name` + // tag gets appended after, never substituted for the model id. + var richLabel = string.IsNullOrEmpty(_name) + ? $"{ModelId} {chosenQuality} {arKeyword}" + : $"{ModelId} {_name} {chosenQuality} {arKeyword}"; + var genTag = richLabel; + + if (promptDetails != null) + { + promptDetails.RuntimeMeta["size"] = chosenSize; + promptDetails.RuntimeMeta["quality"] = chosenQuality.ToString(); + promptDetails.RuntimeMeta["label"] = richLabel; + } + + try + { + _stats.GptImage2RequestCount++; + + var bodyDict = new Dictionary + { + ["model"] = ModelId, + ["prompt"] = promptDetails.Prompt, + ["quality"] = chosenQuality.ToString(), + ["n"] = _imageCount, + ["size"] = chosenSize, + ["stream"] = true, + ["partial_images"] = PartialImageCount, + }; + if (!string.IsNullOrEmpty(_moderation)) + { + bodyDict["moderation"] = _moderation; + } + + var bodyJson = JsonSerializer.Serialize(bodyDict); + Logger.Log($" [{genTag}] POST /v1/images/generations body: {bodyJson}"); + + using var req = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/images/generations"); + req.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json"); + req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream")); + + using var heartbeatCts = new CancellationTokenSource(); + var heartbeatTask = RunHeartbeatAsync(genTag, sw, heartbeatCts.Token); + + try + { + using var resp = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); + Logger.Log($" [{genTag}] HTTP {(int)resp.StatusCode} {resp.ReasonPhrase} (HTTP/{resp.Version})"); + + if (!resp.IsSuccessStatusCode) + { + _stats.GptImage2RefusedCount++; + var errBody = await resp.Content.ReadAsStringAsync(); + string errorMessage; + try + { + using var errDoc = JsonDocument.Parse(errBody); + errorMessage = errDoc.RootElement.GetProperty("error").GetProperty("message").GetString() ?? errBody; + } + catch + { + errorMessage = errBody; + } + var cleanedMessage = errorMessage.Split("If you believe").First().Trim(); + Logger.Log($" [{genTag}] HTTP {(int)resp.StatusCode} after {sw.ElapsedMilliseconds} ms: {cleanedMessage}"); + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = cleanedMessage, + PromptDetails = promptDetails, + ImageGenerator = ImageGeneratorApiType.GptImage2, + CreateTotalMs = sw.ElapsedMilliseconds, + ImageGeneratorDescription = genTag, + }; + } + + Logger.Log($" [{genTag}] connected, streaming (partial_images={PartialImageCount}, n={_imageCount})"); + + // When n>1 the server streams events for each image in + // parallel; each event can carry an `image_index` (or + // `output_index` on some event shapes) distinguishing + // which of the N images it belongs to. Collect completed + // images into a SortedDictionary keyed by that index so + // the final list comes back in a stable 0..N-1 order. + // When no index is present (n=1 or older event shape), + // we assign sequentially so insertion order wins. + var finalImages = new SortedDictionary(); + int nextFallbackIdx = 0; + string streamErrorMessage = null; + long lastEventMs = 0; + + await using var rawStream = await resp.Content.ReadAsStreamAsync(); + using var reader = new StreamReader(rawStream, Encoding.UTF8); + + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync(); + if (line == null) break; + if (line.Length == 0) continue; // event boundary + if (!line.StartsWith("data:")) continue; // ignore `event:` / comment lines + var payload = line.Substring(5).TrimStart(); + if (payload == "[DONE]") break; + + using var evt = JsonDocument.Parse(payload); + var root = evt.RootElement; + var type = root.TryGetProperty("type", out var tEl) ? tEl.GetString() : "(no type)"; + var nowMs = sw.ElapsedMilliseconds; + var sinceLast = nowMs - lastEventMs; + lastEventMs = nowMs; + + switch (type) + { + case "image_generation.partial_image": + { + var pidx = root.TryGetProperty("partial_image_index", out var iEl) ? iEl.GetInt32() : -1; + var imgIdx = ExtractImageIndex(root); + var imgTag = _imageCount > 1 ? $" img{imgIdx}" : ""; + Logger.Log($" [{genTag}] partial #{pidx}{imgTag} at {nowMs} ms (+{sinceLast} ms since last event)"); + if (!string.IsNullOrEmpty(_partialSaveFolder) + && root.TryGetProperty("b64_json", out var pbEl) + && pbEl.ValueKind == JsonValueKind.String) + { + TrySavePartial(pbEl.GetString(), pidx, imgIdx, promptDetails, genTag); + } + break; + } + case "image_generation.completed": + { + var imgIdx = ExtractImageIndex(root); + if (imgIdx < 0) imgIdx = nextFallbackIdx; + nextFallbackIdx = Math.Max(nextFallbackIdx, imgIdx + 1); + + string b64 = root.TryGetProperty("b64_json", out var bEl) ? bEl.GetString() : null; + string revisedPrompt = root.TryGetProperty("revised_prompt", out var rpEl) ? rpEl.GetString() : null; + if (!string.IsNullOrEmpty(b64)) + { + finalImages[imgIdx] = (b64, revisedPrompt); + } + + string usageSummary = ExtractUsageSummary(root); + var imgTag = _imageCount > 1 ? $" img{imgIdx}" : ""; + Logger.Log($" [{genTag}] completed{imgTag} at {nowMs} ms (+{sinceLast} ms since last event).{usageSummary}"); + + if (!string.IsNullOrEmpty(revisedPrompt)) + { + Logger.Log($" [{genTag}] revised_prompt{imgTag}: {revisedPrompt}"); + } + break; + } + case "error": + case "image_generation.error": + { + var (msg, code) = ExtractErrorDetails(root); + streamErrorMessage = msg ?? payload; + var codePart = string.IsNullOrEmpty(code) ? "" : $" [code={code}]"; + Logger.Log($" [{genTag}] ERROR event at {nowMs} ms{codePart}: {streamErrorMessage}"); + break; + } + default: + // Unknown event types: log the type so we notice but don't dump payload. + Logger.Log($" [{genTag}] event '{type}' at {nowMs} ms (no handler)"); + break; + } + } + + if (finalImages.Count == 0) + { + _stats.GptImage2RefusedCount++; + var msg = streamErrorMessage ?? "stream ended without an image_generation.completed event"; + Logger.Log($" [{genTag}] {msg} after {sw.ElapsedMilliseconds} ms"); + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = msg, + PromptDetails = promptDetails, + ImageGenerator = ImageGeneratorApiType.GptImage2, + CreateTotalMs = sw.ElapsedMilliseconds, + ImageGeneratorDescription = genTag, + }; + } + + if (finalImages.Count < _imageCount) + { + // Partial success — we asked for N but only got M new CreatedBase64Image { bytesBase64 = v.b64, newPrompt = v.revisedPrompt ?? "" }) + .ToList(); + + return new TaskProcessResult + { + IsSuccess = true, + Base64ImageDatas = b64s, + Url = "", + ErrorMessage = "", + PromptDetails = promptDetails, + ImageGeneratorDescription = genTag, + ImageGenerator = ImageGeneratorApiType.GptImage2, + CreateTotalMs = sw.ElapsedMilliseconds, + }; + } + finally + { + heartbeatCts.Cancel(); + try { await heartbeatTask; } catch { /* ignored */ } + } + } + catch (Exception ex) + { + Logger.Log($" [{genTag}] EXCEPTION after {sw.ElapsedMilliseconds} ms: {ex.Message}"); + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = ex.Message, + PromptDetails = promptDetails, + ImageGeneratorDescription = genTag, + ImageGenerator = ImageGeneratorApiType.GptImage2, + CreateTotalMs = sw.ElapsedMilliseconds, + }; + } + finally + { + _semaphore.Release(); + } + } + + // When n>1 the server distinguishes per-image events by one of a + // couple possible fields. Try the documented "image_index" first, + // then fall back to "output_index" which some streaming builds emit + // instead. Returns -1 when neither is present (n=1 case; caller + // assigns insertion order). + private static int ExtractImageIndex(JsonElement root) + { + if (root.TryGetProperty("image_index", out var ii) && ii.ValueKind == JsonValueKind.Number) + { + return ii.GetInt32(); + } + if (root.TryGetProperty("output_index", out var oi) && oi.ValueKind == JsonValueKind.Number) + { + return oi.GetInt32(); + } + return -1; + } + + // Pretty-prints the `usage` block if present, including the detailed + // breakdown (image_tokens vs text_tokens) for both input and output. + // Returns leading-space so it composes directly into a one-liner. + private static string ExtractUsageSummary(JsonElement root) + { + if (!root.TryGetProperty("usage", out var u) || u.ValueKind != JsonValueKind.Object) + { + return ""; + } + var inTot = u.TryGetProperty("input_tokens", out var x1) ? x1.GetInt32() : 0; + var outTot = u.TryGetProperty("output_tokens", out var x2) ? x2.GetInt32() : 0; + var total = u.TryGetProperty("total_tokens", out var x3) ? x3.GetInt32() : 0; + var inText = 0; var inImg = 0; var outText = 0; var outImg = 0; + if (u.TryGetProperty("input_tokens_details", out var inD) && inD.ValueKind == JsonValueKind.Object) + { + if (inD.TryGetProperty("text_tokens", out var t)) inText = t.GetInt32(); + if (inD.TryGetProperty("image_tokens", out var i)) inImg = i.GetInt32(); + } + if (u.TryGetProperty("output_tokens_details", out var outD) && outD.ValueKind == JsonValueKind.Object) + { + if (outD.TryGetProperty("text_tokens", out var t)) outText = t.GetInt32(); + if (outD.TryGetProperty("image_tokens", out var i)) outImg = i.GetInt32(); + } + return $" usage: in={inTot} (text={inText},img={inImg}) out={outTot} (text={outText},img={outImg}) total={total}"; + } + + // gpt-image-2 size constraints, cribbed from the class-level doc: + // - "auto" is always valid (server picks) + // - WxH format, each edge a positive multiple of 16 + // - each edge <= 3840 + // - total pixels in [655360, 8294400] + // - long:short edge ratio <= 3:1 + // These are the constants we compare against; keep them in sync with + // the doc at the top of the class if OpenAI changes the envelope. + public const int SizeEdgeMultiple = 16; + public const int SizeMaxEdge = 3840; + public const int SizeMinPixels = 655360; + public const int SizeMaxPixels = 8294400; + public const double SizeMaxAspectRatio = 3.0; + + // Validate and gently autocorrect a user-supplied "WxH" size string. + // + // Returns true when `normalized` is safe to send to the API. Emits the + // possibly-snapped value (e.g. "1526x2048" -> "1520x2048") and a + // non-null `note` whenever the caller should surface something to the + // user. Returns false with a human-readable `error` when no amount of + // snapping can fit the constraints (over 3840, out of pixel range, + // ratio too extreme, unparseable). + // + // "auto" passes through unchanged. Case-insensitive separators 'x' and + // 'X' are both accepted. Whitespace around the input is trimmed. + public static bool TryNormalizeSize(string input, out string normalized, out string note, out string error) + { + normalized = null; + note = null; + error = null; + + if (string.IsNullOrWhiteSpace(input)) + { + error = "size is empty"; + return false; + } + var s = input.Trim(); + if (s.Equals("auto", StringComparison.OrdinalIgnoreCase)) + { + normalized = "auto"; + return true; + } + + var parts = s.Split(new[] { 'x', 'X' }, 2); + if (parts.Length != 2 + || !int.TryParse(parts[0].Trim(), out var w) + || !int.TryParse(parts[1].Trim(), out var h)) + { + error = $"'{input}' is not a WxH size (e.g. 1024x1024) or 'auto'"; + return false; + } + if (w <= 0 || h <= 0) + { + error = $"edges must be positive (got {w}x{h})"; + return false; + } + + // Snap each edge to the nearest multiple of 16. Report what we did. + var snappedW = RoundToMultiple(w, SizeEdgeMultiple); + var snappedH = RoundToMultiple(h, SizeEdgeMultiple); + if (snappedW != w || snappedH != h) + { + note = $"snapped {w}x{h} -> {snappedW}x{snappedH} (each edge must be a multiple of {SizeEdgeMultiple})"; + w = snappedW; + h = snappedH; + } + + // Cookbook (2026-04-21) is explicit that the max edge rule is + // strict: "Maximum edge length must be less than 3840px". The + // popular size 3840x2160 is flagged as experimental and may 400 + // on some accounts; safer canonical near-miss is 3824x2144. + if (w >= SizeMaxEdge || h >= SizeMaxEdge) + { + error = $"edge must be < {SizeMaxEdge} (got {w}x{h}; try 3824x2144)"; + return false; + } + + long pixels = (long)w * h; + if (pixels < SizeMinPixels) + { + error = $"total pixels {pixels:N0} < {SizeMinPixels:N0} minimum ({w}x{h})"; + return false; + } + if (pixels > SizeMaxPixels) + { + error = $"total pixels {pixels:N0} > {SizeMaxPixels:N0} maximum ({w}x{h})"; + return false; + } + + double ratio = w >= h ? (double)w / h : (double)h / w; + if (ratio > SizeMaxAspectRatio + 1e-9) + { + error = $"aspect ratio {ratio:F2}:1 exceeds {SizeMaxAspectRatio}:1 cap ({w}x{h})"; + return false; + } + + normalized = $"{w}x{h}"; + return true; + } + + private static int RoundToMultiple(int value, int step) + { + if (step <= 0) return value; + // Banker-style "nearest" rounding, with ties going up so that + // halfway typos (e.g. 1528 between 1520 and 1536) bias toward the + // larger, more common canonical sizes that users usually meant. + int rem = value % step; + if (rem == 0) return value; + if (rem * 2 >= step) return value + (step - rem); + return value - rem; + } + + // "1024x1024" -> "square", "1024x1536" -> "portrait", "1536x1024" -> "landscape". + // "auto" passes through. Unknown sizes return the string itself so the label + // still carries useful information. + private static string SizeToAspectKeyword(string size) + { + if (string.IsNullOrEmpty(size)) return ""; + if (size == "auto") return "auto"; + var parts = size.Split('x'); + if (parts.Length != 2) return size; + if (!int.TryParse(parts[0], out var w) || !int.TryParse(parts[1], out var h)) return size; + if (w == h) return "square"; + return w > h ? "landscape" : "portrait"; + } + + // OpenAI error events in the SSE stream can be shaped two ways: + // { "type": "error", "message": "...", "code": "...", ... } + // or nested: + // { "type": "error", "error": { "message": "...", "code": "..." } } + // Try both, fall back to null if neither shape matches. + private static (string message, string code) ExtractErrorDetails(JsonElement root) + { + string message = null; + string code = null; + if (root.TryGetProperty("error", out var errEl) && errEl.ValueKind == JsonValueKind.Object) + { + if (errEl.TryGetProperty("message", out var m1) && m1.ValueKind == JsonValueKind.String) + { + message = m1.GetString(); + } + if (errEl.TryGetProperty("code", out var c1) && c1.ValueKind == JsonValueKind.String) + { + code = c1.GetString(); + } + else if (errEl.TryGetProperty("type", out var t1) && t1.ValueKind == JsonValueKind.String) + { + code = t1.GetString(); + } + } + if (message == null && root.TryGetProperty("message", out var m2) && m2.ValueKind == JsonValueKind.String) + { + message = m2.GetString(); + } + if (!string.IsNullOrEmpty(message)) + { + message = message.Split("If you believe").First().Trim(); + } + return (message, code); + } + + // Decode and write one partial PNG under {base}/{today}/PartialsLive/ + // and (if configured) open it in the default image viewer. Best-effort: + // a partial save failure never interrupts the generation. + // + // `partialIdx` is the partial-within-image index (0..PartialImageCount-1, + // from the `partial_image_index` event field). `imageIdx` is which of + // the N images this partial belongs to when n>1; pass -1 when n=1 and + // the server didn't send an image index. + private void TrySavePartial(string b64, int partialIdx, int imageIdx, PromptDetails pd, string genTag) + { + try + { + var bytes = Convert.FromBase64String(b64); + var today = DateTime.Now.ToString("yyyy-MM-dd-dddd"); + var folder = Path.Combine(_partialSaveFolder, today, "PartialsLive"); + Directory.CreateDirectory(folder); + + var promptPart = FilenameGenerator.TruncatePrompt(pd?.Prompt ?? "partial", 60); + // Zero-padded timestamp keeps files sorted by arrival order + // across runs; partial index disambiguates within a single call. + // When n>1, image index distinguishes per-output streams. + var ts = DateTime.Now.ToString("HHmmss_fff"); + var imgPart = imageIdx >= 0 && _imageCount > 1 ? $"_img{imageIdx}" : ""; + var file = $"{ts}_partial{Math.Max(0, partialIdx):D2}{imgPart}_{promptPart}.png"; + var full = Path.Combine(folder, file); + File.WriteAllBytes(full, bytes); + Logger.Log($" [{genTag}] saved partial #{partialIdx}{imgPart} -> {full}"); + + if (_popUpPartials) + { + try + { + Process.Start(new ProcessStartInfo(full) { UseShellExecute = true }); + } + catch (Exception ex) + { + Logger.Log($" [{genTag}] pop-up failed for partial #{partialIdx}: {ex.Message}"); + } + } + } + catch (Exception ex) + { + Logger.Log($" [{genTag}] failed to save partial #{partialIdx}: {ex.Message}"); + } + } + + private static async Task RunHeartbeatAsync(string genTag, Stopwatch sw, CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + try + { + await Task.Delay(HeartbeatInterval, ct); + } + catch (OperationCanceledException) + { + return; + } + if (ct.IsCancellationRequested) return; + Logger.Log($" [{genTag}] ...still waiting, {sw.ElapsedMilliseconds / 1000}s elapsed"); + } + } + } +} diff --git a/MultiImageClient/ImageGenerators/GptImageOneGenerator.cs b/MultiImageClient/ImageGenerators/GptImageOneGenerator.cs new file mode 100644 index 0000000..480656e --- /dev/null +++ b/MultiImageClient/ImageGenerators/GptImageOneGenerator.cs @@ -0,0 +1,317 @@ + + +using OpenAI.Images; + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + + public class GptImageOneGenerator : IImageGenerator + { + private SemaphoreSlim _gptImageOneSemaphore; + static readonly HttpClient http = new HttpClient(); + private MultiClientRunStats _stats; + private string _size; + private string _moderation; + private OpenAIGPTImageOneQuality _quality; + private string _name; + private ImageGeneratorApiType _apiType; + + public ImageGeneratorApiType ApiType => _apiType; + + public string GetGeneratorSpecPart() + { + if (string.IsNullOrEmpty(_name)) + { + if (_apiType == ImageGeneratorApiType.GptImage1) + { + return $"gpt-image-1"; + } + else if (_apiType == ImageGeneratorApiType.GptImage1Mini) + { + return $"gpt-image-1-mini"; + } + else + { + throw new Exception("E"); + } + } + else + { + return $"{_name}"; + } + } + + public GptImageOneGenerator(string apiKey, int maxConcurrency, string size, string moderation, OpenAIGPTImageOneQuality quality, ImageGeneratorApiType apiType, MultiClientRunStats stats, string name) + { + if (apiType == ImageGeneratorApiType.GptImage1 || apiType == ImageGeneratorApiType.GptImage1Mini) + { + _apiType = apiType; + } + else + { + throw new Exception("bad"); + } + _gptImageOneSemaphore = new SemaphoreSlim(maxConcurrency); + + http.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", apiKey); + + _size = size; + _moderation = moderation; + _quality = quality; + _name = string.IsNullOrEmpty(name) ? "" : name; + _stats = stats; + + } + + public string GetFilenamePart(PromptDetails pd) + { + var modpt = ""; + if (_moderation != "low") + { + modpt = $" mod{_moderation}"; + } + + var qualitypt = ""; + if (_quality != OpenAIGPTImageOneQuality.auto) + { + qualitypt = $" qual{_quality}"; + } + + var sizept = _size.ToString(); + + var res = $"gpt-1_{_name}{modpt}{sizept}{qualitypt}"; + return res; + } + public decimal GetCost() + { + if (_apiType == ImageGeneratorApiType.GptImage1) + { + if (_size == "1024x1024") + { + switch (_quality) + { + case OpenAIGPTImageOneQuality.low: + return 0.01088m; + case OpenAIGPTImageOneQuality.medium: + return 0.04224m; + case OpenAIGPTImageOneQuality.high: + return 0.1664m; + case OpenAIGPTImageOneQuality.auto: + return 99.9999m; //strange, not clear how payment works for auto. This 99.99$ is not real + default: + throw new Exception("Swe"); + } + } + else if (_size == "1024x1536") + { + switch (_quality) + { + case OpenAIGPTImageOneQuality.low: + return 0.01632m; + case OpenAIGPTImageOneQuality.medium: + return 0.06336m; + case OpenAIGPTImageOneQuality.high: + return 0.24960m; + case OpenAIGPTImageOneQuality.auto: + return 99.99m; + default: + throw new Exception("S"); + } + } + else if (_size == "1536x1024") + { + switch (_quality) + { + case OpenAIGPTImageOneQuality.low: + return 0.016m; + case OpenAIGPTImageOneQuality.medium: + return 0.06272m; + case OpenAIGPTImageOneQuality.high: + return 0.24832m; + default: + throw new Exception("S"); + } + } + else + { + throw new Exception("bad size."); + } + } + else if (_apiType == ImageGeneratorApiType.GptImage1Mini) + { + if (_size == "1024x1024") + { + switch (_quality) + { + case OpenAIGPTImageOneQuality.low: + return 0.005m; + case OpenAIGPTImageOneQuality.medium: + return 0.011m; + case OpenAIGPTImageOneQuality.high: + return 0.036m; + case OpenAIGPTImageOneQuality.auto: + return 99.9999m; //strange, not clear how payment works for auto. This 99.99$ is not real + default: + throw new Exception("Swe"); + } + } + else if (_size == "1024x1536") + { + switch (_quality) + { + case OpenAIGPTImageOneQuality.low: + return 0.006m; + case OpenAIGPTImageOneQuality.medium: + return 0.015m; + case OpenAIGPTImageOneQuality.high: + return 0.052m; + case OpenAIGPTImageOneQuality.auto: + return 99.99m; + default: + throw new Exception("S"); + } + } + else if (_size == "1536x1024") + { + switch (_quality) + { + case OpenAIGPTImageOneQuality.low: + return 0.006m; + case OpenAIGPTImageOneQuality.medium: + return 0.015m; + case OpenAIGPTImageOneQuality.high: + return 0.052m; + default: + throw new Exception("S"); + } + } + else + { + throw new Exception("bad size."); + } + + } + else + { + throw new Exception("S"); + } + + } + + public List GetRightParts() + { + var modpt = ""; + modpt = $" moderation {_moderation}"; + + var qualitypt = ""; + qualitypt = $"quality {_quality}"; + + var sizept = $"size {_size.ToString()}"; + var rightsideContents = new List() { "gpt-image-1", _name, sizept, qualitypt, modpt }; + + return rightsideContents; + } + + + public async Task ProcessPromptAsync(IImageGenerator generator, PromptDetails promptDetails) + { + await _gptImageOneSemaphore.WaitAsync(); + var sw = Stopwatch.StartNew(); + try + { + _stats.GptImageOneRequestCount++; + var body = new + { + model = "gpt-image-1", + prompt = promptDetails.Prompt, + moderation = _moderation, + quality = _quality.ToString(), + n = 1, + size = _size, + }; + + using var content = new StringContent( + JsonSerializer.Serialize(body), + Encoding.UTF8, + "application/json"); + + using var resp = await http.PostAsync( + "https://api.openai.com/v1/images/generations", + content); + + if (!resp.IsSuccessStatusCode) + { + Console.WriteLine($"\t\tXXXXXXXXXXXXX == Fail: {promptDetails.Prompt}"); + + var json2 = await resp.Content.ReadAsStringAsync(); + using JsonDocument doc2 = JsonDocument.Parse(json2); + string errorMessage = doc2.RootElement.GetProperty("error").GetProperty("message").GetString(); + Console.WriteLine("errorMessage"); + var cleanedMessage = errorMessage.Split("If you believe").First().Trim(); + return new TaskProcessResult { IsSuccess = false, ErrorMessage = cleanedMessage, PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.GptImage1, CreateTotalMs = sw.ElapsedMilliseconds, ImageGeneratorDescription = generator.GetGeneratorSpecPart() }; + } + resp.EnsureSuccessStatusCode(); + var json = await resp.Content.ReadAsStringAsync(); + + //await File.WriteAllTextAsync("response.json", json); + using var doc = JsonDocument.Parse(json); + var qq = doc.RootElement.GetProperty("data"); + var ll = qq.GetArrayLength(); + var b64s = new List(); + foreach (var el in qq.EnumerateArray()) + { + var b64 = el.GetProperty("b64_json").GetString(); + var theGuy = new CreatedBase64Image { bytesBase64 = b64, newPrompt = "" }; + b64s.Add(theGuy); + } + + + //Console.WriteLine($"Generated:{promptDetails.Prompt}"); + return new TaskProcessResult + { + IsSuccess = true, + Base64ImageDatas = b64s, + Url = "", + ErrorMessage = "", + PromptDetails = promptDetails, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + ImageGenerator = ImageGeneratorApiType.GptImage1, + CreateTotalMs = sw.ElapsedMilliseconds + }; + } + catch (Exception ex) + { + Console.WriteLine($"\t\t{promptDetails.Prompt} Error: {ex.Message}"); + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = ex.Message, + PromptDetails = promptDetails, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + + ImageGenerator = ImageGeneratorApiType.GptImage1, + CreateTotalMs = sw.ElapsedMilliseconds + }; + } + finally + { + _gptImageOneSemaphore.Release(); + } + } + + } +} \ No newline at end of file diff --git a/MultiImageClient/ImageGenerators/GrokImagineGenerator.cs b/MultiImageClient/ImageGenerators/GrokImagineGenerator.cs new file mode 100644 index 0000000..2de4ba3 --- /dev/null +++ b/MultiImageClient/ImageGenerators/GrokImagineGenerator.cs @@ -0,0 +1,338 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using XAIGrokAPIClient; + +namespace MultiImageClient +{ + /// Image generator backed by xAI's /v1/images/generations endpoint (Grok + /// Imagine family). Uses our hand-rolled XAIGrokClient so every wire + /// parameter is explicit — no OpenAI SDK indirection. + /// + /// Supports both `grok-imagine-image` ($0.02/img) and + /// `grok-imagine-image-pro` ($0.07/img), selected at construction time via + /// ImageGeneratorApiType.GrokImagine / GrokImaginePro. + public class GrokImagineGenerator : IImageGenerator + { + private readonly SemaphoreSlim _semaphore; + private readonly XAIGrokClient _client; + private readonly HttpClient _httpClient; + private readonly MultiClientRunStats _stats; + private readonly ImageGeneratorApiType _apiType; + private readonly string _model; + private readonly string _aspectRatio; + private readonly string _quality; + private readonly string _resolution; + private readonly string _responseFormat; + private readonly string _name; + + public ImageGeneratorApiType ApiType => _apiType; + + private readonly Settings? _settings; + + /// apiType — GrokImagine or GrokImaginePro. + /// aspectRatio — one of the xAI-documented AR strings ("1:1", "3:4", + /// "4:3", "16:9", etc.), or "auto" to let xAI pick. + /// quality — "low" | "medium" | "high". + /// resolution — "1k" | "2k". + /// responseFormat — "url" (default) or "b64_json". "url" is fine for + /// batch runs since we download immediately. + /// settings — optional; when provided, every successful + /// generation is appended to the Grok history + /// ledger (grok_ledger.jsonl) for archive/sync. + public GrokImagineGenerator( + string apiKey, + int maxConcurrency, + ImageGeneratorApiType apiType, + MultiClientRunStats stats, + string name = "", + string aspectRatio = "1:1", + string quality = "high", + string resolution = "1k", + string responseFormat = "url", + Settings? settings = null) + { + _settings = settings; + if (apiType != ImageGeneratorApiType.GrokImagine && apiType != ImageGeneratorApiType.GrokImaginePro) + { + throw new ArgumentException( + $"GrokImagineGenerator only supports GrokImagine or GrokImaginePro, got {apiType}.", + nameof(apiType)); + } + _apiType = apiType; + _model = apiType == ImageGeneratorApiType.GrokImaginePro + ? XAIGrokClient.ModelGrokImaginePro + : XAIGrokClient.ModelGrokImagine; + + _client = new XAIGrokClient(apiKey); + _httpClient = new HttpClient(); + _semaphore = new SemaphoreSlim(maxConcurrency); + _stats = stats; + _name = name ?? string.Empty; + _aspectRatio = aspectRatio; + _quality = quality; + _resolution = resolution; + _responseFormat = responseFormat; + } + + /// Short human-readable tier label used in the combined-grid panel + /// header ("xAI Grok Imagine" vs "xAI Grok Imagine Pro"). + private string TierLabel => _apiType == ImageGeneratorApiType.GrokImaginePro + ? "xAI Grok Imagine Pro" + : "xAI Grok Imagine"; + + /// Very rough pixel-dimensions estimate for a given resolution+AR, + /// using the xAI convention that "1k"/"2k" sets the longer edge to + /// ~1024 / ~2048 pixels respectively and the other edge follows the + /// aspect ratio. Used for on-image annotation so a viewer can tell + /// at a glance whether they're looking at a 1 MP or 4 MP render. + /// Returns the empty string when we can't parse the AR. + private string EstimatePixelsLabel() + { + var longEdge = string.Equals(_resolution, "2k", StringComparison.OrdinalIgnoreCase) ? 2048 + : string.Equals(_resolution, "1k", StringComparison.OrdinalIgnoreCase) ? 1024 + : 0; + if (longEdge == 0) return ""; + if (string.IsNullOrWhiteSpace(_aspectRatio) || + _aspectRatio.Equals("auto", StringComparison.OrdinalIgnoreCase)) + { + return $"~{longEdge}px long edge"; + } + var parts = _aspectRatio.Split(':', 2); + if (parts.Length != 2 || + !double.TryParse(parts[0], System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, out var a) || + !double.TryParse(parts[1], System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, out var b) || + a <= 0 || b <= 0) + { + return $"~{longEdge}px long edge"; + } + double wRatio = a, hRatio = b; + int w, h; + if (wRatio >= hRatio) + { + w = longEdge; + h = (int)System.Math.Round(longEdge * hRatio / wRatio); + } + else + { + h = longEdge; + w = (int)System.Math.Round(longEdge * wRatio / hRatio); + } + return $"~{w}x{h}"; + } + + public string GetFilenamePart(PromptDetails pd) + { + var shortModel = _apiType == ImageGeneratorApiType.GrokImaginePro ? "grok-pro" : "grok"; + var arLabel = string.IsNullOrWhiteSpace(_aspectRatio) ? "auto" : _aspectRatio.Replace(':', 'x'); + var qLabel = string.IsNullOrWhiteSpace(_quality) ? "q" : $"q{_quality}"; + var resLabel = string.IsNullOrWhiteSpace(_resolution) ? "" : $"_{_resolution}"; + var nameLabel = string.IsNullOrEmpty(_name) ? "" : $"_{_name}"; + return $"{shortModel}_{arLabel}_{qLabel}{resLabel}{nameLabel}"; + } + + public List GetRightParts() + { + var parts = new List + { + TierLabel, + _model, + }; + parts.Add(string.IsNullOrWhiteSpace(_aspectRatio) + ? "AR auto" + : $"AR {_aspectRatio}"); + if (!string.IsNullOrWhiteSpace(_quality)) parts.Add($"quality {_quality}"); + if (!string.IsNullOrWhiteSpace(_resolution)) parts.Add($"res {_resolution}"); + var px = EstimatePixelsLabel(); + if (!string.IsNullOrEmpty(px)) parts.Add(px); + if (!string.IsNullOrEmpty(_name)) parts.Add(_name); + return parts; + } + + public string GetGeneratorSpecPart() + { + // Always lead with the tier label so the combined-grid panel + // header credits xAI explicitly — "grok-imagine-image" alone + // reads like a random slug in a multi-provider grid. The user + // name, if any, is appended so custom variants still show up. + var px = EstimatePixelsLabel(); + var line = TierLabel; + if (!string.IsNullOrWhiteSpace(_aspectRatio)) line += $" {_aspectRatio}"; + if (!string.IsNullOrWhiteSpace(_quality)) line += $" q:{_quality}"; + if (!string.IsNullOrWhiteSpace(_resolution)) line += $" {_resolution}"; + if (!string.IsNullOrEmpty(px)) line += $" ({px})"; + if (!string.IsNullOrEmpty(_name)) line += $" [{_name}]"; + return line; + } + + // https://docs.x.ai/developers/models — flat per-image pricing. + public decimal GetCost() => _apiType == ImageGeneratorApiType.GrokImaginePro ? 0.07m : 0.02m; + + public async Task ProcessPromptAsync(IImageGenerator generator, PromptDetails promptDetails) + { + await _semaphore.WaitAsync(); + var sw = Stopwatch.StartNew(); + try + { + _stats.GrokImageGenerationRequestCount++; + + var prompt = promptDetails.Prompt ?? string.Empty; + + var req = new XAIGrokGenerateRequest + { + Prompt = prompt, + Model = _model, + AspectRatio = string.IsNullOrWhiteSpace(_aspectRatio) ? null : _aspectRatio, + Quality = string.IsNullOrWhiteSpace(_quality) ? null : _quality, + Resolution = string.IsNullOrWhiteSpace(_resolution) ? null : _resolution, + ResponseFormat = string.IsNullOrWhiteSpace(_responseFormat) ? null : _responseFormat, + N = 1, + }; + + var pxHint = EstimatePixelsLabel(); + var pxSuffix = string.IsNullOrEmpty(pxHint) ? "" : $" ({pxHint})"; + Logger.Log($"\t-> {TierLabel} [{_model}] AR={_aspectRatio} q={_quality} res={_resolution}{pxSuffix}: {prompt}"); + var response = await _client.GenerateAsync(req); + sw.Stop(); + + if (response.Data == null || response.Data.Count == 0) + { + _stats.GrokImageGenerationErrorCount++; + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = "Grok returned empty data[] with no images.", + PromptDetails = promptDetails, + ImageGenerator = _apiType, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + CreateTotalMs = sw.ElapsedMilliseconds, + }; + } + + _stats.GrokImageGenerationSuccessCount++; + + var first = response.Data[0]; + var usd = response.Usage?.CostUsd; + var usdLabel = usd.HasValue ? $" cost=${usd:0.####}" : string.Empty; + Logger.Log($"\t<- {TierLabel} [{_model}] OK in {sw.ElapsedMilliseconds} ms{usdLabel}"); + + GrokLedger.Append(_settings, new GrokLedgerEntry + { + Kind = "image", + Model = _model, + Prompt = prompt, + RemoteUrl = first.Url, + Source = "live", + }); + + // URL path — typical response. We HEAD it to capture content-type + // so downstream conversion logic (webp/jpg -> png) kicks in. + if (!string.IsNullOrEmpty(first.Url)) + { + string? contentType = first.MimeType; + if (string.IsNullOrEmpty(contentType)) + { + try + { + var head = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, first.Url)); + contentType = head.Content.Headers.ContentType?.MediaType; + } + catch (Exception ex) + { + Logger.Log($"\t(Grok) HEAD on image url failed ({ex.Message}); leaving content-type null."); + } + } + + return new TaskProcessResult + { + IsSuccess = true, + Url = first.Url, + ContentType = contentType, + PromptDetails = promptDetails, + ImageGenerator = _apiType, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + CreateTotalMs = sw.ElapsedMilliseconds, + }; + } + + // b64_json path — when the caller asked for inline base64. + if (!string.IsNullOrEmpty(first.Base64Json)) + { + var b64Image = new CreatedBase64Image + { + bytesBase64 = first.Base64Json, + newPrompt = prompt, + }; + return new TaskProcessResult + { + IsSuccess = true, + Base64ImageDatas = new List { b64Image }, + ContentType = first.MimeType ?? "image/png", + PromptDetails = promptDetails, + ImageGenerator = _apiType, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + CreateTotalMs = sw.ElapsedMilliseconds, + }; + } + + _stats.GrokImageGenerationErrorCount++; + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = "Grok returned data[0] with neither url nor b64_json.", + PromptDetails = promptDetails, + ImageGenerator = _apiType, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + CreateTotalMs = sw.ElapsedMilliseconds, + }; + } + catch (XAIGrokException ex) + { + sw.Stop(); + _stats.GrokImageGenerationErrorCount++; + Logger.Log($"\t<- Grok {_model} FAIL http={ex.StatusCode}: {Truncate(ex.ResponseBody, 500)}"); + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = $"{ex.StatusCode}: {Truncate(ex.ResponseBody, 300)}", + PromptDetails = promptDetails, + ImageGenerator = _apiType, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + CreateTotalMs = sw.ElapsedMilliseconds, + }; + } + catch (Exception ex) + { + sw.Stop(); + _stats.GrokImageGenerationErrorCount++; + Logger.Log($"\t<- Grok {_model} EXCEPTION: {ex.Message}"); + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = ex.Message, + PromptDetails = promptDetails, + ImageGenerator = _apiType, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + CreateTotalMs = sw.ElapsedMilliseconds, + }; + } + finally + { + _semaphore.Release(); + } + } + + private static string Truncate(string s, int max) + { + if (string.IsNullOrEmpty(s)) return s ?? string.Empty; + return s.Length <= max ? s : s.Substring(0, max) + "..."; + } + } +} diff --git a/MultiImageClient/ImageGenerators/GrokImagineVideoGenerator.cs b/MultiImageClient/ImageGenerators/GrokImagineVideoGenerator.cs new file mode 100644 index 0000000..c926555 --- /dev/null +++ b/MultiImageClient/ImageGenerators/GrokImagineVideoGenerator.cs @@ -0,0 +1,304 @@ +#nullable enable +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using XAIGrokAPIClient; + +namespace MultiImageClient +{ + /// VIDEO generator backed by xAI's asynchronous /v1/videos/generations + /// endpoint (grok-imagine-video). Dispatches exactly like any image + /// generator — it implements IImageGenerator and can sit in the same + /// multiplex list — but the deliverable is an mp4, which does not fit + /// the PNG-only save pipeline. So this generator: + /// + /// 1. starts the request, polls until done (5s cadence, bounded), + /// 2. downloads the mp4 itself into {ImageDownloadBaseFolder}\{day}\Video\ + /// (xAI's URLs are temporary, so we never rely on them later), + /// 3. mirrors the clip via DlMirror, + /// 4. renders a PNG "video card" (what/where/how long) and hands THAT + /// to the combined-grid pipeline so contact sheets stay all-PNG. + /// + /// Pricing (xAI, 2026-06, approximate): per-second billing, roughly + /// $0.05/s at 480p and $0.10/s at 720p. GetCost() reflects that estimate. + public class GrokImagineVideoGenerator : IImageGenerator + { + private readonly SemaphoreSlim _semaphore; + private readonly XAIGrokClient _client; + private readonly HttpClient _httpClient; + private readonly MultiClientRunStats _stats; + private readonly Settings _settings; + private readonly string _name; + private readonly string _aspectRatio; + private readonly string _resolution; + private readonly int _durationSeconds; + private readonly TimeSpan _pollInterval; + private readonly TimeSpan _timeout; + + public ImageGeneratorApiType ApiType => ImageGeneratorApiType.GrokImagineVideo; + + /// durationSeconds — xAI range [1, 15]; default 6 keeps cost and + /// poll time low for smoke tests. + /// resolution — "480p" (default tier, fastest) | "720p" | "1080p". + public GrokImagineVideoGenerator( + string apiKey, + int maxConcurrency, + MultiClientRunStats stats, + Settings settings, + string name = "", + string aspectRatio = "16:9", + string resolution = "480p", + int durationSeconds = 6, + int pollSeconds = 5, + int timeoutMinutes = 10) + { + if (durationSeconds < 1 || durationSeconds > 15) + { + throw new ArgumentOutOfRangeException(nameof(durationSeconds), "xAI video duration must be 1-15 seconds."); + } + _client = new XAIGrokClient(apiKey); + _httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(5) }; + _semaphore = new SemaphoreSlim(maxConcurrency); + _stats = stats; + _settings = settings; + _name = name ?? string.Empty; + _aspectRatio = aspectRatio; + _resolution = resolution; + _durationSeconds = durationSeconds; + _pollInterval = TimeSpan.FromSeconds(pollSeconds); + _timeout = TimeSpan.FromMinutes(timeoutMinutes); + } + + public string GetFilenamePart(PromptDetails pd) + { + var arLabel = string.IsNullOrWhiteSpace(_aspectRatio) ? "auto" : _aspectRatio.Replace(':', 'x'); + var nameLabel = string.IsNullOrEmpty(_name) ? "" : $"_{_name}"; + return $"grok-video_{arLabel}_{_resolution}_{_durationSeconds}s{nameLabel}"; + } + + public List GetRightParts() + { + var parts = new List + { + "xAI Grok Imagine Video", + XAIGrokClient.ModelGrokImagineVideo, + $"AR {_aspectRatio}", + _resolution, + $"{_durationSeconds}s", + }; + if (!string.IsNullOrEmpty(_name)) parts.Add(_name); + return parts; + } + + public string GetGeneratorSpecPart() + { + var line = $"xAI Grok Imagine Video {_aspectRatio} {_resolution} {_durationSeconds}s"; + if (!string.IsNullOrEmpty(_name)) line += $" [{_name}]"; + return line; + } + + public decimal GetCost() + { + var perSecond = string.Equals(_resolution, "720p", StringComparison.OrdinalIgnoreCase) ? 0.10m + : string.Equals(_resolution, "1080p", StringComparison.OrdinalIgnoreCase) ? 0.20m + : 0.05m; + return perSecond * _durationSeconds; + } + + public async Task ProcessPromptAsync(IImageGenerator generator, PromptDetails promptDetails) + { + await _semaphore.WaitAsync(); + var sw = Stopwatch.StartNew(); + try + { + _stats.GrokVideoGenerationRequestCount++; + var prompt = promptDetails.Prompt ?? string.Empty; + + // storage_options => xAI keeps a durable, never-expiring copy in + // the team's Files API store. That's the only server-side + // history xAI exposes, so it's what makes `--grok-sync` + // able to re-download every clip later, from any machine. + var storedFilename = FilenameGenerator.SanitizeFilename( + $"{DateTime.UtcNow:yyyyMMddHHmmss}_{GetFilenamePart(null!)}_{FilenameGenerator.TruncatePrompt(prompt, 80)}") + ".mp4"; + var req = new XAIGrokVideoGenerateRequest + { + Prompt = prompt, + Model = XAIGrokClient.ModelGrokImagineVideo, + Duration = _durationSeconds, + AspectRatio = string.IsNullOrWhiteSpace(_aspectRatio) ? null : _aspectRatio, + Resolution = string.IsNullOrWhiteSpace(_resolution) ? null : _resolution, + StorageOptions = new XAIGrokVideoStorageOptions { Filename = storedFilename }, + }; + + Logger.Log($"\t-> Grok Video [{XAIGrokClient.ModelGrokImagineVideo}] AR={_aspectRatio} res={_resolution} dur={_durationSeconds}s: {prompt}"); + var result = await _client.GenerateVideoAsync(req, _pollInterval, _timeout); + sw.Stop(); + + var hasUrl = !string.IsNullOrEmpty(result.Video?.Url); + var hasStoredFile = !string.IsNullOrEmpty(result.Video?.FileOutput?.FileId); + if (!result.IsDone || (!hasUrl && !hasStoredFile)) + { + _stats.GrokVideoGenerationErrorCount++; + var detail = result.Error != null + ? $"{result.Error.Code}: {result.Error.Message}" + : $"status={result.Status}"; + Logger.Log($"\t<- Grok Video FAIL {detail}"); + return Fail($"Grok video did not complete ({detail}).", promptDetails, generator, sw.ElapsedMilliseconds); + } + + // xAI's direct video URLs are temporary — download immediately. + // With storage_options set we may instead (or also) get a durable + // Files API copy; prefer the temp URL, fall back to the file. + var mp4Bytes = hasUrl + ? await _httpClient.GetByteArrayAsync(result.Video!.Url) + : await _client.DownloadFileContentAsync(result.Video!.FileOutput!.FileId); + var mp4Path = SaveVideo(mp4Bytes, prompt); + DlMirror.Copy(mp4Path, _settings.FlatImageMirrorPath); + + _stats.GrokVideoGenerationSuccessCount++; + var actualDuration = result.Video.Duration ?? _durationSeconds; + Logger.Log($"\t<- Grok Video OK in {sw.ElapsedMilliseconds} ms; {mp4Bytes.Length / 1024} KB, {actualDuration}s -> {mp4Path}"); + + GrokLedger.Append(_settings, new GrokLedgerEntry + { + Kind = "video", + Model = XAIGrokClient.ModelGrokImagineVideo, + Prompt = prompt, + RequestId = result.RequestId, + FileId = result.Video.FileOutput?.FileId, + RemoteUrl = result.Video.Url, + LocalPath = mp4Path, + Bytes = mp4Bytes.Length, + Source = "live", + }); + + // PNG stand-in for the contact-sheet pipeline. The mp4 itself + // is already safe on disk; this card is just how a video shows + // up alongside the stills. + var card = RenderVideoCard(prompt, mp4Path, actualDuration, mp4Bytes.Length); + + var processResult = new TaskProcessResult + { + IsSuccess = true, + ContentType = "image/png", + PromptDetails = promptDetails, + ImageGenerator = ApiType, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + CreateTotalMs = sw.ElapsedMilliseconds, + }; + processResult.SetImageBytes(0, card); + return processResult; + } + catch (XAIGrokException ex) + { + sw.Stop(); + _stats.GrokVideoGenerationErrorCount++; + Logger.Log($"\t<- Grok Video FAIL http={ex.StatusCode}: {Truncate(ex.ResponseBody, 500)}"); + return Fail($"{ex.StatusCode}: {Truncate(ex.ResponseBody, 300)}", promptDetails, generator, sw.ElapsedMilliseconds); + } + catch (Exception ex) + { + sw.Stop(); + _stats.GrokVideoGenerationErrorCount++; + Logger.Log($"\t<- Grok Video EXCEPTION: {ex.Message}"); + return Fail(ex.Message, promptDetails, generator, sw.ElapsedMilliseconds); + } + finally + { + _semaphore.Release(); + } + } + + private TaskProcessResult Fail(string message, PromptDetails pd, IImageGenerator generator, long elapsedMs) + { + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = message, + PromptDetails = pd, + ImageGenerator = ApiType, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + CreateTotalMs = elapsedMs, + }; + } + + /// Writes the mp4 under {ImageDownloadBaseFolder}\{yyyy-MM-dd-dddd}\Video\ + /// (same day-folder convention as image saves) and returns the path. + private string SaveVideo(byte[] mp4Bytes, string prompt) + { + var dayFolder = Path.Combine(_settings.ImageDownloadBaseFolder, DateTime.Now.ToString("yyyy-MM-dd-dddd")); + var videoFolder = Path.Combine(dayFolder, "Video"); + Directory.CreateDirectory(videoFolder); + + var stem = FilenameGenerator.SanitizeFilename( + $"{DateTime.Now:yyyyMMddHHmmss}_{GetFilenamePart(null!)}_{FilenameGenerator.TruncatePrompt(prompt, 90)}"); + var path = Path.Combine(videoFolder, $"{stem}.mp4"); + int count = 1; + while (File.Exists(path)) + { + path = Path.Combine(videoFolder, $"{stem}_{count:D4}.mp4"); + count++; + } + File.WriteAllBytes(path, mp4Bytes); + return path; + } + + /// Renders the PNG "video card" cell used in combined grids: a film- + /// style header band, the key facts in black, and the saved path so a + /// viewer of the sheet can find the actual clip. + private byte[] RenderVideoCard(string prompt, string mp4Path, int durationSeconds, long sizeBytes) + { + const int width = 1024; + const int height = 576; // 16:9 to telegraph "this one is a video" + + using var image = ImageUtils.CreateStandardImage(width, height, UIConstants.White); + // Title is auto-sized to guarantee a single line inside the gold + // band; everything below is wrapped, with the band heights sized + // generously so wrapped lines never spill into the next section. + var titleFont = ImageUtils.AutoSizeFont("VIDEO — xAI Grok Imagine", width, 34, 16, FontStyle.Bold); + var bodyFont = FontUtils.CreateFont(22, FontStyle.Regular); + var pathFont = FontUtils.CreateFont(15, FontStyle.Regular); + + image.Mutate(ctx => + { + ctx.ApplyStandardGraphicsOptions(); + + // Gold header band — film-leader feel, and gold is one of the + // approved semantic colors (see AGENTS.md typography policy). + ctx.DrawTextWithBackground(new RectangleF(0, 0, width, 70), + "VIDEO — xAI Grok Imagine", titleFont, UIConstants.Black, UIConstants.Gold); + + var facts = $"{XAIGrokClient.ModelGrokImagineVideo} {_aspectRatio} {_resolution} {durationSeconds}s {sizeBytes / 1024} KB"; + ctx.DrawTextWithBackground(new RectangleF(0, 90, width, 50), + facts, bodyFont, UIConstants.Black, UIConstants.White); + + ctx.DrawTextWithBackground(new RectangleF(0, 160, width, 230), + Truncate(prompt, 320), bodyFont, UIConstants.Black, UIConstants.White, + SixLabors.Fonts.HorizontalAlignment.Left); + + ctx.DrawTextWithBackground(new RectangleF(0, height - 160, width, 150), + $"saved to:\n{mp4Path}", pathFont, UIConstants.SuccessGreen, UIConstants.White, + SixLabors.Fonts.HorizontalAlignment.Left); + }); + + using var ms = new MemoryStream(); + image.SaveAsPng(ms); + return ms.ToArray(); + } + + private static string Truncate(string s, int max) + { + if (string.IsNullOrEmpty(s)) return s ?? string.Empty; + return s.Length <= max ? s : s.Substring(0, max) + "..."; + } + } +} diff --git a/MultiImageClient/ImageGenerators/IdeogramGenerator.cs b/MultiImageClient/ImageGenerators/IdeogramGenerator.cs new file mode 100644 index 0000000..7cedd86 --- /dev/null +++ b/MultiImageClient/ImageGenerators/IdeogramGenerator.cs @@ -0,0 +1,218 @@ +using IdeogramAPIClient; +using System.Text.RegularExpressions; + +using System; +using System.Collections.Generic; +using System.Net.Http; + +using System.Threading; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public class IdeogramGenerator : IImageGenerator + { + private SemaphoreSlim _ideogramSemaphore; + private IdeogramClient _ideogramClient; + private HttpClient _httpClient = new HttpClient(); + private IdeogramMagicPromptOption _magicPrompt; + private IdeogramAspectRatio _aspectRatio; + private IdeogramStyleType? _ideoGramStyleType; + private string _negativePrompt; + private MultiClientRunStats _stats; + private IdeogramModel _model; + private string _name; + + public ImageGeneratorApiType ApiType => ImageGeneratorApiType.Ideogram; + + public string GetGeneratorSpecPart() + { + if (string.IsNullOrEmpty(_name)) + { + return $"ideogram_{_model}"; + } + else + { + return $"{_name}"; + } + } + + public IdeogramGenerator(string apiKey, int maxConcurrency, IdeogramMagicPromptOption magicPrompt, IdeogramAspectRatio aspectRatio, IdeogramStyleType? styleType, string negativePrompt, IdeogramModel model, MultiClientRunStats stats, string name) + { + _ideogramClient = new IdeogramClient(apiKey); + _ideogramSemaphore = new SemaphoreSlim(maxConcurrency); + _magicPrompt = magicPrompt; + _aspectRatio = aspectRatio; + _ideoGramStyleType = styleType; + _negativePrompt = negativePrompt; + _model = model; + _stats = stats; + _name = string.IsNullOrEmpty(name) ? "" : name; + } + + public string GetFilenamePart(PromptDetails pd) + { + var neg = ""; + var stylepart = ""; + if (_ideoGramStyleType == null) + { + //auto + stylepart = ""; + } + else + { + stylepart = $"_{_ideoGramStyleType.ToString().ToLowerInvariant()}"; + } + var clneg = TextUtils.CleanPrompt(_negativePrompt); + if (!string.IsNullOrEmpty(clneg)) + { + neg = $" neg_{clneg}"; + } + + var verpart = ""; + switch (_model) + { + case IdeogramModel.V_1: + verpart = "v1"; + break; + case IdeogramModel.V_2: + verpart = "v2"; + break; + case IdeogramModel.V_1_TURBO: + verpart = "v1_turbo"; + break; + case IdeogramModel.V_2_TURBO: + verpart = "v2_turbo"; + break; + case IdeogramModel.V_2A: + verpart = "v2a"; + break; + case IdeogramModel.V_2A_TURBO: + verpart = "v2a_turbo"; + break; + default: + throw new Exception("Q"); + } + + + var res = $"ideogram_{verpart}{_name}_magic_{_magicPrompt.ToString().ToLowerInvariant()} {_aspectRatio.ToString().ToLowerInvariant().Replace("aspect_","ar").Replace("_","x")} {stylepart}{neg}"; + + return res; + } + + // https://ideogram.ai/features/api-pricing + //https://ideogram.ai/manage-api + public decimal GetCost() + { + switch (_model) + { + case IdeogramModel.V_1: + return 0.06m; + case IdeogramModel.V_1_TURBO: + return 0.02m; + case IdeogramModel.V_2: + return 0.08m; + case IdeogramModel.V_2_TURBO: + return 0.05m; + case IdeogramModel.V_2A: + return 0.04m; + case IdeogramModel.V_2A_TURBO: + return 0.025m; + default: + throw new Exception("Q"); + } + } + + public List GetRightParts() + { + var neg = ""; + var stylepart = ""; + if (_ideoGramStyleType == null) + { + //auto + stylepart = "style auto"; + } + else + { + stylepart = $"style {_ideoGramStyleType.ToString().ToLowerInvariant()}"; + } + var clneg = TextUtils.CleanPrompt(_negativePrompt); + if (!string.IsNullOrEmpty(clneg)) + { + neg = $" neg {clneg}"; + } + var verpart = _model.ToString().Replace("_", "").ToLowerInvariant(); + + var rightsideContents = new List() { $"ideogram {verpart}", _name, stylepart, neg, $"magicprompt_{_magicPrompt.ToString()}", _aspectRatio.ToString().ToLowerInvariant().Replace("aspect_", "ar").Replace("_", "x") }; + + return rightsideContents; + } + + public async Task ProcessPromptAsync(IImageGenerator generator, PromptDetails promptDetails) + { + await _ideogramSemaphore.WaitAsync(); + try + { + var ideogramOptions = new IdeogramOptions + { + AspectRatio = _aspectRatio, + Model = IdeogramModel.V_2, + MagicPromptOption = _magicPrompt, + StyleType = _ideoGramStyleType + }; + + var request = new IdeogramGenerateRequest(promptDetails.Prompt, ideogramOptions); + + _stats.IdeogramRequestCount++; + GenerateResponse response = await _ideogramClient.GenerateImageAsync(request); + if (response?.Data?.Count == 1) + { + foreach (var imageObject in response.Data) + { + //there is only actually one ever. + var returnedPrompt = imageObject.Prompt; + if (returnedPrompt != promptDetails.Prompt && returnedPrompt.Replace(" ", " ") != promptDetails.Prompt.Replace(" ", " ")) + { + //Ideogram replaced the prompt. + promptDetails.ReplacePrompt(returnedPrompt, returnedPrompt, TransformationType.IdeogramRewrite); + } + var headResponse = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, imageObject.Url)); + var contentType = headResponse.Content.Headers.ContentType?.MediaType; + return new TaskProcessResult { IsSuccess = true, Url = imageObject.Url, PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.Ideogram, ImageGeneratorDescription = generator.GetGeneratorSpecPart() }; + } + throw new Exception("No images returned"); + } + else if (response?.Data?.Count >= 1) + { + throw new Exception("Multiple images returned? I can't handle this! Who knew!"); + } + else + { + return new TaskProcessResult { IsSuccess = false, ErrorMessage = "No images generated", PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.Ideogram, GenericImageErrorType = GenericImageGenerationErrorType.NoImagesGenerated, ImageGeneratorDescription = generator.GetGeneratorSpecPart() }; + } + } + + catch (Exception ex) + { + Console.WriteLine(ex); + + Match m = Regex.Match(ex.Message, "\"error\"\\s*:\\s*\"([^\"]+)\""); + string errorMessage; + if (m.Success) + { + errorMessage = m.Groups[1].Value; + Console.WriteLine(errorMessage); + } + else + { + errorMessage = ex.Message; + } + return new TaskProcessResult { IsSuccess = false, ErrorMessage = errorMessage, PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.Ideogram, GenericImageErrorType = GenericImageGenerationErrorType.Unknown, ImageGeneratorDescription = generator.GetGeneratorSpecPart() }; + } + finally + { + _ideogramSemaphore.Release(); + } + } + } +} \ No newline at end of file diff --git a/MultiImageClient/ImageGenerators/IdeogramV3Generator.cs b/MultiImageClient/ImageGenerators/IdeogramV3Generator.cs new file mode 100644 index 0000000..abd011f --- /dev/null +++ b/MultiImageClient/ImageGenerators/IdeogramV3Generator.cs @@ -0,0 +1,188 @@ +using MultiImageClient; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + + +namespace IdeogramAPIClient +{ + public class IdeogramV3Generator : IImageGenerator + { + private readonly SemaphoreSlim _semaphore; + private readonly IdeogramClient _client; + private readonly MultiClientRunStats _stats; + private readonly IdeogramV3StyleType _styleType; + private readonly IdeogramMagicPromptOption? _magicPromptOption; + private readonly string _aspectRatio; + private readonly IdeogramRenderingSpeed _renderingSpeed; + private readonly string _negativePrompt; + private readonly string _name; + + public ImageGeneratorApiType ApiType => ImageGeneratorApiType.IdeogramV3; + + public IdeogramV3Generator( + string apiKey, + int maxConcurrency, + IdeogramV3StyleType styleType, + IdeogramMagicPromptOption? magicPromptOption, + IdeogramAspectRatio aspectRatio, + IdeogramRenderingSpeed renderingSpeed, + string negativePrompt, + MultiClientRunStats stats, + string name) + { + _client = new IdeogramClient(apiKey); + _semaphore = new SemaphoreSlim(maxConcurrency); + _stats = stats; + _styleType = styleType; + _magicPromptOption = magicPromptOption; + var convertedAspectRatio = aspectRatio.ToString().Replace("ASPECT_", "").Replace("_", "x"); + _aspectRatio = convertedAspectRatio; + _renderingSpeed = renderingSpeed; + _negativePrompt = negativePrompt ?? string.Empty; + _name = string.IsNullOrEmpty(name) ? "" : name; + } + + + + public string GetFilenamePart(PromptDetails pd) + { + var parts = new List { ApiType.ToString() }; + if (!string.IsNullOrEmpty(_name)) + { + parts.Add(_name); + } + + parts.Add(_styleType.ToString()); + parts.Add(_aspectRatio.ToString().Replace(":", "x")); + parts.Add(_renderingSpeed.ToString()); + return string.Join("_", parts.Where(p => !string.IsNullOrEmpty(p))); + } + + public List GetRightParts() + { + var contents = new List { "ideogram v3" }; + if (!string.IsNullOrEmpty(_name)) + { + contents.Add(_name); + } + + contents.Add(_styleType.ToString()); + contents.Add(_aspectRatio.ToString().Replace(":", "x")); + contents.Add(_renderingSpeed.ToString()); + + return contents; + } + + public string GetGeneratorSpecPart() + { + if (!string.IsNullOrEmpty(_name)) + { + return _name; + } + return "ideogram-v3"; + } + + public decimal GetCost() + { + // Pricing is not yet documented; leave a placeholder number until official rates available. + return 0.08m; + } + + public async Task ProcessPromptAsync(IImageGenerator generator, PromptDetails promptDetails) + { + await _semaphore.WaitAsync(); + try + { + _stats.IdeogramV3RequestCount++; + + var request = new IdeogramV3GenerateRequest(promptDetails.Prompt) + { + AspectRatio = _aspectRatio, + RenderingSpeed = _renderingSpeed, + StyleType = _styleType, + MagicPrompt = _magicPromptOption, + NegativePrompt = string.IsNullOrWhiteSpace(_negativePrompt) ? null : _negativePrompt + }; + + var response = await _client.GenerateImageV3Async(request); + + if (response?.Data == null || response.Data.Count == 0) + { + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = "No images generated", + PromptDetails = promptDetails, + ImageGenerator = ImageGeneratorApiType.IdeogramV3, + GenericImageErrorType = GenericImageGenerationErrorType.NoImagesGenerated, + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + + if (response.Data.Count > 1) + { + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = "Multiple images returned and the client is not configured for batches", + PromptDetails = promptDetails, + ImageGenerator = ImageGeneratorApiType.IdeogramV3, + GenericImageErrorType = GenericImageGenerationErrorType.Unknown, + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + + var imageObject = response.Data[0]; + if (!string.IsNullOrWhiteSpace(imageObject.Prompt) && + !string.Equals(imageObject.Prompt, promptDetails.Prompt, StringComparison.OrdinalIgnoreCase)) + { + promptDetails.ReplacePrompt(imageObject.Prompt, imageObject.Prompt, TransformationType.IdeogramRewrite); + } + + return new TaskProcessResult + { + IsSuccess = true, + Url = imageObject.Url, + PromptDetails = promptDetails, + ImageGenerator = ImageGeneratorApiType.IdeogramV3, + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + catch (HttpRequestException ex) + { + _stats.IdeogramV3RefusedCount++; + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = ex.Message, + PromptDetails = promptDetails, + ImageGenerator = ImageGeneratorApiType.IdeogramV3, + GenericImageErrorType = GenericImageGenerationErrorType.Unknown, + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + catch (Exception ex) + { + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = ex.Message, + PromptDetails = promptDetails, + ImageGenerator = ImageGeneratorApiType.IdeogramV3, + GenericImageErrorType = GenericImageGenerationErrorType.Unknown, + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + finally + { + _semaphore.Release(); + } + } + } +} + diff --git a/MultiImageClient/ImageGenerators/IdeogramV4Generator.cs b/MultiImageClient/ImageGenerators/IdeogramV4Generator.cs new file mode 100644 index 0000000..906ded9 --- /dev/null +++ b/MultiImageClient/ImageGenerators/IdeogramV4Generator.cs @@ -0,0 +1,186 @@ +using MultiImageClient; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace IdeogramAPIClient +{ + /// Image generator backed by Ideogram 4.0 (POST /v1/ideogram-v4/generate, + /// released 2026-06-03). 2K-native output; rendering_speed trades latency + /// against fidelity (FLASH cheapest/fastest -> QUALITY best). + /// + /// v4 has no style_type/magic_prompt knobs — plain text_prompt is + /// auto-expanded into a structured JSON prompt server-side, and that + /// expansion comes back in data[0].prompt (as serialized JSON). + public class IdeogramV4Generator : IImageGenerator + { + private readonly SemaphoreSlim _semaphore; + private readonly IdeogramClient _client; + private readonly MultiClientRunStats _stats; + private readonly string _resolution; + private readonly IdeogramRenderingSpeed _renderingSpeed; + private readonly string _name; + + public ImageGeneratorApiType ApiType => ImageGeneratorApiType.IdeogramV4; + + /// resolution — one of the documented v4 2K strings ("2048x2048", + /// "2304x1728", "2560x1440", ...), or null/empty to let the API + /// default to 2048x2048. + public IdeogramV4Generator( + string apiKey, + int maxConcurrency, + string resolution, + IdeogramRenderingSpeed renderingSpeed, + MultiClientRunStats stats, + string name) + { + _client = new IdeogramClient(apiKey); + _semaphore = new SemaphoreSlim(maxConcurrency); + _stats = stats; + _resolution = resolution ?? string.Empty; + _renderingSpeed = renderingSpeed; + _name = string.IsNullOrEmpty(name) ? "" : name; + } + + public string GetFilenamePart(PromptDetails pd) + { + var parts = new List { "IdeogramV4" }; + if (!string.IsNullOrEmpty(_name)) + { + parts.Add(_name); + } + if (!string.IsNullOrEmpty(_resolution)) + { + parts.Add(_resolution); + } + parts.Add(_renderingSpeed.ToString()); + return string.Join("_", parts.Where(p => !string.IsNullOrEmpty(p))); + } + + public List GetRightParts() + { + var contents = new List { "ideogram v4" }; + if (!string.IsNullOrEmpty(_name)) + { + contents.Add(_name); + } + if (!string.IsNullOrEmpty(_resolution)) + { + contents.Add(_resolution); + } + contents.Add(_renderingSpeed.ToString()); + return contents; + } + + public string GetGeneratorSpecPart() + { + if (!string.IsNullOrEmpty(_name)) + { + return _name; + } + return $"ideogram-v4 {_renderingSpeed}"; + } + + public decimal GetCost() + { + // Per-image pricing varies by rendering_speed; Ideogram's pricing + // page lists v4 DEFAULT around $0.08 with FLASH/TURBO cheaper and + // QUALITY pricier. Rough estimates until we wire exact rates. + return _renderingSpeed switch + { + IdeogramRenderingSpeed.FLASH => 0.025m, + IdeogramRenderingSpeed.TURBO => 0.04m, + IdeogramRenderingSpeed.QUALITY => 0.12m, + _ => 0.08m, + }; + } + + public async Task ProcessPromptAsync(IImageGenerator generator, PromptDetails promptDetails) + { + await _semaphore.WaitAsync(); + try + { + _stats.IdeogramV4RequestCount++; + + var request = new IdeogramV4GenerateRequest(promptDetails.Prompt) + { + Resolution = string.IsNullOrWhiteSpace(_resolution) ? null : _resolution, + RenderingSpeed = _renderingSpeed, + }; + + var response = await _client.GenerateImageV4Async(request); + + if (response?.Data == null || response.Data.Count == 0) + { + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = "No images generated", + PromptDetails = promptDetails, + ImageGenerator = ImageGeneratorApiType.IdeogramV4, + GenericImageErrorType = GenericImageGenerationErrorType.NoImagesGenerated, + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + + var imageObject = response.Data[0]; + + if (!imageObject.IsImageSafe) + { + _stats.IdeogramV4RefusedCount++; + } + + // v4 returns the server-side structured-JSON prompt expansion in + // `prompt`. We do NOT ReplacePrompt with it (it's JSON, not a + // human-readable rewrite) — log it via the history instead. + if (!string.IsNullOrWhiteSpace(imageObject.Prompt) && + !string.Equals(imageObject.Prompt, promptDetails.Prompt, StringComparison.OrdinalIgnoreCase)) + { + promptDetails.AddStep(imageObject.Prompt, TransformationType.IdeogramRewrite); + } + + return new TaskProcessResult + { + IsSuccess = true, + Url = imageObject.Url, + PromptDetails = promptDetails, + ImageGenerator = ImageGeneratorApiType.IdeogramV4, + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + catch (HttpRequestException ex) + { + _stats.IdeogramV4RefusedCount++; + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = ex.Message, + PromptDetails = promptDetails, + ImageGenerator = ImageGeneratorApiType.IdeogramV4, + GenericImageErrorType = GenericImageGenerationErrorType.Unknown, + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + catch (Exception ex) + { + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = ex.Message, + PromptDetails = promptDetails, + ImageGenerator = ImageGeneratorApiType.IdeogramV4, + GenericImageErrorType = GenericImageGenerationErrorType.Unknown, + ImageGeneratorDescription = generator.GetGeneratorSpecPart() + }; + } + finally + { + _semaphore.Release(); + } + } + } +} diff --git a/MultiImageClient/ImageGenerators/LocalFlux2ComfyGenerator.cs b/MultiImageClient/ImageGenerators/LocalFlux2ComfyGenerator.cs new file mode 100644 index 0000000..8a904b6 --- /dev/null +++ b/MultiImageClient/ImageGenerators/LocalFlux2ComfyGenerator.cs @@ -0,0 +1,578 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public class LocalFlux2ComfyGenerator : IImageGenerator + { + private static readonly string[] ModelInputNames = + { "unet_name", "ckpt_name", "model_name", "diffusion_model_name" }; + + private static readonly string[] TextEncoderInputNames = + { + "clip_name", "clip_name1", "clip_name2", "clip_name3", + "text_encoder_name", "text_encoder_name1", "text_encoder_name2", + "t5_name", "qwen_name" + }; + + private static readonly string[] VaeInputNames = { "vae_name" }; + + private readonly Settings _settings; + private readonly SemaphoreSlim _semaphore; + private readonly MultiClientRunStats _stats; + private readonly HttpClient _httpClient; + private readonly string _endpoint; + private readonly string _name; + + public ImageGeneratorApiType ApiType => ImageGeneratorApiType.LocalFlux2Uncensored; + + public LocalFlux2ComfyGenerator(Settings settings, int maxConcurrency, MultiClientRunStats stats, string name = "") + { + _settings = settings; + _stats = stats; + _name = string.IsNullOrWhiteSpace(name) ? "" : name; + _semaphore = new SemaphoreSlim(Math.Max(1, maxConcurrency)); + _endpoint = NormalizeEndpoint(settings.LocalFlux2ComfyEndpoint); + EnsureEndpointAllowed(_endpoint, settings.LocalFlux2AllowRemoteEndpoint); + + var timeout = settings.LocalFlux2TimeoutSeconds <= 0 ? 900 : settings.LocalFlux2TimeoutSeconds; + _httpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(timeout + 30) + }; + } + + public string GetFilenamePart(PromptDetails pd) + { + var suffix = string.IsNullOrEmpty(_name) ? "" : _name; + return $"{ApiType}{suffix}_{_settings.LocalFlux2Height}x{_settings.LocalFlux2Width}"; + } + + public List GetRightParts() + { + return new List + { + "Local Flux2", + "ComfyUI", + $"{_settings.LocalFlux2Width}x{_settings.LocalFlux2Height}", + EndpointHostLabel(_endpoint) + }; + } + + public string GetGeneratorSpecPart() + { + return string.IsNullOrEmpty(_name) ? "local-flux2-uncensored" : _name; + } + + public decimal GetCost() + { + return 0m; + } + + public async Task ProcessPromptAsync(IImageGenerator generator, PromptDetails promptDetails) + { + await _semaphore.WaitAsync(); + var sw = Stopwatch.StartNew(); + _stats.LocalFlux2RequestCount++; + + try + { + if (string.IsNullOrWhiteSpace(_settings.LocalFlux2WorkflowPath)) + { + throw new InvalidOperationException( + "Settings.LocalFlux2WorkflowPath is required for the local Flux2 ComfyUI generator."); + } + if (!File.Exists(_settings.LocalFlux2WorkflowPath)) + { + throw new FileNotFoundException( + $"Local Flux2 workflow file not found: {_settings.LocalFlux2WorkflowPath}"); + } + + var workflow = LoadApiWorkflow(_settings.LocalFlux2WorkflowPath); + PatchWorkflow(workflow, promptDetails.Prompt); + + var promptId = await QueuePromptAsync(workflow); + Logger.Log($"{promptDetails} Local Flux2 queued in ComfyUI: {promptId}"); + + var history = await WaitForHistoryAsync(promptId); + var images = await DownloadOutputImagesAsync(history); + if (images.Count == 0) + { + throw new InvalidOperationException("ComfyUI completed the workflow but returned no images."); + } + + _stats.LocalFlux2SuccessCount++; + Logger.Log($"{promptDetails} Local Flux2 generated {images.Count} image(s)."); + + return new TaskProcessResult + { + IsSuccess = true, + Base64ImageDatas = images.Select(i => new CreatedBase64Image + { + bytesBase64 = Convert.ToBase64String(i.Bytes), + newPrompt = promptDetails.Prompt + }).ToList(), + ContentType = images.First().ContentType, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + PromptDetails = promptDetails, + ImageGenerator = ApiType, + CreateTotalMs = sw.ElapsedMilliseconds, + }; + } + catch (Exception ex) + { + _stats.LocalFlux2ErrorCount++; + Logger.Log($"{promptDetails} Local Flux2 error: {ex.Message}"); + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = ex.Message, + PromptDetails = promptDetails, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + ImageGenerator = ApiType, + CreateTotalMs = sw.ElapsedMilliseconds, + }; + } + finally + { + _semaphore.Release(); + } + } + + private JObject LoadApiWorkflow(string workflowPath) + { + var root = JObject.Parse(File.ReadAllText(workflowPath)); + if (root["prompt"] is JObject promptObject) + { + return (JObject)promptObject.DeepClone(); + } + if (LooksLikeApiPrompt(root)) + { + return (JObject)root.DeepClone(); + } + if (root["nodes"] is JArray) + { + throw new InvalidOperationException( + "The configured ComfyUI workflow looks like UI format. Use ComfyUI's \"Save (API Format)\" export for LocalFlux2WorkflowPath."); + } + + throw new InvalidOperationException( + "The configured ComfyUI workflow is not an API-format prompt JSON object."); + } + + private static bool LooksLikeApiPrompt(JObject root) + { + return root.Properties().Any(p => + p.Value is JObject node && + node["class_type"] != null && + node["inputs"] is JObject); + } + + private void PatchWorkflow(JObject workflow, string prompt) + { + ApplyPromptText(workflow, prompt); + ApplyImageSize(workflow, _settings.LocalFlux2Width, _settings.LocalFlux2Height); + ApplySeedAndSamplerSettings(workflow); + ApplyFilenamePrefix(workflow); + + ApplyNamedInput(workflow, _settings.LocalFlux2ModelName, ModelInputNames, IsModelLoaderNode); + ApplyNamedInput(workflow, _settings.LocalFlux2TextEncoderName, TextEncoderInputNames, IsTextEncoderLoaderNode); + ApplyNamedInput(workflow, _settings.LocalFlux2VaeName, VaeInputNames, IsVaeLoaderNode); + + ApplyExplicitOverrides(workflow); + } + + private void ApplyPromptText(JObject workflow, string prompt) + { + if (!string.IsNullOrWhiteSpace(_settings.LocalFlux2PositivePromptNodeId)) + { + SetNodeInput(workflow, _settings.LocalFlux2PositivePromptNodeId, "text", prompt); + } + else + { + var positive = WorkflowNodes(workflow) + .FirstOrDefault(n => HasInput(n.Node, "text") && IsTextEncodeNode(n.Node) && !LooksNegative(n.Node)); + if (positive.Node != null) + { + SetInput(positive.Node, "text", prompt); + } + else + { + throw new InvalidOperationException( + "Could not find a positive text prompt node in the ComfyUI workflow. Set LocalFlux2PositivePromptNodeId in settings.json."); + } + } + + if (!string.IsNullOrWhiteSpace(_settings.LocalFlux2NegativePrompt)) + { + if (!string.IsNullOrWhiteSpace(_settings.LocalFlux2NegativePromptNodeId)) + { + SetNodeInput(workflow, _settings.LocalFlux2NegativePromptNodeId, "text", _settings.LocalFlux2NegativePrompt); + } + else + { + var negative = WorkflowNodes(workflow) + .FirstOrDefault(n => HasInput(n.Node, "text") && IsTextEncodeNode(n.Node) && LooksNegative(n.Node)); + if (negative.Node != null) + { + SetInput(negative.Node, "text", _settings.LocalFlux2NegativePrompt); + } + } + } + } + + private static void ApplyImageSize(JObject workflow, int width, int height) + { + foreach (var (_, node) in WorkflowNodes(workflow)) + { + if (HasInput(node, "width")) SetInput(node, "width", width); + if (HasInput(node, "height")) SetInput(node, "height", height); + } + } + + private void ApplySeedAndSamplerSettings(JObject workflow) + { + var seed = _settings.LocalFlux2Seed ?? RandomNumberGenerator.GetInt32(1, int.MaxValue); + foreach (var (_, node) in WorkflowNodes(workflow)) + { + if (HasInput(node, "seed")) SetInput(node, "seed", seed); + if (HasInput(node, "noise_seed")) SetInput(node, "noise_seed", seed); + if (HasInput(node, "steps")) SetInput(node, "steps", _settings.LocalFlux2Steps); + + if (_settings.LocalFlux2Guidance > 0) + { + if (HasInput(node, "guidance")) SetInput(node, "guidance", _settings.LocalFlux2Guidance); + if (HasInput(node, "cfg")) SetInput(node, "cfg", _settings.LocalFlux2Guidance); + } + } + } + + private void ApplyFilenamePrefix(JObject workflow) + { + var prefix = $"MultiImageClient/local_flux2_{DateTime.UtcNow:yyyyMMdd_HHmmss}"; + foreach (var (_, node) in WorkflowNodes(workflow)) + { + if (HasInput(node, "filename_prefix")) SetInput(node, "filename_prefix", prefix); + } + } + + private static void ApplyNamedInput( + JObject workflow, + string value, + IEnumerable inputNames, + Func nodePredicate) + { + if (string.IsNullOrWhiteSpace(value)) return; + + var names = inputNames.ToHashSet(StringComparer.OrdinalIgnoreCase); + foreach (var (_, node) in WorkflowNodes(workflow)) + { + if (!nodePredicate(node)) continue; + var inputs = node["inputs"] as JObject; + if (inputs == null) continue; + + foreach (var prop in inputs.Properties().ToList()) + { + if (names.Contains(prop.Name)) + { + inputs[prop.Name] = value; + } + } + } + } + + private void ApplyExplicitOverrides(JObject workflow) + { + var overrides = _settings.LocalFlux2WorkflowInputOverrides; + if (overrides == null) return; + + foreach (var nodeOverride in overrides) + { + if (string.IsNullOrWhiteSpace(nodeOverride.Key)) continue; + if (workflow[nodeOverride.Key] is not JObject node || node["inputs"] is not JObject inputs) + { + throw new InvalidOperationException( + $"LocalFlux2WorkflowInputOverrides references missing node id '{nodeOverride.Key}'."); + } + + foreach (var inputOverride in nodeOverride.Value ?? new Dictionary()) + { + inputs[inputOverride.Key] = inputOverride.Value?.DeepClone() ?? JValue.CreateNull(); + } + } + } + + private async Task QueuePromptAsync(JObject workflow) + { + var body = new JObject + { + ["prompt"] = workflow, + ["client_id"] = Guid.NewGuid().ToString("N") + }; + using var content = new StringContent(body.ToString(Formatting.None), Encoding.UTF8, "application/json"); + using var response = await _httpClient.PostAsync($"{_endpoint}/prompt", content); + var responseText = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"ComfyUI /prompt failed: {(int)response.StatusCode} {response.ReasonPhrase}: {responseText}"); + } + + var parsed = JObject.Parse(responseText); + var promptId = parsed["prompt_id"]?.Value(); + if (string.IsNullOrWhiteSpace(promptId)) + { + throw new InvalidOperationException($"ComfyUI /prompt did not return prompt_id: {responseText}"); + } + return promptId; + } + + private async Task WaitForHistoryAsync(string promptId) + { + var timeout = _settings.LocalFlux2TimeoutSeconds <= 0 ? 900 : _settings.LocalFlux2TimeoutSeconds; + var deadline = DateTime.UtcNow.AddSeconds(timeout); + var pollDelay = TimeSpan.FromSeconds(1); + + while (DateTime.UtcNow < deadline) + { + using var response = await _httpClient.GetAsync($"{_endpoint}/history/{Uri.EscapeDataString(promptId)}"); + var responseText = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"ComfyUI /history failed: {(int)response.StatusCode} {response.ReasonPhrase}: {responseText}"); + } + + var historyRoot = string.IsNullOrWhiteSpace(responseText) + ? new JObject() + : JObject.Parse(responseText); + if (historyRoot[promptId] is JObject item) + { + var completed = item["status"]?["completed"]?.Value() == true; + if (completed) + { + var status = item["status"]?["status_str"]?.Value(); + if (!string.IsNullOrWhiteSpace(status) && !status.Equals("success", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"ComfyUI workflow failed with status '{status}': {ExtractHistoryError(item)}"); + } + return item; + } + } + + await Task.Delay(pollDelay); + } + + throw new TimeoutException($"Timed out waiting {timeout}s for ComfyUI prompt {promptId}."); + } + + private async Task> DownloadOutputImagesAsync(JObject historyItem) + { + var results = new List(); + if (historyItem["outputs"] is not JObject outputs) + { + return results; + } + + foreach (var output in outputs.Properties()) + { + if (output.Value is not JObject outputObject) continue; + if (outputObject["images"] is not JArray images) continue; + + foreach (var imageToken in images.OfType()) + { + var filename = imageToken["filename"]?.Value(); + if (string.IsNullOrWhiteSpace(filename)) continue; + + var type = imageToken["type"]?.Value() ?? "output"; + var subfolder = imageToken["subfolder"]?.Value() ?? ""; + var url = BuildViewUrl(filename, type, subfolder); + + using var response = await _httpClient.GetAsync(url); + var bytes = await response.Content.ReadAsByteArrayAsync(); + if (!response.IsSuccessStatusCode) + { + var body = Encoding.UTF8.GetString(bytes); + throw new HttpRequestException($"ComfyUI /view failed: {(int)response.StatusCode} {response.ReasonPhrase}: {body}"); + } + if (bytes.Length == 0) + { + throw new InvalidOperationException($"ComfyUI /view returned empty bytes for {filename}."); + } + + results.Add(new DownloadedImage + { + Bytes = bytes, + ContentType = response.Content.Headers.ContentType?.MediaType ?? "image/png" + }); + } + } + + return results; + } + + private string BuildViewUrl(string filename, string type, string subfolder) + { + var parts = new List + { + $"filename={Uri.EscapeDataString(filename)}", + $"type={Uri.EscapeDataString(type)}" + }; + if (!string.IsNullOrEmpty(subfolder)) + { + parts.Add($"subfolder={Uri.EscapeDataString(subfolder)}"); + } + + return $"{_endpoint}/view?{string.Join("&", parts)}"; + } + + private static IEnumerable<(string Id, JObject Node)> WorkflowNodes(JObject workflow) + { + foreach (var property in workflow.Properties()) + { + if (property.Value is JObject node && node["inputs"] is JObject) + { + yield return (property.Name, node); + } + } + } + + private static bool IsTextEncodeNode(JObject node) + { + var classType = NodeClass(node); + return classType.Contains("TextEncode", StringComparison.OrdinalIgnoreCase) + || classType.Contains("CLIPTextEncode", StringComparison.OrdinalIgnoreCase) + || classType.Contains("Conditioning", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsModelLoaderNode(JObject node) + { + var classType = NodeClass(node); + return classType.Contains("Loader", StringComparison.OrdinalIgnoreCase) + && (classType.Contains("UNET", StringComparison.OrdinalIgnoreCase) + || classType.Contains("Unet", StringComparison.OrdinalIgnoreCase) + || classType.Contains("Checkpoint", StringComparison.OrdinalIgnoreCase) + || classType.Contains("Diffusion", StringComparison.OrdinalIgnoreCase) + || classType.Contains("GGUF", StringComparison.OrdinalIgnoreCase)); + } + + private static bool IsTextEncoderLoaderNode(JObject node) + { + var classType = NodeClass(node); + return classType.Contains("Loader", StringComparison.OrdinalIgnoreCase) + && (classType.Contains("CLIP", StringComparison.OrdinalIgnoreCase) + || classType.Contains("TextEncoder", StringComparison.OrdinalIgnoreCase) + || classType.Contains("DualCLIP", StringComparison.OrdinalIgnoreCase) + || classType.Contains("TripleCLIP", StringComparison.OrdinalIgnoreCase) + || classType.Contains("GGUF", StringComparison.OrdinalIgnoreCase)); + } + + private static bool IsVaeLoaderNode(JObject node) + { + return NodeClass(node).Contains("VAE", StringComparison.OrdinalIgnoreCase) + && NodeClass(node).Contains("Loader", StringComparison.OrdinalIgnoreCase); + } + + private static bool LooksNegative(JObject node) + { + var title = node["_meta"]?["title"]?.Value() ?? ""; + var text = node["inputs"]?["text"]?.Value() ?? ""; + return title.Contains("negative", StringComparison.OrdinalIgnoreCase) + || text.Contains("negative", StringComparison.OrdinalIgnoreCase); + } + + private static string NodeClass(JObject node) + { + return node["class_type"]?.Value() ?? ""; + } + + private static bool HasInput(JObject node, string inputName) + { + return node["inputs"] is JObject inputs && inputs.Property(inputName) != null; + } + + private static void SetNodeInput(JObject workflow, string nodeId, string inputName, JToken value) + { + if (workflow[nodeId] is not JObject node || node["inputs"] is not JObject) + { + throw new InvalidOperationException($"ComfyUI workflow does not contain node id '{nodeId}'."); + } + SetInput(node, inputName, value); + } + + private static void SetInput(JObject node, string inputName, JToken value) + { + if (node["inputs"] is not JObject inputs) + { + throw new InvalidOperationException("ComfyUI workflow node is missing an inputs object."); + } + inputs[inputName] = value; + } + + private static string NormalizeEndpoint(string endpoint) + { + if (string.IsNullOrWhiteSpace(endpoint)) + { + endpoint = "http://127.0.0.1:8188"; + } + return endpoint.Trim().TrimEnd('/'); + } + + private static void EnsureEndpointAllowed(string endpoint, bool allowRemote) + { + if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var uri)) + { + throw new InvalidOperationException($"LocalFlux2ComfyEndpoint is not a valid absolute URL: {endpoint}"); + } + if (allowRemote) return; + + var host = uri.Host; + var local = host.Equals("localhost", StringComparison.OrdinalIgnoreCase) + || host.Equals("127.0.0.1") + || host.Equals("::1") + || host.Equals("[::1]"); + if (!local) + { + throw new InvalidOperationException( + $"Refusing to send local Flux2 prompts to non-local ComfyUI endpoint '{endpoint}'. Set LocalFlux2AllowRemoteEndpoint=true only for a trusted LAN host."); + } + } + + private static string EndpointHostLabel(string endpoint) + { + return Uri.TryCreate(endpoint, UriKind.Absolute, out var uri) + ? $"{uri.Host}:{uri.Port}" + : endpoint; + } + + private static string ExtractHistoryError(JObject historyItem) + { + var messages = historyItem["status"]?["messages"] as JArray; + if (messages == null) return "(no ComfyUI error details)"; + + foreach (var message in messages.OfType()) + { + if (message.Count < 2) continue; + var kind = message[0]?.Value() ?? ""; + if (!kind.Contains("error", StringComparison.OrdinalIgnoreCase)) continue; + return message[1]?.ToString(Formatting.None) ?? "(empty error)"; + } + return "(no ComfyUI error details)"; + } + + private class DownloadedImage + { + public byte[] Bytes { get; set; } + public string ContentType { get; set; } + } + } +} diff --git a/MultiImageClient/ImageGenerators/RecraftGenerator.cs b/MultiImageClient/ImageGenerators/RecraftGenerator.cs new file mode 100644 index 0000000..ea8717a --- /dev/null +++ b/MultiImageClient/ImageGenerators/RecraftGenerator.cs @@ -0,0 +1,261 @@ +using IdeogramAPIClient; + + +using RecraftAPIClient; + +using SixLabors.ImageSharp; + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public class RecraftGenerator : IImageGenerator + { + private SemaphoreSlim _recraftSemaphore; + private RecraftClient _recraftClient; + private HttpClient _httpClient; + private MultiClientRunStats _stats; + private static Random _Random = new Random(); + private RecraftStyle _style; + private RecraftVectorIllustrationSubstyle? _substyleVector; + private RecraftDigitalIllustrationSubstyle? _substyleDigital; + private RecraftRealisticImageSubstyle? _substyleRealistic; + private RecraftImageSize _imageSize; + private RecraftModel _model; + private ImageGeneratorApiType _apiType; + private string _artistic_level; + private string _name; + + public ImageGeneratorApiType ApiType => _apiType; + + private static ImageGeneratorApiType ApiTypeFor(RecraftModel model) => model switch + { + RecraftModel.recraftv4 => ImageGeneratorApiType.RecraftV4, + RecraftModel.recraftv4pro => ImageGeneratorApiType.RecraftV4Pro, + RecraftModel.recraftv4_1 => ImageGeneratorApiType.RecraftV41, + RecraftModel.recraftv4_1_pro => ImageGeneratorApiType.RecraftV41Pro, + _ => ImageGeneratorApiType.Recraft, + }; + + public string GetGeneratorSpecPart() + { + if (string.IsNullOrEmpty(_name)) + { + var usingSubstyle = ""; + if (_style == RecraftStyle.digital_illustration) + { + usingSubstyle = "\n" + _substyleDigital.ToString(); + } + else if (_style == RecraftStyle.realistic_image) + { + usingSubstyle = "\n" + _substyleRealistic.ToString(); + } + else if (_style == RecraftStyle.vector_illustration) + { + usingSubstyle = "\n"+_substyleVector.ToString(); + } + else if (_style == RecraftStyle.any) + { + usingSubstyle = ""; + } + else + { + throw new Exception("x"); + } + var alpart = ""; + if (!string.IsNullOrEmpty(_artistic_level) && _artistic_level != "0" ) + { + alpart = $"\nartistic level {_artistic_level}"; + } + var using2 = string.Join('\n', usingSubstyle.Split('\n').Where(el => !string.IsNullOrWhiteSpace(el))); + return $"{_model}\n{_style}\n{using2}{alpart}"; + } + else + { + return $"{_name}"; + } + } + + public RecraftGenerator(string apiKey, int maxConcurrency, RecraftImageSize size, RecraftStyle style, RecraftVectorIllustrationSubstyle? substyleVector, RecraftDigitalIllustrationSubstyle? substyleDigital, RecraftRealisticImageSubstyle? substyleRealistic, MultiClientRunStats stats, string name, string artistic_level = "", RecraftModel model = RecraftModel.recraftv3) + { + _recraftClient = new RecraftClient(apiKey); + _recraftSemaphore = new SemaphoreSlim(maxConcurrency); + _httpClient = new HttpClient(); + _artistic_level = artistic_level.ToString() ?? ""; + // so actually, "" + + + // we probably could use some validation here. + _style = style; + _substyleVector = substyleVector; + _substyleDigital = substyleDigital; + _substyleRealistic = substyleRealistic; + + _imageSize = size; + _stats = stats; + _name = string.IsNullOrEmpty(name) ? "" : name; + _model = model; + _apiType = ApiTypeFor(model); + } + + public string GetFilenamePart(PromptDetails pd) + { + var usingSubstyle = ""; + if (_style == RecraftStyle.digital_illustration) + { + usingSubstyle = _substyleDigital.ToString(); + } + else if (_style == RecraftStyle.realistic_image) + { + usingSubstyle = _substyleRealistic.ToString(); + } + else if (_style == RecraftStyle.vector_illustration) + { + usingSubstyle = _substyleVector.ToString(); + } + var res = $"{_model}{_name}_{_imageSize}_{_style}_{usingSubstyle}"; + return res; + } + + // https://www.recraft.ai/docs/api-reference/pricing + public decimal GetCost() + { + // Pro tiers charge a flat premium regardless of raster/vector style. + // V4.1 Pro is assumed to match V4 Pro until Recraft publishes a delta. + if (_model == RecraftModel.recraftv4pro || _model == RecraftModel.recraftv4_1_pro) + { + return _style == RecraftStyle.vector_illustration ? 0.30m : 0.25m; + } + + // V2 / V3 / V4 / V4.1 raster: $0.04 (V2: $0.022); vector: $0.08 (V2: $0.044). + var isVector = _style == RecraftStyle.vector_illustration; + return _model switch + { + RecraftModel.recraftv2 => isVector ? 0.044m : 0.022m, + _ => isVector ? 0.08m : 0.04m, + }; + } + + public List GetRightParts() + { + var usingSubstyle = ""; + if (_style == RecraftStyle.digital_illustration) + { + usingSubstyle = _substyleDigital.ToString(); + } + else if (_style == RecraftStyle.realistic_image) + { + usingSubstyle = _substyleRealistic.ToString(); + } + else if (_style == RecraftStyle.vector_illustration) + { + usingSubstyle = _substyleVector.ToString(); + } + var alpart = ""; + if (!string.IsNullOrEmpty(_artistic_level)) + { + alpart = $"artistic level {_artistic_level}"; + } + + var rightsideContents = new List() { _model.ToString(), _name, _style.ToString(), usingSubstyle, alpart }; + return rightsideContents; + } + + public async Task ProcessPromptAsync(IImageGenerator generator, PromptDetails promptDetails) + { + await _recraftSemaphore.WaitAsync(); + try + { + _stats.RecraftImageGenerationRequestCount++; + var usingPrompt = promptDetails.Prompt; + if (usingPrompt.Length > 1000) + { + usingPrompt = usingPrompt.Substring(0, 990); + Logger.Log("Truncating the prompt for Recraft."); + } + + + var usingSubstyle = ""; + if (_style == RecraftStyle.digital_illustration) + { + usingSubstyle = _substyleDigital.ToString(); + } + else if (_style == RecraftStyle.realistic_image) + { + usingSubstyle = _substyleRealistic.ToString(); + } + else if (_style == RecraftStyle.vector_illustration) + { + usingSubstyle = _substyleVector.ToString(); + } + else if (_style == RecraftStyle.any) + { + usingSubstyle = ""; + } + else + { + Console.WriteLine("err."); + usingSubstyle = "any"; + } + + usingSubstyle = Regex.Replace(usingSubstyle, @"^_([\d])", "$1"); + var generationResult = await _recraftClient.GenerateImageAsync(usingPrompt, _artistic_level, usingSubstyle, _style.ToString(), _imageSize, _model); + Logger.Log($"\tFrom Recraft: {promptDetails.Show()} '{generationResult.Created}'"); + _stats.RecraftImageGenerationSuccessCount++; + var theUrl = generationResult.Data[0].Url; + + var headResponse = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, theUrl)); + var contentType = headResponse.Content.Headers.ContentType?.MediaType; + + return new TaskProcessResult + { + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + IsSuccess = true, + Url = theUrl, + ContentType = contentType, + PromptDetails = promptDetails, + ImageGenerator = _apiType + }; + } + catch (Exception ex) + { + Logger.Log($"Recraft error: {ex.Message}"); + var jsonPart = ex.Message.Split(" - ").Last().Trim(); + + using var doc = JsonDocument.Parse(jsonPart); + var detailedError = doc.RootElement.GetProperty("code").GetString(); + return new TaskProcessResult { IsSuccess = false, ErrorMessage = detailedError, PromptDetails = promptDetails, ImageGenerator = _apiType, ImageGeneratorDescription = generator.GetGeneratorSpecPart() }; + } + finally + { + _recraftSemaphore.Release(); + } + } + + public string GetFullStyleName(string style, string substyle) + { + switch (style) + { + case "digital_illustration": + return $"{nameof(RecraftStyle.digital_illustration)} - {substyle}"; + case "realistic_image": + return $"{nameof(RecraftStyle.realistic_image)} - {substyle}"; + case "vector_illustration": + return $"{nameof(RecraftStyle.vector_illustration)} - {substyle}"; + case "any": + return "Any"; + default: + return "Unknown"; + } + } + } +} diff --git a/MultiImageClient/Implementation/AbstractPromptGenerator.cs b/MultiImageClient/Implementation/AbstractPromptGenerator.cs deleted file mode 100644 index 0a57ab3..0000000 --- a/MultiImageClient/Implementation/AbstractPromptGenerator.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.Emit; - -namespace MultiImageClient.Implementation -{ - /// - /// generate as much as you can of the prompt in here. - /// - public abstract class AbstractPromptGenerator - { - public abstract IEnumerable Prompts { get; } - public abstract Func CleanPrompt { get; } - public abstract int ImageCreationLimit { get; } - /// how many times we send this prompt as of this level. - public abstract int CopiesPer { get; } - - /// i.e. how many times we send it to each image generator, after applying all prior manipulation steps - public virtual int FullyResolvedCopiesPer { get; } = 3; - public abstract string Prefix { get; } - /// - /// Variant leading texts, if any, which you want to include after the global prefix. - /// - /// The overall structure we'll iterate through is: (copiesPer *) Prefix + Variant + Prompt + Suffix - /// - public abstract IEnumerable Variants { get; } - public abstract string Suffix { get; } - public abstract bool RandomizeOrder { get; } - public abstract string Name { get; } - - //defaults - public virtual bool UseClaude => true; - public virtual bool UseIdeogram => false; - public virtual bool UseBFL => true; - public virtual bool UseDalle3 => false; - public virtual bool SaveRaw => true; - public virtual bool SaveFullAnnotation => true; - public virtual bool SaveFinalPrompt => true; - public virtual bool SaveInitialIdea => true; - public virtual bool TryBothBFLUpsamplingAndNot => false; - - /// Whether or not claude rewriting succeeds, just toss in a raw direct version of whatever you have to the image generator anyway. - public virtual bool AlsoDoVersionSkippingClaude => false; - - public Settings Settings { get; set; } - - public AbstractPromptGenerator(Settings settings) - { - Settings = settings; - } - - /// Implementers should have their .Run method called from Program.cs to iterate through your prompts. - public IEnumerable Run() - { - var useVariants = new List() { "" }; - if (Variants != null && Variants.Count() > 0) - { - useVariants = Variants.ToList(); - } - var returnedCount = 0; - - foreach (var promptDetails in Prompts) - { - var cleanPrompt = CleanPrompt(promptDetails.Prompt).Trim(); - if (string.IsNullOrWhiteSpace(cleanPrompt)) - { - continue; - } - - foreach (var variantText in useVariants) - { - var usingPrompt = cleanPrompt; - if (!string.IsNullOrWhiteSpace(Prefix)) - { - usingPrompt = $"{Prefix} {cleanPrompt}".Trim(); - } - if (!string.IsNullOrWhiteSpace(variantText)) - { - usingPrompt = $"{variantText} {usingPrompt}".Trim(); - } - if (!string.IsNullOrWhiteSpace(Suffix)) - { - usingPrompt = $"{usingPrompt} {Suffix}".Trim(); - } - - if (usingPrompt != promptDetails.Prompt) - { - promptDetails.ReplacePrompt(usingPrompt, usingPrompt, TransformationType.Variants); - } - - for (var ii = 0; ii < CopiesPer; ii++) - { - var copy = promptDetails.Clone(); - yield return copy; - returnedCount++; - if (returnedCount >= ImageCreationLimit) yield break; - } - if (returnedCount >= ImageCreationLimit) yield break; - } - if (returnedCount >= ImageCreationLimit) yield break; - } - } - } -} \ No newline at end of file diff --git a/MultiImageClient/Implementation/CreatedBase64Image.cs b/MultiImageClient/Implementation/CreatedBase64Image.cs new file mode 100644 index 0000000..dcf37da --- /dev/null +++ b/MultiImageClient/Implementation/CreatedBase64Image.cs @@ -0,0 +1,9 @@ +namespace MultiImageClient +{ + /// some generators return multiple images per query. we sort of mix doing the query with getting the result including saving the new prompt + public class CreatedBase64Image + { + public string bytesBase64 { get; set; } + public string newPrompt { get; set; } + } +} diff --git a/MultiImageClient/Implementation/DlMirror.cs b/MultiImageClient/Implementation/DlMirror.cs new file mode 100644 index 0000000..2aeb2bd --- /dev/null +++ b/MultiImageClient/Implementation/DlMirror.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace MultiImageClient +{ + /// Mirrors every image we write into a single flat folder (configured + /// via ) so they're easy to + /// grab / share. Copies are synchronous and best-effort: failures are + /// logged but do not throw, since mirroring must never break a run. + /// + /// If the mirror path is null/empty, every call here is a no-op — this + /// is the default, and nothing is written unless the user has opted in + /// via settings. + /// + /// Collision policy: if a file with the same name already exists + /// and has the same length, skip (assume identical). Otherwise + /// append _1, _2, ... until unique. + public static class DlMirror + { + private static readonly HashSet ImageExtensions = new(StringComparer.OrdinalIgnoreCase) + { + ".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", + ".mp4", // Grok Imagine video output + }; + + /// Synchronously mirror one file to . + /// No-op when is null/empty (feature + /// disabled), or when the source doesn't exist. Returns true only + /// when a new file was actually created at the destination. + public static bool Copy(string sourceFilePath, string mirrorRoot) + { + if (string.IsNullOrWhiteSpace(mirrorRoot)) return false; + try + { + if (string.IsNullOrEmpty(sourceFilePath) || !File.Exists(sourceFilePath)) + { + return false; + } + Directory.CreateDirectory(mirrorRoot); + var filename = Path.GetFileName(sourceFilePath); + var dest = Path.Combine(mirrorRoot, filename); + + if (File.Exists(dest)) + { + var srcLen = new FileInfo(sourceFilePath).Length; + var dstLen = new FileInfo(dest).Length; + if (srcLen == dstLen) return false; // already mirrored + var stem = Path.GetFileNameWithoutExtension(filename); + var ext = Path.GetExtension(filename); + int i = 1; + do + { + dest = Path.Combine(mirrorRoot, $"{stem}_{i}{ext}"); + i++; + } while (File.Exists(dest)); + } + + File.Copy(sourceFilePath, dest, overwrite: false); + return true; + } + catch (Exception ex) + { + Logger.Log($"DlMirror: failed to copy {sourceFilePath} -> {mirrorRoot}: {ex.Message}"); + return false; + } + } + + /// One-shot backfill: walk and copy + /// every image file into . Returns + /// the number of newly-copied files. No-op if either path is + /// null/empty. + public static int Backfill(string savesRoot, string mirrorRoot) + { + if (string.IsNullOrWhiteSpace(mirrorRoot)) + { + Logger.Log("DlMirror.Backfill: no FlatImageMirrorPath configured; nothing to do."); + return 0; + } + if (string.IsNullOrWhiteSpace(savesRoot) || !Directory.Exists(savesRoot)) + { + Logger.Log($"DlMirror.Backfill: saves root '{savesRoot}' does not exist; nothing to do."); + return 0; + } + Directory.CreateDirectory(mirrorRoot); + + var files = Directory.EnumerateFiles(savesRoot, "*", SearchOption.AllDirectories) + .Where(p => ImageExtensions.Contains(Path.GetExtension(p))) + .ToList(); + + Logger.Log($"DlMirror.Backfill: scanning {files.Count} image file(s) under {savesRoot} -> {mirrorRoot}"); + + int copied = 0; + foreach (var f in files) + { + if (Copy(f, mirrorRoot)) copied++; + } + + Logger.Log($"DlMirror.Backfill: copied {copied} new file(s) into {mirrorRoot} (the rest were already present)."); + return copied; + } + } +} diff --git a/MultiImageClient/Implementation/GrokArchive.cs b/MultiImageClient/Implementation/GrokArchive.cs new file mode 100644 index 0000000..37ee531 --- /dev/null +++ b/MultiImageClient/Implementation/GrokArchive.cs @@ -0,0 +1,404 @@ +#nullable enable +using Newtonsoft.Json.Linq; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +using XAIGrokAPIClient; + +namespace MultiImageClient +{ + /// Reusable Grok history archiver/sync. Call SyncAsync(settings) any time + /// (or via the --grok-sync CLI flag) to bring the local archive up to + /// date. It is idempotent and incremental — already-downloaded assets are + /// skipped — so it doubles as the "keep local copies synced" mechanism. + /// + /// What xAI actually exposes, and therefore what each phase does: + /// + /// Phase 1 — Files API sweep. GET /v1/files is the ONLY enumerable + /// server-side history xAI offers. Videos we generate with + /// storage_options (which GrokImagineVideoGenerator now always + /// sets) land there permanently; this phase pages through the + /// whole inventory and downloads anything we don't have locally + /// into {ImageDownloadBaseFolder}\GrokArchive\. + /// + /// Phase 2 — request_id re-poll. Image generations are synchronous + /// and their URLs are temporary, so there is no server history for + /// them; but video request_ids recorded in the ledger can be + /// re-polled via GET /v1/videos/{id}. Any clip whose local file + /// went missing gets re-downloaded while xAI still has it. + /// + /// Phase 3 — JSON-log backfill. Reconstructs ledger entries (prompt, + /// url, local path) for every PRE-ledger Grok image/video by + /// scanning the per-image JSON logs under the saves tree. This is + /// the "back-read all prompts" part: after one sync, the entire + /// known Grok history lives in grok_ledger.jsonl. + public static class GrokArchive + { + private static readonly HashSet GrokGeneratorTypes = new() + { + ImageGeneratorApiType.GrokImagine, + ImageGeneratorApiType.GrokImaginePro, + ImageGeneratorApiType.GrokImagineVideo, + }; + + public static string GetArchiveFolder(Settings settings) + => Path.Combine(settings.ImageDownloadBaseFolder, "GrokArchive"); + + public static async Task SyncAsync(Settings settings) + { + if (string.IsNullOrWhiteSpace(settings.XAIGrokApiKey)) + { + Logger.Log("Grok sync: settings.json has no XAIGrokApiKey; nothing to do."); + return; + } + + var client = new XAIGrokClient(settings.XAIGrokApiKey); + var ledger = GrokLedger.ReadAll(settings); + Logger.Log($"Grok sync: ledger has {ledger.Count} entries ({GrokLedger.GetPath(settings)})"); + + var backfilled = BackfillLedgerFromJsonLogs(settings, ledger); + var downloadedFiles = await SyncFilesApiAsync(settings, client, ledger); + var repolled = await RepollMissingVideosAsync(settings, client, ledger); + + Logger.Log($"Grok sync complete: +{backfilled} ledger entries backfilled from logs, " + + $"{downloadedFiles} files downloaded from xAI storage, {repolled} videos re-fetched by request_id."); + Logger.Log($"Grok sync: archive folder {GetArchiveFolder(settings)}; ledger {GrokLedger.GetPath(settings)}"); + } + + /// Full export of the entire known Grok history to a folder OUTSIDE + /// the repo/saves tree (default C:\GrokArchive). Runs a sync first so + /// every remotely-recoverable asset is local, then copies every + /// ledger-known image and video into {dest}\Images / {dest}\Videos, + /// plus a copy of the ledger and a human-readable prompts.txt with + /// every prompt ever recorded. Incremental and rerunnable: files + /// already present with the same size are skipped, so this is also + /// the "keep the external copy synced" mechanism. + public static async Task ExportAsync(Settings settings, string destRoot) + { + await SyncAsync(settings); + + var ledger = GrokLedger.ReadAll(settings); + if (ledger.Count == 0) + { + Logger.Log("Grok export: ledger is empty; nothing to export."); + return; + } + + var imagesDir = Path.Combine(destRoot, "Images"); + var videosDir = Path.Combine(destRoot, "Videos"); + var otherDir = Path.Combine(destRoot, "Other"); + Directory.CreateDirectory(imagesDir); + Directory.CreateDirectory(videosDir); + Directory.CreateDirectory(otherDir); + + int copied = 0, skipped = 0, missing = 0; + // The same local file can appear in multiple ledger entries + // (live + sync); export each distinct path once. + var distinctPaths = ledger + .Where(e => !string.IsNullOrEmpty(e.LocalPath)) + .GroupBy(e => Path.GetFullPath(e.LocalPath!), StringComparer.OrdinalIgnoreCase) + .Select(g => (Path: g.Key, Entry: g.First())); + foreach (var (sourcePath, entry) in distinctPaths) + { + if (!File.Exists(sourcePath)) + { + missing++; + continue; + } + var destDir = entry.Kind switch + { + "video" => videosDir, + "image" => imagesDir, + _ => GuessKind(sourcePath) switch { "video" => videosDir, "image" => imagesDir, _ => otherDir }, + }; + var destPath = Path.Combine(destDir, Path.GetFileName(sourcePath)); + var sourceInfo = new FileInfo(sourcePath); + if (File.Exists(destPath) && new FileInfo(destPath).Length == sourceInfo.Length) + { + skipped++; + continue; + } + // Same name, different content: keep both. + int n = 1; + while (File.Exists(destPath) && new FileInfo(destPath).Length != sourceInfo.Length) + { + destPath = Path.Combine(destDir, + $"{Path.GetFileNameWithoutExtension(sourcePath)}_{n:D4}{Path.GetExtension(sourcePath)}"); + n++; + } + if (File.Exists(destPath) && new FileInfo(destPath).Length == sourceInfo.Length) + { + skipped++; + continue; + } + File.Copy(sourcePath, destPath); + copied++; + } + + WritePromptsFile(ledger, Path.Combine(destRoot, "prompts.txt")); + File.Copy(GrokLedger.GetPath(settings), Path.Combine(destRoot, "grok_ledger.jsonl"), overwrite: true); + + Logger.Log($"Grok export: {copied} files copied, {skipped} already present, {missing} ledger paths missing on disk."); + Logger.Log($"Grok export: complete archive at {destRoot} (Images\\, Videos\\, prompts.txt, grok_ledger.jsonl)."); + } + + /// One block per generation, newest last: timestamp, kind/model, and + /// the full prompt. The greppable "all prompts ever" companion to the + /// machine-readable ledger. + private static void WritePromptsFile(List ledger, string path) + { + var lines = new List + { + $"# Every recorded Grok prompt - exported {DateTime.Now:yyyy-MM-dd HH:mm:ss}", + $"# {ledger.Count} ledger entries total", + "", + }; + foreach (var e in ledger.Where(e => !string.IsNullOrWhiteSpace(e.Prompt)).OrderBy(e => e.TimestampUtc)) + { + lines.Add($"[{e.TimestampUtc:yyyy-MM-dd HH:mm:ss}Z] {e.Kind} ({e.Model ?? "?"})"); + lines.Add(e.Prompt!); + lines.Add(""); + } + File.WriteAllLines(path, lines); + } + + // ----- Phase 1: Files API sweep --------------------------------- + + private static async Task SyncFilesApiAsync(Settings settings, XAIGrokClient client, List ledger) + { + List remoteFiles; + try + { + remoteFiles = await client.ListAllFilesAsync(); + } + catch (Exception ex) + { + Logger.Log($"Grok sync: Files API list failed: {ex.Message}"); + return 0; + } + Logger.Log($"Grok sync: xAI Files API reports {remoteFiles.Count} stored files."); + + // A file counts as "have" when some ledger entry maps its file_id + // to a path that still exists on disk. + var haveFileIds = ledger + .Where(e => !string.IsNullOrEmpty(e.FileId) && !string.IsNullOrEmpty(e.LocalPath) && File.Exists(e.LocalPath)) + .Select(e => e.FileId!) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var archiveFolder = GetArchiveFolder(settings); + int downloaded = 0; + foreach (var file in remoteFiles) + { + if (haveFileIds.Contains(file.Id)) + { + continue; + } + try + { + var bytes = await client.DownloadFileContentAsync(file.Id); + var localPath = BuildArchivePath(archiveFolder, file); + Directory.CreateDirectory(archiveFolder); + await File.WriteAllBytesAsync(localPath, bytes); + DlMirror.Copy(localPath, settings.FlatImageMirrorPath); + downloaded++; + Logger.Log($"Grok sync: downloaded {file.Id} ({bytes.Length / 1024} KB) -> {localPath}"); + + var entry = new GrokLedgerEntry + { + Kind = GuessKind(file.Filename), + FileId = file.Id, + LocalPath = localPath, + Bytes = bytes.Length, + TimestampUtc = DateTimeOffset.FromUnixTimeSeconds(file.CreatedAt).UtcDateTime, + Source = "sync", + }; + GrokLedger.Append(settings, entry); + ledger.Add(entry); + } + catch (Exception ex) + { + Logger.Log($"Grok sync: failed to download file {file.Id} ('{file.Filename}'): {ex.Message}"); + } + } + return downloaded; + } + + private static string BuildArchivePath(string archiveFolder, XAIGrokFileObject file) + { + var created = DateTimeOffset.FromUnixTimeSeconds(file.CreatedAt).UtcDateTime; + // file ids look like "file_a128090d-..."; the first id chunk is + // plenty to guarantee uniqueness alongside the timestamp. + var idPart = file.Id.Replace("file_", "").Split('-')[0]; + var name = string.IsNullOrWhiteSpace(file.Filename) ? "unnamed.bin" : file.Filename; + var stem = FilenameGenerator.SanitizeFilename($"{created:yyyyMMddHHmmss}_{idPart}_{Path.GetFileNameWithoutExtension(name)}"); + var ext = Path.GetExtension(name); + if (string.IsNullOrEmpty(ext)) ext = ".bin"; + return Path.Combine(archiveFolder, stem + ext); + } + + private static string GuessKind(string? filename) + { + var ext = Path.GetExtension(filename ?? "").ToLowerInvariant(); + if (ext == ".mp4" || ext == ".mov" || ext == ".webm") return "video"; + if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".webp" || ext == ".gif") return "image"; + return "file"; + } + + // ----- Phase 2: re-poll known video request ids ------------------ + + private static async Task RepollMissingVideosAsync(Settings settings, XAIGrokClient client, List ledger) + { + // request_ids where NO ledger entry currently has a live local copy. + var byRequestId = ledger + .Where(e => !string.IsNullOrEmpty(e.RequestId)) + .GroupBy(e => e.RequestId!, StringComparer.OrdinalIgnoreCase) + .Where(g => !g.Any(e => !string.IsNullOrEmpty(e.LocalPath) && File.Exists(e.LocalPath))) + .ToList(); + if (byRequestId.Count == 0) + { + return 0; + } + Logger.Log($"Grok sync: {byRequestId.Count} video request_ids have no surviving local file; re-polling xAI."); + + using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(5) }; + var archiveFolder = GetArchiveFolder(settings); + int recovered = 0; + foreach (var group in byRequestId) + { + var requestId = group.Key; + var original = group.OrderByDescending(e => e.TimestampUtc).First(); + try + { + var result = await client.GetVideoAsync(requestId); + byte[]? bytes = null; + if (result.IsDone && !string.IsNullOrEmpty(result.Video?.Url)) + { + bytes = await http.GetByteArrayAsync(result.Video.Url); + } + else if (result.IsDone && !string.IsNullOrEmpty(result.Video?.FileOutput?.FileId)) + { + bytes = await client.DownloadFileContentAsync(result.Video.FileOutput.FileId); + } + if (bytes == null) + { + Logger.Log($"Grok sync: request {requestId} status={result.Status}; not recoverable."); + continue; + } + + Directory.CreateDirectory(archiveFolder); + var stem = FilenameGenerator.SanitizeFilename( + $"{DateTime.UtcNow:yyyyMMddHHmmss}_repoll_{requestId.Substring(0, Math.Min(12, requestId.Length))}"); + var localPath = Path.Combine(archiveFolder, stem + ".mp4"); + await File.WriteAllBytesAsync(localPath, bytes); + DlMirror.Copy(localPath, settings.FlatImageMirrorPath); + recovered++; + Logger.Log($"Grok sync: recovered video {requestId} ({bytes.Length / 1024} KB) -> {localPath}"); + + var entry = new GrokLedgerEntry + { + Kind = "video", + Model = result.Model ?? original.Model, + Prompt = original.Prompt, + RequestId = requestId, + FileId = result.Video?.FileOutput?.FileId, + RemoteUrl = result.Video?.Url, + LocalPath = localPath, + Bytes = bytes.Length, + Source = "sync", + }; + GrokLedger.Append(settings, entry); + ledger.Add(entry); + } + catch (Exception ex) + { + // Expected for old requests — xAI expires deferred results. + Logger.Log($"Grok sync: request {requestId} re-poll failed (likely expired): {ex.Message}"); + } + } + return recovered; + } + + // ----- Phase 3: back-read prompts from existing JSON logs --------- + + /// Every image save (with SaveJsonLog=true) wrote a per-image JSON + /// log containing the full PromptDetails, the remote URL, the local + /// save paths, and which generator made it. That tree IS our + /// pre-ledger Grok history; fold the Grok entries into the ledger so + /// "all prompts" are queryable in one place. + private static int BackfillLedgerFromJsonLogs(Settings settings, List ledger) + { + var root = settings.ImageDownloadBaseFolder; + if (!Directory.Exists(root)) + { + return 0; + } + + var knownLocalPaths = ledger + .Where(e => !string.IsNullOrEmpty(e.LocalPath)) + .Select(e => Path.GetFullPath(e.LocalPath!)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + int added = 0; + foreach (var logFile in Directory.EnumerateFiles(root, "*.json", SearchOption.AllDirectories)) + { + // only the per-image logs live in folders literally named "logs" + if (!string.Equals(Path.GetFileName(Path.GetDirectoryName(logFile)), "logs", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + try + { + var obj = JObject.Parse(File.ReadAllText(logFile)); + var generatorToken = obj["GeneratorUsed"]; + if (generatorToken == null) continue; + + ImageGeneratorApiType generator; + if (generatorToken.Type == JTokenType.Integer) + { + generator = (ImageGeneratorApiType)(int)generatorToken; + } + else if (!Enum.TryParse((string?)generatorToken, out generator)) + { + continue; + } + if (!GrokGeneratorTypes.Contains(generator)) continue; + + var rawPath = (string?)obj["SavedImagePaths"]?["Raw"]; + if (string.IsNullOrEmpty(rawPath)) continue; + var fullRawPath = Path.GetFullPath(rawPath); + if (knownLocalPaths.Contains(fullRawPath)) continue; + + var entry = new GrokLedgerEntry + { + Kind = generator == ImageGeneratorApiType.GrokImagineVideo ? "video" : "image", + TimestampUtc = (DateTime?)obj["Timestamp"] ?? DateTime.UtcNow, + Model = generator.ToString(), + Prompt = (string?)obj["PromptDetails"]?["Prompt"], + RemoteUrl = (string?)obj["GeneratedImageUrl"], + LocalPath = fullRawPath, + Bytes = File.Exists(fullRawPath) ? new FileInfo(fullRawPath).Length : 0, + Source = "log-backfill", + }; + GrokLedger.Append(settings, entry); + ledger.Add(entry); + knownLocalPaths.Add(fullRawPath); + added++; + } + catch (Exception ex) + { + Logger.Log($"Grok sync: skipping unparseable log {logFile}: {ex.Message}"); + } + } + if (added > 0) + { + Logger.Log($"Grok sync: backfilled {added} pre-ledger Grok generations from JSON logs into the ledger."); + } + return added; + } + } +} diff --git a/MultiImageClient/Implementation/GrokLedger.cs b/MultiImageClient/Implementation/GrokLedger.cs new file mode 100644 index 0000000..c5e8902 --- /dev/null +++ b/MultiImageClient/Implementation/GrokLedger.cs @@ -0,0 +1,112 @@ +#nullable enable +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.IO; + +namespace MultiImageClient +{ + /// One line of the Grok history ledger. Append-only JSONL — the local, + /// machine-readable record of every Grok generation we know about: + /// prompts, request ids, remote file ids, and where the bytes live on + /// disk. The sync workflow (GrokArchive) reads this to know what's + /// already downloaded and what still needs fetching. + public class GrokLedgerEntry + { + /// "image" | "video" | "file" (file = synced from the Files API + /// without a locally-known generation context). + [JsonProperty("kind")] + public string Kind { get; set; } = ""; + + [JsonProperty("timestampUtc")] + public DateTime TimestampUtc { get; set; } = DateTime.UtcNow; + + [JsonProperty("model")] + public string? Model { get; set; } + + [JsonProperty("prompt")] + public string? Prompt { get; set; } + + /// xAI deferred request id (videos only). + [JsonProperty("requestId")] + public string? RequestId { get; set; } + + /// xAI Files API file id, when the asset has a durable server copy. + [JsonProperty("fileId")] + public string? FileId { get; set; } + + /// The (usually temporary) xAI URL the asset was served from. + [JsonProperty("remoteUrl")] + public string? RemoteUrl { get; set; } + + [JsonProperty("localPath")] + public string? LocalPath { get; set; } + + [JsonProperty("bytes")] + public long Bytes { get; set; } + + /// "live" (recorded at generation time) | "sync" (pulled down by + /// GrokArchive) | "log-backfill" (reconstructed from old JSON logs). + [JsonProperty("source")] + public string Source { get; set; } = "live"; + } + + /// Append-only JSONL ledger at {ImageDownloadBaseFolder}\grok_ledger.jsonl. + /// Writes are lock-serialized and best-effort: ledger problems must never + /// break a generation run. + public static class GrokLedger + { + private static readonly object _writeLock = new object(); + + public static string GetPath(Settings settings) + => Path.Combine(settings.ImageDownloadBaseFolder, "grok_ledger.jsonl"); + + public static void Append(Settings? settings, GrokLedgerEntry entry) + { + if (settings == null || string.IsNullOrWhiteSpace(settings.ImageDownloadBaseFolder)) + { + return; + } + try + { + var line = JsonConvert.SerializeObject(entry, Formatting.None); + lock (_writeLock) + { + Directory.CreateDirectory(settings.ImageDownloadBaseFolder); + File.AppendAllText(GetPath(settings), line + Environment.NewLine); + } + } + catch (Exception ex) + { + Logger.Log($"GrokLedger: failed to append entry: {ex.Message}"); + } + } + + /// Reads every parseable line; malformed lines are skipped (the + /// ledger is advisory, not a database). + public static List ReadAll(Settings settings) + { + var result = new List(); + var path = GetPath(settings); + if (!File.Exists(path)) + { + return result; + } + foreach (var line in File.ReadAllLines(path)) + { + if (string.IsNullOrWhiteSpace(line)) continue; + try + { + var entry = JsonConvert.DeserializeObject(line); + if (entry != null) result.Add(entry); + } + catch + { + // skip malformed line + } + } + return result; + } + } +} diff --git a/MultiImageClient/Implementation/IImageGenerationService.cs b/MultiImageClient/Implementation/IImageGenerationService.cs deleted file mode 100644 index 3a63cac..0000000 --- a/MultiImageClient/Implementation/IImageGenerationService.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace MultiImageClient.Implementation -{ - public interface IImageGenerationService - { - Task ProcessPromptAsync(PromptDetails promptDetails, MultiClientRunStats stats); - } -} \ No newline at end of file diff --git a/MultiImageClient/Implementation/IImageGenerator.cs b/MultiImageClient/Implementation/IImageGenerator.cs deleted file mode 100644 index 33c360a..0000000 --- a/MultiImageClient/Implementation/IImageGenerator.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MultiImageClient.Implementation -{ - public interface IImageGenerator - { - Task ProcessPromptAsync(PromptDetails pd, MultiClientRunStats stats); - } -} diff --git a/MultiImageClient/Implementation/ITransformationStep.cs b/MultiImageClient/Implementation/ITransformationStep.cs deleted file mode 100644 index e15310e..0000000 --- a/MultiImageClient/Implementation/ITransformationStep.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading.Tasks; - -namespace MultiImageClient.Implementation -{ - public interface ITransformationStep - { - Task DoTransformation(PromptDetails pd, MultiClientRunStats stats); - string Name { get; } - } -} diff --git a/MultiImageClient/Implementation/ImageManager.cs b/MultiImageClient/Implementation/ImageManager.cs new file mode 100644 index 0000000..85cc5cb --- /dev/null +++ b/MultiImageClient/Implementation/ImageManager.cs @@ -0,0 +1,171 @@ +using ImageMagick; + +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection.Emit; +using System.Security.AccessControl; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace MultiImageClient +{ + public class ImageManager + { + private readonly Settings _settings; + private readonly MultiClientRunStats _stats; + + public ImageManager(Settings settings, MultiClientRunStats stats) + { + _settings = settings; + _stats = stats; + } + + public async Task> DoSaveAsync(int n, PromptDetails pd, string contentType, byte[] imageBytes, IImageGenerator generator, Settings settings) + { + var thesePaths = new Dictionary(); + if (imageBytes == null || imageBytes.Length == 0) + { + Logger.Log($"Empty or null image bytes received"); + throw new Exception("no bytes in the image data received; probably caller's problem.)"); + } + + // just one time, convert the bytes to png if needed. + if (contentType == "image/webp") + { + var fakeImage = new MagickImage(imageBytes, MagickFormat.WebP); + imageBytes = fakeImage.ToByteArray(MagickFormat.Png); + } + else if (contentType == "image/svg+xml") + { + var fakeImage = new MagickImage(imageBytes, MagickFormat.Svg); + imageBytes = fakeImage.ToByteArray(MagickFormat.Png); + } + else if (contentType == "image/jpeg") + { + var fakeImage = new MagickImage(imageBytes, MagickFormat.Jpg); + imageBytes = fakeImage.ToByteArray(MagickFormat.Png); + } + else if (contentType == "image/png") + { + //Console.WriteLine("png do nothing, all good"); + } + else if (contentType == null) + { + //Console.WriteLine("contentType null, so fall into .png"); + } + else + { + Console.WriteLine("some other weird contenttype. {result.ContentType}"); + } + + thesePaths[SaveType.Raw] = await ImageSaving.SaveImageAsync(pd, imageBytes, n, contentType, settings, SaveType.Raw, generator); + thesePaths[SaveType.FullAnnotation] = await ImageSaving.SaveImageAsync(pd, imageBytes, n, contentType, settings, SaveType.FullAnnotation, generator); + thesePaths[SaveType.FinalPrompt] = await ImageSaving.SaveImageAsync(pd, imageBytes, n, contentType, settings, SaveType.FinalPrompt, generator); + thesePaths[SaveType.InitialIdea] = await ImageSaving.SaveImageAsync(pd, imageBytes, n, contentType, settings, SaveType.InitialIdea, generator); + thesePaths[SaveType.JustOverride] = await ImageSaving.SaveImageAsync(pd, imageBytes, n, contentType, settings, SaveType.JustOverride, generator); + thesePaths[SaveType.Label] = await ImageSaving.SaveImageAsync(pd, imageBytes, n, contentType, settings, SaveType.Label, generator); + + + return thesePaths; + } + + public async Task ProcessAndSaveAsync(TaskProcessResult result, IImageGenerator generator) + { + try + { + if (!result.IsSuccess) + { + Console.WriteLine("failur."); + return result; + } + var sw = Stopwatch.StartNew(); + byte[] imageBytes; + + if (!string.IsNullOrEmpty(result.Url)) + { + imageBytes = await ImageSaving.DownloadImageAsync(result); + result.DownloadTotalMs = sw.ElapsedMilliseconds; + // downloading it can just fail sometimes. + result.SetImageBytes(0, imageBytes); + var pd = result.PromptDetails.Copy(); + var downloadResults = await DoSaveAsync(0, pd, result.ContentType, imageBytes, generator, _settings); + await SaveJsonLogAsync(result, downloadResults); + return result; + } + else + { + var ii = 0; + foreach (var qq in result.Base64ImageDatas) + { + imageBytes = Convert.FromBase64String(qq.bytesBase64); + result.SetImageBytes(ii, imageBytes); + var pd = result.PromptDetails.Copy(); + + if (pd.Prompt != qq.newPrompt && !string.IsNullOrEmpty(qq.newPrompt)) + { + + if (generator.ApiType == ImageGeneratorApiType.GoogleImagen4) + { + pd.AddStep(qq.newPrompt, TransformationType.Imagen4Rewrite); + } + else + { + Console.WriteLine("s"); + } + } + var downloadResults = await DoSaveAsync(ii, pd, result.ContentType, imageBytes, generator, _settings); + ii++; + await SaveJsonLogAsync(result, downloadResults); + } + result.DownloadTotalMs = sw.ElapsedMilliseconds; + return result; + } + + + } + catch (Exception ex) + { + Logger.Log($"\tAn error occurred while processing a task: {ex.Message}"); + result.ErrorMessage = ex.Message; + return result; + } + } + + private async Task SaveJsonLogAsync(TaskProcessResult result, Dictionary savedImagePaths) + { + if (!_settings.SaveJsonLog) return; + + var jsonLog = new + { + Timestamp = DateTime.UtcNow, + result.PromptDetails, + GeneratedImageUrl = result.Url, + SavedImagePaths = savedImagePaths, + GeneratorUsed = result.ImageGenerator, + result.ErrorMessage, + }; + + string jsonString = JsonConvert.SerializeObject(jsonLog, Formatting.Indented); + + if (savedImagePaths.TryGetValue(SaveType.Raw, out string rawImagePath)) + { + string baseDirectory = Path.GetDirectoryName(rawImagePath); + string logsDirectory = Path.Combine(baseDirectory, "logs"); + Directory.CreateDirectory(logsDirectory); + + string logFileName = Path.GetFileNameWithoutExtension(rawImagePath) + ".json"; + string jsonFilePath = Path.Combine(logsDirectory, logFileName); + + await File.WriteAllTextAsync(jsonFilePath, jsonString); + } + else + { + Logger.Log("\tUnable to save JSON log: Raw image path not found."); + } + } + } +} diff --git a/MultiImageClient/Implementation/ImageSaving.cs b/MultiImageClient/Implementation/ImageSaving.cs deleted file mode 100644 index ea4fedd..0000000 --- a/MultiImageClient/Implementation/ImageSaving.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.Threading.Tasks; -using Newtonsoft.Json; -using System.Collections.Generic; -using System.IO; - -using IdeogramAPIClient; -using System.Linq; -using MultiImageClient.Enums; - -namespace MultiImageClient -{ - public enum SaveType - { - Raw = 1, - FullAnnotation = 2, - InitialIdea = 3, - FinalPrompt = 4 - } - - public static class ImageSaving - { - public static async Task SaveImageAsync( - byte[] imageBytes, - TaskProcessResult result, - Settings settings, - MultiClientRunStats stats, - SaveType saveType, - string promptGeneratorName) - { - string todayFolder = DateTime.Now.ToString("yyyy-MM-dd-dddd"); - string baseFolder = Path.Combine(settings.ImageDownloadBaseFolder, todayFolder); - - if (saveType != SaveType.Raw) - { - baseFolder = Path.Combine(baseFolder, saveType.ToString()); - } - - Directory.CreateDirectory(baseFolder); - var safeFilename = TextUtils.GenerateUniqueFilename(result, baseFolder, promptGeneratorName, saveType); - var fullPath = Path.Combine(baseFolder, $"{safeFilename}{result.ImageGenerator.GetFileExtension()}"); - - try - { - if (File.Exists(fullPath)) - { - Console.WriteLine("Overwriting!", fullPath); - throw new Exception("no overwriting!"); - } - await File.WriteAllBytesAsync(fullPath, imageBytes); - if (saveType == SaveType.Raw) - { - Console.WriteLine($"\tSaved {saveType} image. Fp: {fullPath}"); - } - - if (saveType == SaveType.Raw) - { - stats.SavedRawImageCount++; - } - else - { - await AddAnnotationsAsync(imageBytes, result, fullPath, stats, saveType, promptGeneratorName); - stats.SavedAnnotatedImageCount++; - } - } - catch (Exception ex) - { - Console.WriteLine($"\tError saving {saveType} image: {ex.Message}"); - } - - return fullPath; - } - - private static async Task AddAnnotationsAsync( - byte[] imageBytes, - TaskProcessResult result, - string fullPath, - MultiClientRunStats stats, - SaveType saveType, - string promptGeneratorName) - { - var generator = result.ImageGenerator; - var promptDetails = result.PromptDetails; - var imageInfo = new Dictionary(); - var usingSteps = promptDetails.TransformationSteps; - switch (saveType) - { - case SaveType.FullAnnotation: - - AddFullAnnotationInfo(imageInfo, generator, promptDetails, promptGeneratorName); - imageInfo.Add("Filename", Path.GetFileName(fullPath)); - usingSteps = promptDetails.TransformationSteps; - break; - case SaveType.InitialIdea: - - var initialPrompt = promptDetails.TransformationSteps.First().Explanation; - imageInfo.Add("Producer", generator.ToString()); - imageInfo.Add("Initial Prompt", initialPrompt); - usingSteps = new List(); - break; - case SaveType.FinalPrompt: - - var finalPrompt = promptDetails.Prompt; - imageInfo.Add("Producer", generator.ToString()); - imageInfo.Add("Final Prompt", finalPrompt); - usingSteps = new List(); - break; - case SaveType.Raw: - imageInfo = new Dictionary(); - usingSteps = new List(); - break; - } - - imageInfo = imageInfo.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value)) - .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - - await TextFormatting.SaveImageAndAnnotateText( - imageBytes, - usingSteps, - imageInfo, - fullPath - ); - } - - private static void AddFullAnnotationInfo(Dictionary imageInfo, ImageGeneratorApiType generator, PromptDetails promptDetails, string promptGeneratorName) - { - - switch (generator) - { - case ImageGeneratorApiType.Ideogram: - imageInfo.Add("Generator", "Ideogram V2"); - var ideogramDetails = promptDetails.IdeogramDetails; - if (ideogramDetails.Model != default) - imageInfo.Add("Model", ideogramDetails.Model.ToString()); - if (ideogramDetails.AspectRatio.HasValue) - imageInfo.Add("AspectRatio", IdeogramUtils.StringifyAspectRatio(ideogramDetails.AspectRatio.Value)); - if (ideogramDetails.StyleType.HasValue) - imageInfo.Add("StyleType", ideogramDetails.StyleType.Value.ToString()); - if (!string.IsNullOrWhiteSpace(ideogramDetails.NegativePrompt)) - imageInfo.Add("NegativePrompt", ideogramDetails.NegativePrompt); - break; - case ImageGeneratorApiType.BFL: - var bflDetails = promptDetails.BFLDetails; - - imageInfo.Add("Generator", "BFL Flux 1.1"); - //imageInfo.Add("Rewriting prompt", bflDetails.PromptUpsampling.ToString()); - if (bflDetails.Seed != default) - imageInfo.Add("Seed", bflDetails.Seed.Value.ToString()); - if (bflDetails.Width != default && bflDetails.Height != default) - imageInfo.Add("Size", $"{bflDetails.Width}x{bflDetails.Height}"); - if (bflDetails.SafetyTolerance != default) - imageInfo.Add("SafetyTolerance", bflDetails.SafetyTolerance.ToString()); - break; - case ImageGeneratorApiType.Dalle3: - imageInfo.Add("Generator", "Dall-e 3"); - var dalle3Details = promptDetails.Dalle3Details; - imageInfo.Add("Size", $"{dalle3Details.Size}"); - imageInfo.Add("Quality", dalle3Details.Quality.ToString()); - break; - } - imageInfo.Add("Kind", promptGeneratorName); - imageInfo.Add("Generated", DateTime.Now.ToString()); - } - } -} diff --git a/MultiImageClient/Implementation/MultiClientRunStats.cs b/MultiImageClient/Implementation/MultiClientRunStats.cs deleted file mode 100644 index 473d697..0000000 --- a/MultiImageClient/Implementation/MultiClientRunStats.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MultiImageClient -{ - public class MultiClientRunStats - { - public int SavedRawImageCount { get; set; } - public int SavedAnnotatedImageCount { get; set; } - public int SavedJsonLogCount { get; set; } - public int IdeogramRequestCount { get; set; } - public int IdeogramRefusedCount { get; set; } - public int ClaudeRequestCount { get; set; } - public int ClaudeWouldRefuseCount { get; set; } - public int ClaudeRefusedCount { get; set; } - public int ClaudeRewroteCount { get; set; } - public int Dalle3RequestCount { get; set; } - public int Dalle3RefusedCount { get; set; } - public int BFLImageGenerationRequestCount { get; set; } - public int BFLImageGenerationSuccessCount { get; set; } - public int BFLImageGenerationErrorCount { get; set; } - - public string PrintStats() - { - var nonZeroStats = new List(); - - if (SavedRawImageCount > 0) - nonZeroStats.Add($"Raw Image Saved:{SavedRawImageCount}"); - if (SavedJsonLogCount > 0) - nonZeroStats.Add($"JSON:{SavedJsonLogCount}"); - - if (ClaudeWouldRefuseCount > 0) - nonZeroStats.Add($"Claude Would Refuse:{ClaudeWouldRefuseCount}"); - if (ClaudeRequestCount > 0) - nonZeroStats.Add($"Claude Requests:{ClaudeRequestCount}"); - if (ClaudeRefusedCount > 0) - nonZeroStats.Add($"Claude Refused:{ClaudeRefusedCount}"); - if (ClaudeRewroteCount > 0) - nonZeroStats.Add($"Claude Accepted:{ClaudeRewroteCount}"); - - if (IdeogramRequestCount > 0) - nonZeroStats.Add($"Ideogram Requests:{IdeogramRequestCount}"); - if (IdeogramRefusedCount > 0) - nonZeroStats.Add($"Ideogram Refused:{IdeogramRefusedCount}"); - - if (Dalle3RequestCount > 0) - nonZeroStats.Add($"Dalle3 Requests:{Dalle3RequestCount}"); - if (Dalle3RefusedCount > 0) - nonZeroStats.Add($"Dalle3 Requests:{Dalle3RefusedCount}"); - - if (BFLImageGenerationRequestCount > 0 | BFLImageGenerationErrorCount>0 | BFLImageGenerationRequestCount > 0) - nonZeroStats.Add($"BFL: total:{BFLImageGenerationRequestCount}, ok:{BFLImageGenerationSuccessCount}, bad:{BFLImageGenerationErrorCount} "); - - return $"Stats: {string.Join(", ", nonZeroStats)}"; - } - } -} \ No newline at end of file diff --git a/MultiImageClient/promptGenerators/PromptDetails.cs b/MultiImageClient/Implementation/PromptDetails.cs similarity index 50% rename from MultiImageClient/promptGenerators/PromptDetails.cs rename to MultiImageClient/Implementation/PromptDetails.cs index 29e33a2..a341f6c 100644 --- a/MultiImageClient/promptGenerators/PromptDetails.cs +++ b/MultiImageClient/Implementation/PromptDetails.cs @@ -1,110 +1,117 @@ -using System; -using System.Collections.Generic; - -using IdeogramAPIClient; - - -namespace MultiImageClient -{ - public class PromptDetails - { - - /// This should track the "active" prompt which the next step in the process should care about. Earlier versions are in ImageConstructionSteps. - /// To modify it call ReplacePrompt to also fix up history. - public string Prompt { get; set; } - - /// - /// If a generator wants the file to have a specific filename, it should fill this in. that is, regardless of whatever happens to the prompt after the transformation, or even the initail actual prompt, just use this. For example if you are generating a deck of cards, you might just set this up to say "2 or hearts" even if your raw original prompt for even the first step is "two hearts entwined in the style of jack vance" etc. This way the resulting filenames will make sense. - /// - public string OverrideFilename { get; set; } = ""; - - public IList TransformationSteps { get; set; } = new List(); - - /// Send these along and maybe one of the image consumers can use them to make a better image path etc.? - public BFLDetails BFLDetails { get; set; } - - public IdeogramDetails IdeogramDetails { get; set; } - public Dalle3Details Dalle3Details { get; set; } - - /// - /// This item's "Prompt" field is always the active prompt which will be really used to send to external consuemrs - /// however, often times it is really composed of like: $"Outer prompt stuff {previous version} Tail suffix stuff" and for user understanding we oftenw ant to show that. - /// So when you revise a prompt, always call this with the actual prompt, the description of what this transformation step is, and include a details field which is what we'll use to explain to users in the annotation, etc. - /// Explanation should be the full prompt, but can ALSO be preceded by any step-specific details such as temperature. - /// - /// This is a helper method which forces callers to fill in the history of a request when they change the prompt. That way we won't lose track of it. - /// - public void ReplacePrompt(string newPrompt, string explanation, TransformationType transformationType) - { - var currentPromptText = Prompt; - if (string.IsNullOrEmpty(currentPromptText)) - { - - } - else - { - explanation = explanation.Replace(currentPromptText, "{PROMPT}"); - } - - var item = new PromptHistoryStep(newPrompt, explanation, transformationType); - TransformationSteps.Add(item); - Prompt = newPrompt.Trim(); - } - - public string Show() - { - var shortText = Prompt.Length > 150 ? Prompt.Substring(0, 150) + "..." : Prompt; - return shortText; - } - - public void UndoLastStep() - { - if (TransformationSteps.Count > 0) - { - TransformationSteps.RemoveAt(TransformationSteps.Count - 1); - - if (TransformationSteps.Count > 0) - { - Prompt = TransformationSteps[TransformationSteps.Count - 1].Prompt; - } - else - { - Prompt = string.Empty; - } - } - } - - public void AddStep(string stepDescription, TransformationType transformationType) - { - var item = new PromptHistoryStep(Prompt, stepDescription, transformationType); - TransformationSteps.Add(item); - } - - public PromptDetails Clone() - { - var clone = new PromptDetails - { - Prompt = Prompt, - OverrideFilename = OverrideFilename, - BFLDetails = BFLDetails != null ? new BFLDetails(BFLDetails) : null, - IdeogramDetails = IdeogramDetails != null ? new IdeogramDetails(IdeogramDetails) : null, - Dalle3Details = Dalle3Details != null ? new Dalle3Details(Dalle3Details) : null, - TransformationSteps = new List() - }; - - foreach (var step in TransformationSteps) - { - clone.TransformationSteps.Add(new PromptHistoryStep(step)); - } - - return clone; - } - - //a method to make mousing over this object show the basic info on it: - public override string ToString() - { - - return $"{Prompt}"; - } - } + +using System.Collections.Generic; + +using IdeogramAPIClient; +using BFLAPIClient; +using RecraftAPIClient; +using System.Linq; + +namespace MultiImageClient +{ + /// + /// This is generic at above the generator level. This means that generators which allow specification of specific styles will be tough since other ones won't accept those params! + /// + public class PromptDetails + { + // ugh this is back now. + public PromptDetails Copy() + { + var res = new PromptDetails(); + res.ReplacePrompt(Prompt, Prompt, TransformationType.InitialPrompt); + foreach (var step in TransformationSteps) + { + res.AddStep(step.Explanation, step.TransformationType); + } + return res; + } + + public string Prompt { get; set; } + + /// + /// These are for programmatic manipulation. For example, a user of this program first adds a concept like "fire" then maps that into 4 different variants by using an internal Claude client or something like that. + /// Then it'll be sent to the actual image generator (which may do more steps of its own). For now these steps are both handled by this class; the only latter type is when the remote thing say, rewrites the prompt again. + /// For us things to do are cool ones like: choose an aspect ratio. + /// + public IList TransformationSteps { get; set; } = new List(); + + /// + /// Free-form per-call runtime metadata set by the generator that processed this prompt + /// (e.g. the randomly-chosen size/quality for gpt-image-2). Lets the filename builder and + /// the combined-image label reflect the actual values used for this particular request + /// rather than a static default on the generator instance. + /// Keys by convention: "size", "quality", "label". + /// + public Dictionary RuntimeMeta { get; set; } = new Dictionary(); + + public PromptDetails() { } + + /// + /// These are generally for internal prompt manipulations before we send it out anywhere. So they'll mostly be shared before going out to an actual image generator. + /// + public void ReplacePrompt(string newPrompt, string explanation, TransformationType transformationType) + { + ReplacePrompt(newPrompt, explanation, transformationType, null); + } + + /// + /// This item's "Prompt" field is always the active prompt which will be really used to send to external consuemrs + /// however, often times it is really composed of like: $"Outer prompt stuff {previous version} Tail suffix stuff" and for user understanding we oftenw ant to show that. + /// So when you revise a prompt, always call this with the actual prompt, the description of what this transformation step is, and include a details field which is what we'll use to explain to users in the annotation, etc. + /// Explanation should be the full prompt, but can ALSO be preceded by any step-specific details such as temperature. + /// + /// This is a helper method which forces callers to fill in the history of a request when they change the prompt. That way we won't lose track of it. + /// + public void ReplacePrompt(string newPrompt, string explanation, TransformationType transformationType, PromptReplacementMetadata promptReplacementMetadata) + { + var currentPromptText = Prompt; + if (string.IsNullOrEmpty(currentPromptText)) + { + + } + else + { + explanation = explanation.Replace(currentPromptText, "{PROMPT}"); + } + + var item = new PromptHistoryStep(newPrompt, explanation, transformationType, promptReplacementMetadata); + TransformationSteps.Add(item); + Prompt = newPrompt.Trim(); + } + + public string Show() + { + var parts = new List(); + + + var detailsPart = string.Empty; + if (parts.Count > 0) + { + detailsPart = $" {string.Join(", ", parts)}"; + } + return $"\'{Prompt}\' {detailsPart}"; + } + + public void UndoLastStep() + { + if (TransformationSteps.Count > 0) + { + TransformationSteps.RemoveAt(TransformationSteps.Count - 1); + + if (TransformationSteps.Count > 0) + { + Prompt = TransformationSteps[TransformationSteps.Count - 1].Prompt; + } + else + { + Prompt = string.Empty; + } + } + } + + public void AddStep(string stepDescription, TransformationType transformationType) + { + var item = new PromptHistoryStep(Prompt, stepDescription, transformationType); + TransformationSteps.Add(item); + } + } } \ No newline at end of file diff --git a/MultiImageClient/Implementation/SaveType.cs b/MultiImageClient/Implementation/SaveType.cs new file mode 100644 index 0000000..c1f502c --- /dev/null +++ b/MultiImageClient/Implementation/SaveType.cs @@ -0,0 +1,15 @@ +namespace MultiImageClient +{ + /// + /// The annotation type on a saved image generated from a prompt via a number of steps. + /// + public enum SaveType + { + Raw = 1, + FullAnnotation = 2, + InitialIdea = 3, + FinalPrompt = 4, + JustOverride = 5, // when w generate the prompt sometimes we just have a core word/phrase called the which we want to make visible in both the filename and in this version with the subtitle for illustrative purposes. If you get one of these then just draw the text large, centered, in a nice font, with no other junk. + Label = 6, + } +} diff --git a/MultiImageClient/Implementation/TaskProcessResult.cs b/MultiImageClient/Implementation/TaskProcessResult.cs index cfb2fd6..5be9553 100644 --- a/MultiImageClient/Implementation/TaskProcessResult.cs +++ b/MultiImageClient/Implementation/TaskProcessResult.cs @@ -1,34 +1,75 @@ -using IdeogramAPIClient; -using MultiImageClient.Enums; - -namespace MultiImageClient -{ - /// Response type for async text rewrites or image generation steps. - public class TaskProcessResult - { - public bool IsSuccess { get; set; } - public GenericImageGenerationErrorType GenericImageErrorType { get; set; } = 0; - public GenericTextGenerationErrorType GenericTextErrorType { get; set; } = 0; - - public string ErrorMessage { get; set; } - public string Url { get; set; } - - //The original request data and all changes etc. - public PromptDetails PromptDetails { get; set; } - public ImageGeneratorApiType ImageGenerator { get; set; } - public TextGeneratorApiType TextGenerator { get; set; } - - public override string ToString() - { - if (GenericImageErrorType != 0) - { - return $"Error: {GenericImageErrorType} {ErrorMessage}"; - } - if (GenericTextErrorType != 0) - { - return $"Error: {GenericTextErrorType} {ErrorMessage}"; - } - return $"Success. {PromptDetails}"; - } - } -} \ No newline at end of file +using Google.Protobuf; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace MultiImageClient +{ + + public class TaskProcessResult + { + public bool IsSuccess { get; set; } + public GenericImageGenerationErrorType GenericImageErrorType { get; set; } = 0; + public GenericTextGenerationErrorType GenericTextErrorType { get; set; } = 0; + + public string ErrorMessage { get; set; } + + // to make multi + public string Url { get; set; } + + /// gpt-image-1 returns the data as base64 encoded string, so we have already decoded it and just have it here. + /// so, sometimes guy won't have Url but will have the image data. + public IEnumerable Base64ImageDatas { get; set; } = new List(); + public string ContentType { get; set; } + public PromptDetails PromptDetails { get; set; } + public ImageGeneratorApiType ImageGenerator { get; set; } + public required string ImageGeneratorDescription { get; set; } + public TextGeneratorApiType TextGenerator { get; set; } + public long CreateTotalMs { get; set; } = 0; + public long DownloadTotalMs { get; set; } = 0; + private Dictionary _ImageBytes { get; set; } = new Dictionary(); + public IEnumerable GetAllImages + { + get { return _ImageBytes.Values; } + } + + public void SetImageBytes(int n, byte[] imageBytes) + { + if (imageBytes == null || imageBytes.Length == 0) + { + IsSuccess = false; + GenericImageErrorType = GenericImageGenerationErrorType.NoImagesGenerated; + ErrorMessage = "No image data."; + return; + } + if (_ImageBytes.ContainsKey(n)) + { + throw new Exception("double"); + } + + _ImageBytes[n] = imageBytes; + } + + + public override string ToString() + { + if (GenericImageErrorType != 0) + return $"Error: {GenericImageErrorType} {ErrorMessage}"; + if (GenericTextErrorType != 0) + return $"Error: {GenericTextErrorType} {ErrorMessage}"; + + return $"Success. {PromptDetails}"; + } + + internal byte[] GetImageBytes(int n) + { + if (_ImageBytes == null) + { + throw new Exception("No image bytes set."); + } + return _ImageBytes[n]; + } + } +} diff --git a/MultiImageClient/Implementation/TextFormatting.cs b/MultiImageClient/Implementation/TextFormatting.cs deleted file mode 100644 index 56407cc..0000000 --- a/MultiImageClient/Implementation/TextFormatting.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing.Imaging; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using System.Reflection; -using System.Timers; -using System.Text; -using System.Windows.Forms; - -using IdeogramAPIClient; -using System.Text.RegularExpressions; - -namespace MultiImageClient -{ - public static class TextFormatting - { - private static readonly float KEY_WIDTH_PROPORTION = 0.15f; - private static readonly float VALUE_WIDTH_PROPORTION = 1f - KEY_WIDTH_PROPORTION; - - private static void DrawKeyValuePair(Graphics graphics, PromptHistoryStep step, float fontSize, float x, float keyWidth, float valueWidth, ref float y) - { - using (var whiteBrush = new SolidBrush(Color.White)) - using (var blackBrush = new SolidBrush(Color.Black)) - { - var key = $"{step.TransformationType}"; - var value = $"{step.Explanation}"; - - using (var keyFont = GetSupportedFont(key, fontSize, FontStyle.Regular)) - using (var valueFont = GetMonospacedFont(value, fontSize, FontStyle.Regular)) - { - float lineHeight = Math.Max(keyFont.GetHeight(graphics), valueFont.GetHeight(graphics)) + 2; - StringFormat stringFormat = new StringFormat - { - Trimming = StringTrimming.Word, - FormatFlags = StringFormatFlags.LineLimit | StringFormatFlags.MeasureTrailingSpaces - }; - - // Measure the height required for key and value - var keySizeMeasured = graphics.MeasureString(key, keyFont, new SizeF(keyWidth, float.MaxValue), stringFormat); - var valueSizeMeasured = graphics.MeasureString(value, valueFont, new SizeF(valueWidth, float.MaxValue), stringFormat); - - float maxHeight = Math.Max(keySizeMeasured.Height, valueSizeMeasured.Height); - - // Draw key - graphics.FillRectangle(Brushes.Black, x, y, keyWidth, maxHeight); - graphics.DrawString(key, keyFont, whiteBrush, new RectangleF(x, y, keyWidth, maxHeight), stringFormat); - - // Draw value - graphics.FillRectangle(Brushes.White, x + keyWidth, y, valueWidth, maxHeight); - graphics.DrawString(value, valueFont, blackBrush, new RectangleF(x + keyWidth, y, valueWidth, maxHeight), stringFormat); - - y += maxHeight + 5; // Move to the next line with some spacing - } - } - } - - private static void DrawImageInfo(Graphics graphics, Dictionary imageInfo, Font font, float x, float totalWidth, ref float y) - { - const float padding = 5f; - float lineHeight = font.GetHeight(graphics); - float currentX = x; - float maxY = y; - - using (var whiteBrush = new SolidBrush(Color.White)) - using (var blackBrush = new SolidBrush(Color.Black)) - { - StringFormat stringFormat = new StringFormat - { - Trimming = StringTrimming.Word, - FormatFlags = StringFormatFlags.LineLimit | StringFormatFlags.MeasureTrailingSpaces - }; - - foreach (var kvp in imageInfo) - { - float itemKeyWidth = graphics.MeasureString(kvp.Key, font).Width + padding; - float maxValueWidth = totalWidth - itemKeyWidth; - - // Measure the actual width needed for the value - SizeF valueSizeMeasured = graphics.MeasureString(kvp.Value, font, new SizeF(maxValueWidth, float.MaxValue), stringFormat); - float actualValueWidth = Math.Min(valueSizeMeasured.Width, maxValueWidth); - - // Check if we need to move to the next line - if (currentX + itemKeyWidth + actualValueWidth > x + totalWidth) - { - currentX = x; - y = maxY + padding; - } - - // Draw key (white on black) - graphics.FillRectangle(Brushes.Black, currentX, y, itemKeyWidth, valueSizeMeasured.Height); - graphics.DrawString(kvp.Key, font, whiteBrush, new RectangleF(currentX, y, itemKeyWidth, valueSizeMeasured.Height), stringFormat); - - // Draw value (black on white) - graphics.FillRectangle(Brushes.White, currentX + itemKeyWidth, y, actualValueWidth, valueSizeMeasured.Height); - graphics.DrawString(kvp.Value, font, blackBrush, new RectangleF(currentX + itemKeyWidth, y, actualValueWidth, valueSizeMeasured.Height), stringFormat); - - // Update positions - currentX += itemKeyWidth + actualValueWidth + padding; - maxY = Math.Max(maxY, y + valueSizeMeasured.Height); - - // If we're close to the right edge, move to the next line - if (currentX + itemKeyWidth > x + totalWidth - padding) - { - currentX = x; - y = maxY + padding; - } - } - } - - y = maxY + padding; // Update final y position - } - - public static async Task SaveImageAndAnnotateText(byte[] imageBytes, IEnumerable historySteps, Dictionary imageInfo, string outputPath) - { - using var ms = new MemoryStream(imageBytes); - using var originalImage = Image.FromStream(ms); - - int annotationWidth = 1000; // Standard width for the right side annotation section - using var annotatedImage = new Bitmap(originalImage.Width + annotationWidth, originalImage.Height); - using var graphics = Graphics.FromImage(annotatedImage); - - // Draw the original image - graphics.DrawImage(originalImage, 0, 0); - - // Set up the annotation area - graphics.FillRectangle(Brushes.Black, originalImage.Width, 0, annotationWidth, originalImage.Height); - - float y = 5; // Start a bit below the top - float fontSize = 12; - - float leftMargin = originalImage.Width + 5; - float rightMargin = annotatedImage.Width - 5; - float totalWidth = rightMargin - leftMargin; - float keyWidth = totalWidth * KEY_WIDTH_PROPORTION; - float valueWidth = totalWidth * VALUE_WIDTH_PROPORTION; - - // Draw text into the right side black rectangle - foreach (var historyStep in historySteps) - { - DrawKeyValuePair(graphics, historyStep, fontSize, leftMargin, keyWidth, valueWidth, ref y); - y += 2; // Add small gap between text pairs - } - - DrawImageInfo(graphics, imageInfo, new Font("Arial", fontSize, FontStyle.Regular), leftMargin, totalWidth, ref y); - - annotatedImage.Save(outputPath, ImageFormat.Png); - } - - private static Font GetSupportedFont(string text, float fontSize, FontStyle style) - { - // List of fonts to try, in order of preference - string[] fontFamilies = { "Arial", "Segoe UI", "Microsoft Sans Serif", "Arial Unicode MS" }; - - foreach (string fontFamily in fontFamilies) - { - using (var font = new Font(fontFamily, fontSize, style)) - { - if (font.Name == fontFamily && CanDisplayText(font, text)) - { - return new Font(fontFamily, fontSize, style); - } - } - } - - // If no font can display all characters, return a default font - return new Font(FontFamily.GenericSansSerif, fontSize, style); - } - - private static bool CanDisplayText(Font font, string text) - { - using (var graphics = Graphics.FromHwnd(IntPtr.Zero)) - { - return text.All(c => font.FontFamily.GetEmHeight(font.Style) != 0); - } - } - - private static Font GetMonospacedFont(string text, float fontSize, FontStyle style) - { - // List of monospaced fonts to try, in order of preference - string[] monoFontFamilies = { "Consolas", "Courier New", "Lucida Console", "Monaco" }; - - foreach (string fontFamily in monoFontFamilies) - { - using (var font = new Font(fontFamily, fontSize, style)) - { - if (font.Name == fontFamily && CanDisplayText(font, text)) - { - return new Font(fontFamily, fontSize, style); - } - } - } - - // If no monospaced font can display all characters, return a default monospaced font - return new Font(FontFamily.GenericMonospace, fontSize, style); - } - } -} diff --git a/MultiImageClient/Implementation/TextUtils.cs b/MultiImageClient/Implementation/TextUtils.cs deleted file mode 100644 index 63adaff..0000000 --- a/MultiImageClient/Implementation/TextUtils.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing.Imaging; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using System.Reflection; -using System.Timers; -using System.Text; -using System.Windows.Forms; - -using IdeogramAPIClient; -using System.Text.RegularExpressions; -using System.Xml.Linq; -using MultiImageClient.Enums; - -namespace MultiImageClient -{ - public static class TextUtils - { - private static readonly object _lockObject = new object(); - private static readonly Dictionary _filenameCounts = new Dictionary(); - private static readonly float KEY_WIDTH_PROPORTION = 0.15f; - private static readonly float VALUE_WIDTH_PROPORTION = 1f - KEY_WIDTH_PROPORTION; - - public static string GenerateUniqueFilename(TaskProcessResult result, string baseFolder, string promptGeneratorName, SaveType saveType) - { - var components = new List - { - promptGeneratorName, - result.ImageGenerator.ToString(), - TruncatePrompt(result.PromptDetails.Prompt, 90), - - GetResolution(result.PromptDetails), - GetIfAPIServiceDoesRewrites(result.PromptDetails), - - DateTime.Now.ToString("yyyyMMddHHmmss"), - saveType.ToString(), - }; - - string combined = string.Join("_", components.Where(c => !string.IsNullOrEmpty(c))); - string sanitized = SanitizeFilename(combined); - - // Ensure the filename is unique - int count = 0; - string uniqueFilename; - do - { - uniqueFilename = count == 0 ? sanitized : $"{sanitized}_{count:D4}"; - count++; - } while (File.Exists(Path.Combine(baseFolder, $"{uniqueFilename}{result.ImageGenerator.GetFileExtension()}"))); - - return uniqueFilename; - } - - private static string TruncatePrompt(string prompt, int maxLength) - { - return prompt.Length > maxLength ? prompt.Substring(0, maxLength) : prompt; - } - - private static string GetIfAPIServiceDoesRewrites(PromptDetails details) - { - if (details.BFLDetails != null) - { - if (details.BFLDetails.PromptUpsampling) - { - return "BFL_upsampling"; - } - else - { - return ""; - } - } - if (details.IdeogramDetails != null) - { - if (details.IdeogramDetails.MagicPromptOption == IdeogramMagicPromptOption.ON) - { - return "MagicPrompt_YES"; - } - else if (details.IdeogramDetails.MagicPromptOption == IdeogramMagicPromptOption.AUTO) - { - return "MagicPrompt_Auto"; - } - else - { - return ""; - } - } - return ""; - } - - private static string GetResolution(PromptDetails details) - { - if (details.BFLDetails != null && details.BFLDetails.Width != default && details.BFLDetails.Height != default) - return $"{details.BFLDetails.Width}x{details.BFLDetails.Height}"; - if (details.Dalle3Details != null) - return details.Dalle3Details.Size.ToString(); - if (details.IdeogramDetails?.AspectRatio != null) - return IdeogramUtils.StringifyAspectRatio(details.IdeogramDetails.AspectRatio.Value); - return ""; - } - - private static string GetSafetyTolerance(PromptDetails details) - { - if (details.BFLDetails != null && details.BFLDetails.SafetyTolerance != default) - return $"safety{details.BFLDetails.SafetyTolerance}"; - return ""; - } - - private static string SanitizeFilename(string filename) - { - string sanitized = Regex.Replace(filename, @"[^a-zA-Z0-9_\-]", "_"); - while (sanitized.Contains("__")) - { - sanitized = sanitized.Replace("__", "_"); - } - return sanitized.Length > 200 ? sanitized.Substring(0, 200) : sanitized; - } - } -} diff --git a/MultiImageClient/Interfaces/AbstractPromptSource.cs b/MultiImageClient/Interfaces/AbstractPromptSource.cs new file mode 100644 index 0000000..eaa71b7 --- /dev/null +++ b/MultiImageClient/Interfaces/AbstractPromptSource.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Emit; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + /// this a way of loading a bunch of user configs for prompts. like, what do you want the text of the prompt to say? (and optionally, should it be big, landscape, whatever?) + /// things like actually mapping that to whatever options are provided by the image generator is another job, for future, IJobSpec or something like that) + public abstract class AbstractPromptSource + { + public abstract IEnumerable Prompts { get; } + + /// ----------- SETTINGS ----------------- + + public abstract int ImageCreationLimit { get; } + + /// how many times we send this prompt as of this level. + public abstract int CopiesPer { get; } + + /// i.e. how many times we send it to each image generator, after applying all prior manipulation steps + public virtual int FullyResolvedCopiesPer { get; } = 1; + public abstract string Prefix { get; } + public abstract string Suffix { get; } + + /// Variant leading texts, if any, which you want to include after the global prefix. + /// The overall structure we'll iterate through is: (copiesPer *) Prefix + Variant + Prompt + Suffix + public abstract bool RandomizeOrder { get; } + public abstract string Name { get; } + public Settings Settings { get; set; } + + + public AbstractPromptSource(Settings settings) + { + Settings = settings; + } + + /// Implementers should have their .Iter method called from Program.cs to iterate through your prompts. + // expand the things we've set up into all the prmopts wanted. + public IEnumerable Iter(IEnumerable thePromptDetails) + { + var returnedCount = 0; + foreach (var promptDetails in thePromptDetails) + { + var cleanPrompt = TextUtils.CleanPrompt(promptDetails.Prompt).Trim(); + if (string.IsNullOrWhiteSpace(cleanPrompt)) + { + continue; + } + + var usingPrompt = cleanPrompt; + if (!string.IsNullOrWhiteSpace(Prefix)) + { + usingPrompt = $"{Prefix} {cleanPrompt}".Trim(); + } + + + + if (usingPrompt != promptDetails.Prompt) + { + Console.WriteLine(usingPrompt); + Console.WriteLine(promptDetails.Prompt); + promptDetails.ReplacePrompt(usingPrompt, usingPrompt, TransformationType.Variants); + } + + Console.WriteLine(promptDetails.Show()); + Console.WriteLine("Do you accept this prompt? y for yes."); + var value = Console.ReadLine(); + if (value.Trim() == "y") + { + Console.WriteLine("accepted."); + yield return promptDetails; + } + else if (value.TrimEnd() == "n") + { + continue; + } + else + { + var userPrompt = TextUtils.CleanPrompt(value.Trim()).Trim(); + var newPd = new PromptDetails(); + newPd.ReplacePrompt(userPrompt, userPrompt, TransformationType.InitialPrompt); + yield return newPd; + continue; + } + + //for (var ii = 0; ii < CopiesPer; ii++) + //{ + // var copy = promptDetails.Clone(); + // returnedCount++; + // yield return copy; + // if (returnedCount >= ImageCreationLimit) yield break; + //} + + if (returnedCount >= ImageCreationLimit) yield break; + } + } + } +} \ No newline at end of file diff --git a/MultiImageClient/Interfaces/ILocalVisionModel.cs b/MultiImageClient/Interfaces/ILocalVisionModel.cs new file mode 100644 index 0000000..97d834d --- /dev/null +++ b/MultiImageClient/Interfaces/ILocalVisionModel.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public interface ILocalVisionModel + { + Task DescribeImageAsync(byte[] imageBytes, string prompt, int maxTokens = 512, float temperature = 0.8f); + string GetModelName(); + } +} + + diff --git a/MultiImageClient/Interfaces/ITransformationStep.cs b/MultiImageClient/Interfaces/ITransformationStep.cs new file mode 100644 index 0000000..3471354 --- /dev/null +++ b/MultiImageClient/Interfaces/ITransformationStep.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public interface ITransformationStep + { + Task DoTransformation(PromptDetails pd); + string Name { get; } + } +} diff --git a/MultiImageClient/MultiImageClient.csproj b/MultiImageClient/MultiImageClient.csproj index 38c6715..23101a0 100644 --- a/MultiImageClient/MultiImageClient.csproj +++ b/MultiImageClient/MultiImageClient.csproj @@ -1,48 +1,58 @@ - - - - Exe - net6.0 - 10.0 - {0AB858B6-6624-452A-A126-9851F2552B0A} - AnyCPU;ARM64 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - + + + + Exe + net9.0-windows + 13.0 + {0AB858B6-6624-452A-A126-9851F2552B0A} + AnyCPU;ARM64 + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + \ No newline at end of file diff --git a/MultiImageClient/Program.cs b/MultiImageClient/Program.cs index 4a70403..caad7e1 100644 --- a/MultiImageClient/Program.cs +++ b/MultiImageClient/Program.cs @@ -1,207 +1,159 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using System.Collections.Generic; -using IdeogramAPIClient; -using Newtonsoft.Json; -using System.IO; -using OpenAI.Images; -using System.Linq; -using System.ComponentModel.Design; -using static LLama.Native.NativeLibraryConfig; -using System.ComponentModel; -using System.Drawing.Drawing2D; -using System.Numerics; -using System.Xml.Linq; -using System.Xml; -using System.Security.Cryptography.X509Certificates; -using MultiImageClient.promptTransformation; -using MultiImageClient.Implementation; - -namespace MultiImageClient -{ - /// We only well-control the initial prompt text generation. The actual process of applying various steps, logging etc is all hardcoded in here which is not ideal. - public class Program - { - private static readonly HttpClient httpClient = new HttpClient(); - private static BFLService _BFLService; - private static IdeogramService _IdeogramService; - private static Dalle3Service _Dalle3Service; - - static async Task Main(string[] args) - { - var settingsFilePath = "settings.json"; - var settings = Settings.LoadFromFile(settingsFilePath); - - Console.WriteLine("Current settings:"); - Console.WriteLine($"Image Download Base:\t{settings.ImageDownloadBaseFolder}"); - Console.WriteLine($"Save JSON Log:\t\t{settings.SaveJsonLog}"); - Console.WriteLine($"Enable Logging:\t\t{settings.EnableLogging}"); - Console.WriteLine($"Annotation Side:\t{settings.AnnotationSide}"); - _BFLService = new BFLService(settings.BFLApiKey, 10); - _IdeogramService = new IdeogramService(settings.IdeogramApiKey, 10); - _Dalle3Service = new Dalle3Service(settings.OpenAIApiKey, 5); - var claudeService = new ClaudeService(settings.AnthropicApiKey, 10); - - // here is where you have a choice. Super specific stuff like managing a run with repeats, targets etc can be controlled - // with specific classes which inherit from AbstractPromptGenerator. e.g. DeckOfCards - var basePromptGenerator = new LoadFromFile(settings, "D:\\proj\\multiImageClient\\IdeogramHistoryExtractor\\myPrompts\\myPrivatePrompts.txt"); - //var basePromptGenerator = new WriteHere(settings); - var stats = new MultiClientRunStats(); - var processingTasks = new List(); - - var steps = new List(); - - //var llamaRewriteStep = new LLAMARewriteStep("Rewrite this, adding details: ","Output 100 words of clear, simple text, describing an image which you imagine in detail.",llamaService); - //steps.Add(llamaRewriteStep); - - var rstep = new RandomizerStep(); - steps.Add(rstep); - - //var claudeStep = new ClaudeRewriteStep("", "Expand the preceding INPUT data with unusual but fitting obscure, archiac, poetic, punning words which still focus on the ultimate goal, to elucidate new, created-by-you, well-chosen details on her style, beliefs, life, heart, mind, soul, appearance, and habits, to produce a hyper-ULTRA-condensed prose output which still encapsulates [mystery-universe-gaia-purity-edge-tao] in all wonder, while retaining a thrilling environment, glitchcore, unspoken musings of null-thought, arcana, jargon, mumbled transliterations of mega-negative dimensional thought space and poetry CONCRETE illustration style. DO IT. ALSO: you must emit MANY words and arrange them in a beautiful ASCII ART style of width 50 characters.", claudeService, 1.0m); - //steps.Add(claudeStep); - - var claudeStep = new ClaudeRewriteStep("", "Draw this making it full of many details, into a specific, news-paper photography style description of an image, most important and largest elements first as well as textures, colors, styles, then going into super details about the rest of it, which you should create to make the visual effect intense, interesting, and exceptional. Generate MANY words such as 500 or even 600, and then add a caption/title in the form of a beautiful ASCII ART style of width 50 characters. Our overall theme is purity, simplicity, natural forms, SATISFYING images that are fun to look at;", claudeService, 1.0m); - steps.Add(claudeStep); - - - - - //var stylizerStep = new StylizerStep(); - //steps.Add(stylizerStep); - - //var mmstep = new ManualModificationStep("A dramatically simple, emotional, lush and colorful razor-sharp vector art graphic illustration style glowing and pure muted or intense, evocative image on the following theme: ",""); - //steps.Add(mmstep); - - - - var generators = new List(); - - generators.Add(new BFLGenerator(_BFLService)); - //generators.Add(new IdeogramGenerator(_IdeogramService)); - //generators.Add(new Dalle3Generator(_Dalle3Service)); - - foreach (var promptDetails in basePromptGenerator.Run()) - { - Console.WriteLine(stats.PrintStats()); - Console.WriteLine($"\n----------------- Processing prompt: {promptDetails.Show()}"); - - foreach (var step in steps) - { - var res = await step.DoTransformation(promptDetails, stats); - if (!res) - { - Console.WriteLine($"\tStep{step.Name} failed so skipping it. {promptDetails.Show()}"); - continue; - } - - Console.WriteLine($"\tStep{step.Name} rewrote it to: {promptDetails.Show()}"); - } - - - for (var jj = 0; jj < basePromptGenerator.FullyResolvedCopiesPer; jj++) - { - foreach (var generator in generators) - { - var theCopy = promptDetails.Clone(); - processingTasks.Add(ProcessAndDownloadAsync( - generator.ProcessPromptAsync(theCopy, stats), - settings, - basePromptGenerator, - stats)); - } - await Task.Delay(250); - } - } - - - await Task.WhenAll(processingTasks); - Console.WriteLine("All tasks completed."); - stats.PrintStats(); - } - - private static async Task ProcessAndDownloadAsync(Task processingTask, Settings settings, AbstractPromptGenerator abstractPromptGenerator, MultiClientRunStats stats) - { - try - { - var result = await processingTask; - if (result.IsSuccess) - { - if (!string.IsNullOrEmpty(result.Url)) - { - Console.WriteLine($"\t\tDownloading image"); - byte[] imageBytes = await DownloadImageAsync(result.Url); - var savedImagePaths = new Dictionary(); - if (abstractPromptGenerator.SaveRaw) - { - savedImagePaths[SaveType.Raw] = await ImageSaving.SaveImageAsync(imageBytes, result, settings, stats, SaveType.Raw, abstractPromptGenerator.Name); - } - if (abstractPromptGenerator.SaveFullAnnotation) - { - savedImagePaths[SaveType.FullAnnotation] = await ImageSaving.SaveImageAsync(imageBytes, result, settings, stats, SaveType.FullAnnotation, abstractPromptGenerator.Name); - } - if (abstractPromptGenerator.SaveFinalPrompt) - { - savedImagePaths[SaveType.FinalPrompt] = await ImageSaving.SaveImageAsync(imageBytes, result, settings, stats, SaveType.FinalPrompt, abstractPromptGenerator.Name); - } - if (abstractPromptGenerator.SaveInitialIdea) - { - savedImagePaths[SaveType.InitialIdea] = await ImageSaving.SaveImageAsync(imageBytes, result, settings, stats, SaveType.InitialIdea, abstractPromptGenerator.Name); - } - if (settings.SaveJsonLog) - { - await SaveJsonLogAsync(result, savedImagePaths, settings); - } - } - else - { - Console.WriteLine($"No URL: {result.ErrorMessage}"); - } - } - else - { - Console.WriteLine(result); - } - } - catch (Exception ex) - { - Console.WriteLine($"\tAn error occurred while processing a task: {ex.Message}"); - } - } - private static async Task SaveJsonLogAsync(TaskProcessResult result, Dictionary savedImagePaths, Settings settings) - { - var jsonLog = new - { - Timestamp = DateTime.UtcNow, - result.PromptDetails, - GeneratedImageUrl = result.Url, - SavedImagePaths = savedImagePaths, - ServiceUsed = result.ImageGenerator, - result.ErrorMessage, - }; - string jsonString = JsonConvert.SerializeObject(jsonLog, Newtonsoft.Json.Formatting.Indented); - if (savedImagePaths.TryGetValue(SaveType.Raw, out string rawImagePath)) - { - string jsonFilePath = Path.ChangeExtension(rawImagePath, ".json"); - await File.WriteAllTextAsync(jsonFilePath, jsonString); - Console.WriteLine($"\tJSON log saved to: {jsonFilePath}"); - } - else - { - Console.WriteLine("\tUnable to save JSON log: Raw image path not found."); - } - } - public static async Task DownloadImageAsync(string imageUrl) - { - try - { - return await httpClient.GetByteArrayAsync(imageUrl); - } - catch (Exception ex) - { - Console.WriteLine($"Failed to download image from {imageUrl}: {ex.Message}"); - return Array.Empty(); - } - } - } -} +//using GenerativeAI.Types.RagEngine; + +using IdeogramAPIClient; + + + +//using OpenAI.Images; + +//using RecraftAPIClient; + +using System; +//using System.Collections.Generic; +//using System.Diagnostics.Metrics; +//using System.Drawing.Printing; +//using System.Linq; +//using System.Reflection.Metadata.Ecma335; +//using System.Runtime; +//using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + + public class Program + { + static async Task Main(string[] args) + { + var options = RunOptions.Parse(args); + + // Look for settings.json in the obvious places so `dotnet run` + // works from either the repo root OR the MultiImageClient folder: + // 1. current working directory (legacy: run from MultiImageClient\) + // 2. CWD\MultiImageClient\settings.json (run from repo root) + // 3. next to the exe (AppContext.BaseDirectory) + // First one that exists wins. If none do, fall back to the legacy + // path so the error message matches the old behavior. + var settingsFilePath = ResolveSettingsPath(); + var settings = Settings.LoadFromFile(settingsFilePath); + + if (options.BackfillDl) + { + DlMirror.Backfill(settings.ImageDownloadBaseFolder, settings.FlatImageMirrorPath); + return; + } + + if (options.GrokSync) + { + await GrokArchive.SyncAsync(settings); + return; + } + + if (options.GrokExportPath != null) + { + await GrokArchive.ExportAsync(settings, options.GrokExportPath); + return; + } + + var concurrency = 1; + var stats = new MultiClientRunStats(); + + // REPL mode bypasses the usual prompt-source + workflow menu + // entirely — prompts come from stdin one line at a time, fire + // off as async tasks, and results are saved silently (no viewer + // pops). See ReplWorkflow.cs for the full command set. + if (options.Repl) + { + var repl = new ReplWorkflow(settings, stats, options); + await repl.RunAsync(); + return; + } + + AbstractPromptSource promptSource = string.IsNullOrEmpty(options.OverridePrompt) + ? new ReadAllPromptsFromFile(settings, "") + : new InlinePromptSource(settings, options.OverridePrompt); + + if (options.GrokVideoTest) + { + // Exercises text-to-video, grok-image-to-video, and + // extend-video with one prompt; saves + ledgers every clip. + var videoModes = new GrokVideoModesWorkflow(); + await videoModes.RunAsync(promptSource, settings); + return; + } + + if (options.AllProviders) + { + // One prompt -> one flagship generator per provider -> one + // combined contact sheet. Keyless providers fail soft into + // error cells, so this is also the cross-provider auth check. + var allProviders = new AllProvidersShowcaseWorkflow(); + await allProviders.RunAsync(promptSource, settings, stats, options); + return; + } + + if (options.GrokShowcase) + { + // --limit defaults to int.MaxValue; clamp to 10 for the showcase + // so the grid stays readable and the cheap tier stays ~$0.20. + var showcaseLimit = options.Limit == int.MaxValue ? 10 : options.Limit; + var showcase = new GrokShowcaseWorkflow(); + await showcase.RunAsync(promptSource, settings, stats, pro: options.GrokPro, limit: showcaseLimit); + return; + } + + int workflow = options.Workflow; + if (workflow == 0) + { + while (workflow == 0) + { + Console.WriteLine($"What do you want to do: \n\n1. Batch Workflow (make a bunch images for each prompt you choose or write yourself)\r\n2. Image2desc2image take an image, then describe it, then batch that out into a bunch of images again.\r\nq. quit"); + var line = Console.ReadLine(); + if (line is null) + { + Console.WriteLine("stdin closed, exiting."); + return; + } + var val = line.Trim(); + if (val == "1") workflow = 1; + else if (val == "2") workflow = 2; + else if (val.Equals("q", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("quitting."); + return; + } + else Console.WriteLine("not recognized."); + } + } + + if (workflow == 1) + { + var bw = new BatchWorkflow(); + await bw.RunAsync(promptSource, settings, concurrency, stats, options); + } + else if (workflow == 2) + { + var rw = new RoundTripWorkflow(); + await rw.RunAsync(settings, concurrency, stats); + } + } + + // Searches the obvious places for `settings.json`. Returns "settings.json" + // (i.e. relative to CWD, the legacy path) if none of the candidates exist, + // which preserves the old error text for anyone used to it. + private static string ResolveSettingsPath() + { + var candidates = new[] + { + "settings.json", + System.IO.Path.Combine("MultiImageClient", "settings.json"), + System.IO.Path.Combine(System.AppContext.BaseDirectory, "settings.json"), + }; + foreach (var c in candidates) + { + if (System.IO.File.Exists(c)) return c; + } + return "settings.json"; + } + } +} diff --git a/MultiImageClient/Properties/launchSettings.json b/MultiImageClient/Properties/launchSettings.json new file mode 100644 index 0000000..3c3595f --- /dev/null +++ b/MultiImageClient/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "profiles": { + "MultiImageClient (REPL)": { + "commandName": "Project", + "commandLineArgs": "--repl", + "nativeDebugging": false + }, + "MultiImageClient (interactive menu)": { + "commandName": "Project" + }, + "MultiImageClient (REPL, 10 parallel)": { + "commandName": "Project", + "commandLineArgs": "--repl --repl-concurrency 10" + } + } +} diff --git a/MultiImageClient/RunOptions.cs b/MultiImageClient/RunOptions.cs new file mode 100644 index 0000000..477e566 --- /dev/null +++ b/MultiImageClient/RunOptions.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; + +namespace MultiImageClient +{ + /// Command-line options for non-interactive runs. Parsed in + /// Program.Main from args. Passing no args keeps the old fully + /// interactive behavior. + public class RunOptions + { + /// If true, skip the top-level workflow menu and every per-prompt + /// confirmation; auto-accept everything. + public bool Auto { get; set; } + + /// Max number of prompts to process. int.MaxValue = no cap. + public int Limit { get; set; } = int.MaxValue; + + /// If non-empty, overrides the prompt source: use this single + /// prompt instead of reading from PromptFiles. + public string OverridePrompt { get; set; } = ""; + + /// 1 = Batch, 2 = RoundTrip, 0 = ask interactively. + public int Workflow { get; set; } + + /// If true, mirror every image under Settings.ImageDownloadBaseFolder + /// into C:\dl and exit. Does not run any workflow. + public bool BackfillDl { get; set; } + + /// If true, use the smallest/cheapest/fastest generator set + /// (gpt-image-2 low quality, 1024x1024 square, moderation=low). + /// Intended for iteration/smoke-testing, not production runs. + public bool Fast { get; set; } + + /// If true, use a single gpt-image-2 call per prompt configured for + /// maximum interactive feedback: 1024x1024 low quality, moderation=low, + /// n=1, and every streamed partial PNG is saved to disk and opened + /// with the system default viewer the moment it arrives. Intended for + /// human-in-the-loop development where seeing the generation refine + /// in real time is the point. + public bool QuickTest { get; set; } + + /// If true, start an interactive prompt-by-prompt REPL. Each line is + /// either a `:command` or a prompt fired off asynchronously against + /// the current active generator set. Up to ReplConcurrency prompts + /// run in parallel; results are saved to disk as they arrive and + /// NO images are popped open in the default viewer. See ReplWorkflow + /// for the command list. + public bool Repl { get; set; } + + /// Default gpt-image-2 size for REPL sessions. Can be changed at + /// runtime via `:size WxH`. 2048x2048 matches the "large, high + /// quality" iteration profile the REPL is designed for. + public string ReplSize { get; set; } = "2048x2048"; + + /// Default gpt-image-2 quality for REPL sessions. low | medium | high. + /// Can be changed at runtime via `:quality `. + public string ReplQuality { get; set; } = "high"; + + /// Default gpt-image-2 moderation for REPL sessions. auto | low. + /// Can be changed at runtime via `:moderation `. + public string ReplModeration { get; set; } = "low"; + + /// How many prompts can be in flight at once in REPL mode. Higher + /// values let you fire prompts faster than the backend completes + /// them; the REPL will queue beyond this limit. + public int ReplConcurrency { get; set; } = 5; + + /// Default `n` (images per call) for the gpt-image-2 slot in REPL + /// sessions. Useful for variant exploration (e.g. logo design). Can + /// be changed at runtime via `:n N` or per-prompt via `[n=N] ...`. + public int ReplImageCount { get; set; } = 1; + + /// If true, bypass every other workflow and run GrokShowcaseWorkflow: + /// pull the first --limit prompts from the active prompt source, fire + /// them at xAI Grok Imagine in parallel, save each, then compose one + /// combined grid image and pop it open. + public bool GrokShowcase { get; set; } + + /// Pair with --grok-showcase to route through grok-imagine-image-pro + /// at 2k resolution instead of the standard grok-imagine-image at 1k. + public bool GrokPro { get; set; } + + /// If true, run AllProvidersShowcaseWorkflow: take ONE prompt and + /// fire it at one flagship generator per provider (gpt-image-2, + /// Ideogram 4.0, flux-2-pro-preview, Recraft V4.1, Grok Imagine, + /// Nano Banana Pro), then compose every result into a single + /// contact-sheet grid and pop it open. Failed/keyless providers + /// show as error cells, so this doubles as a key health check. + public bool AllProviders { get; set; } + + /// Pair with --all-providers to ALSO dispatch a Grok Imagine video + /// (grok-imagine-video, 6s 480p) for the same prompt. The mp4 is + /// saved under the day folder's Video\ subfolder; videos are NOT + /// composited into the PNG contact sheet (stills only). + public bool WithVideo { get; set; } + + /// If non-null, run GrokArchive.ExportAsync and exit: sync the full + /// Grok history, then copy every known image/video plus prompts.txt + /// and the ledger into this folder (outside the repo). Defaults to + /// C:\GrokArchive when --grok-export is passed without a path. + public string? GrokExportPath { get; set; } + + /// If true, run GrokVideoModesWorkflow and exit: exercise all three + /// Grok video request modes with one prompt — text-to-video, + /// grok-image-to-video, and extend-video — saving each clip and + /// recording everything in grok_ledger.jsonl. + public bool GrokVideoTest { get; set; } + + /// If true, run GrokArchive.SyncAsync and exit: back-read the entire + /// reachable Grok history (xAI Files API inventory + re-pollable + /// video request_ids + local JSON logs) into grok_ledger.jsonl and + /// download every asset we don't already have locally. Idempotent; + /// run it whenever to keep local copies synced. + public bool GrokSync { get; set; } + + public static RunOptions Parse(string[] args) + { + var o = new RunOptions(); + for (int i = 0; i < args.Length; i++) + { + var a = args[i]; + switch (a) + { + case "--auto": + o.Auto = true; + // --auto means "just run the default workflow, don't + // prompt me for anything". Default to Batch (1) unless + // --workflow was already passed. + if (o.Workflow == 0) o.Workflow = 1; + break; + case "--limit": + o.Limit = int.Parse(args[++i]); + break; + case "--prompt": + o.OverridePrompt = args[++i]; + break; + case "--workflow": + o.Workflow = int.Parse(args[++i]); + break; + case "--backfill-dl": + o.BackfillDl = true; + break; + case "--fast": + o.Fast = true; + break; + case "--quick-test": + o.QuickTest = true; + // Skip the workflow menu (there's only one thing + // quick-test does) but deliberately DO NOT force + // --auto: the user still wants the per-prompt + // y/n/custom loop for iterative work. Pair with + // --auto explicitly for fully unattended runs. + if (o.Workflow == 0) o.Workflow = 1; + break; + case "--repl": + o.Repl = true; + break; + case "--repl-size": + o.ReplSize = args[++i]; + break; + case "--repl-quality": + o.ReplQuality = args[++i]; + break; + case "--repl-moderation": + o.ReplModeration = args[++i]; + break; + case "--repl-concurrency": + o.ReplConcurrency = int.Parse(args[++i]); + break; + case "--repl-n": + o.ReplImageCount = int.Parse(args[++i]); + if (o.ReplImageCount < 1) + { + Console.Error.WriteLine($"--repl-n must be >= 1 (got {o.ReplImageCount})"); + Environment.Exit(2); + } + break; + case "--grok-showcase": + o.GrokShowcase = true; + break; + case "--grok-pro": + o.GrokPro = true; + break; + case "--all-providers": + o.AllProviders = true; + break; + case "--with-video": + o.WithVideo = true; + break; + case "--grok-sync": + o.GrokSync = true; + break; + case "--grok-export": + // Optional path argument; default to C:\GrokArchive. + o.GrokExportPath = (i + 1 < args.Length && !args[i + 1].StartsWith("--")) + ? args[++i] + : @"C:\GrokArchive"; + break; + case "--grok-video-test": + o.GrokVideoTest = true; + break; + case "--help": + case "-h": + PrintUsage(); + Environment.Exit(0); + break; + default: + Console.Error.WriteLine($"Unknown argument: {a}"); + PrintUsage(); + Environment.Exit(2); + break; + } + } + return o; + } + + private static void PrintUsage() + { + Console.WriteLine("Usage: MultiImageClient [--auto] [--workflow 1|2] [--limit N] [--prompt \"...\"]"); + Console.WriteLine(" --auto Non-interactive: skip menu, auto-accept every prompt."); + Console.WriteLine(" --workflow 1|2 1 = batch, 2 = round-trip. Default: ask."); + Console.WriteLine(" --limit N Stop after N prompts."); + Console.WriteLine(" --prompt \"text\" Use this prompt instead of reading from PromptFiles."); + Console.WriteLine(" --backfill-dl One-shot: mirror all images under ImageDownloadBaseFolder to C:\\dl and exit."); + Console.WriteLine(" --fast Use cheapest/fastest generator set (gpt-image-2 low 1024x1024). Good for smoke tests."); + Console.WriteLine(" --quick-test Like --fast plus: save every streamed partial PNG and open each one in the default viewer as it arrives. Still asks y/n/custom per prompt unless combined with --auto."); + Console.WriteLine(" --repl Interactive prompt-by-prompt REPL. Prompts fire asynchronously (up to --repl-concurrency at a time); NO viewer pops. Commands: :help :size :quality :gens :status :wait :edit :retry :quit."); + Console.WriteLine(" --repl-size WxH REPL session default size for gpt-image-2 (default 2048x2048). Change at runtime with :size WxH."); + Console.WriteLine(" --repl-quality L REPL session default quality: low|medium|high (default high). Change at runtime with :quality ."); + Console.WriteLine(" --repl-moderation M REPL session default moderation: auto|low (default low). Change at runtime with :moderation ."); + Console.WriteLine(" --repl-concurrency N Max prompts in flight simultaneously in REPL mode (default 5). Change at runtime with :concurrency N."); + Console.WriteLine(" --repl-n N REPL session default n (images per gpt-image-2 call, default 1). Change at runtime with :n N, or per-prompt via [n=N] in the override prefix."); + Console.WriteLine(" --grok-showcase One-shot: take the first --limit prompts from the active prompt source (--prompt or PromptFiles), fire them at xAI Grok Imagine in parallel, and open a single combined grid image. Default --limit for this mode is 10."); + Console.WriteLine(" --grok-pro Pair with --grok-showcase to route through grok-imagine-image-pro at 2k resolution ($0.07/img, 30 rpm) instead of grok-imagine-image at 1k ($0.02/img, 300 rpm)."); + Console.WriteLine(" --all-providers One-shot: fire ONE prompt (--prompt or first PromptFiles line) at one flagship generator per provider (gpt-image-2, Ideogram 4.0, flux-2-pro-preview, Recraft V4.1, Grok Imagine, Nano Banana Pro) and open a single contact-sheet grid. Keyless providers show as error cells."); + Console.WriteLine(" --with-video Pair with --all-providers to also dispatch a Grok Imagine VIDEO (6s, 480p) for the same prompt; the mp4 lands in the day folder's Video\\ subfolder. Videos are not composited into the PNG sheet."); + Console.WriteLine(" --grok-video-test One-shot: exercise all three Grok video modes with one prompt (--prompt or first PromptFiles line) — text-to-video, grok-image-to-video, and extend-video (3s, 480p each). Clips are saved, stored durably at xAI, and ledgered."); + Console.WriteLine(" --grok-export [path] One-shot: full Grok history export OUTSIDE the repo. Runs --grok-sync first, then copies every known Grok image/video plus prompts.txt and grok_ledger.jsonl into [path] (default C:\\GrokArchive). Rerunnable; already-present files are skipped."); + Console.WriteLine(" --grok-sync One-shot: back-read/back-download the entire reachable Grok history and exit. Sweeps the xAI Files API inventory, re-polls any ledger video request_ids whose local file is missing, backfills prompts from old JSON logs, and writes everything to grok_ledger.jsonl + saves\\GrokArchive\\. Idempotent — run it whenever to stay synced."); + } + } +} diff --git a/MultiImageClient/Services/BFLService.cs b/MultiImageClient/Services/BFLService.cs deleted file mode 100644 index 611904d..0000000 --- a/MultiImageClient/Services/BFLService.cs +++ /dev/null @@ -1,91 +0,0 @@ -using BFLAPIClient; - -using IdeogramAPIClient; -using MultiImageClient.Enums; -using MultiImageClient.Implementation; -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace MultiImageClient -{ - public class BFLService : IImageGenerationService - { - private SemaphoreSlim _bflSemaphore; - private BFLClient _bflClient; - - public BFLService(string apiKey, int maxConcurrency) - { - _bflClient = new BFLClient(apiKey); - _bflSemaphore = new SemaphoreSlim(maxConcurrency); - } - - public async Task ProcessPromptAsync(PromptDetails promptDetails, MultiClientRunStats stats) - { - await _bflSemaphore.WaitAsync(); - try - { - var bflDetails = promptDetails.BFLDetails; - var request = new FluxPro11Request - { - Prompt = promptDetails.Prompt, - Width = bflDetails.Width, - Height = bflDetails.Height, - PromptUpsampling = bflDetails.PromptUpsampling, - SafetyTolerance = bflDetails.SafetyTolerance, - Seed = bflDetails.Seed - }; - - stats.BFLImageGenerationRequestCount++; - - var generationResult = await _bflClient.GenerateFluxPro11Async(request); - Console.WriteLine($"\tFrom BFL: '{generationResult.Status}'"); - - // this is where we handle generator-specific error types. - if (generationResult.Status != "Ready") - { - if (generationResult.Status == "Content Moderated") - { - stats.BFLImageGenerationErrorCount++; - return new TaskProcessResult { IsSuccess = false, ErrorMessage = generationResult.Status, PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.BFL, GenericImageErrorType = GenericImageGenerationErrorType.ContentModerated}; - } - else if (generationResult.Status == "Request Moderated") - { - stats.BFLImageGenerationErrorCount++; - return new TaskProcessResult { IsSuccess = false, ErrorMessage = generationResult.Status, PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.BFL, GenericImageErrorType = GenericImageGenerationErrorType.RequestModerated }; - } - else - { - // also you have to handle the out of money case. - stats.BFLImageGenerationErrorCount++; - return new TaskProcessResult { IsSuccess = false, ErrorMessage = generationResult.Status, PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.BFL, GenericImageErrorType = GenericImageGenerationErrorType.Unknown }; - } - - } - else - { - Console.WriteLine($"BFL image generated: {generationResult.Result.Sample}"); - stats.BFLImageGenerationRequestCount++; - var returnedPrompt = generationResult.Result.Prompt.Trim(); - if (returnedPrompt.Trim() != promptDetails.Prompt.Trim()) - { - //BFL replaced the prompt. Never actually happens. - promptDetails.ReplacePrompt(returnedPrompt.Trim(), returnedPrompt.Trim(), TransformationType.BFLRewrite); - } - return new TaskProcessResult { IsSuccess = true, Url = generationResult.Result.Sample, PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.BFL }; - } - - } - catch (Exception ex) - { - Console.WriteLine($"BFL error: {ex.Message}"); - return new TaskProcessResult { IsSuccess = false, ErrorMessage = ex.Message, PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.BFL }; - } - finally - { - _bflSemaphore.Release(); - } - } - } -} \ No newline at end of file diff --git a/MultiImageClient/Services/Dalle3Service.cs b/MultiImageClient/Services/Dalle3Service.cs deleted file mode 100644 index 00dd7b7..0000000 --- a/MultiImageClient/Services/Dalle3Service.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Anthropic.SDK; -using Anthropic.SDK.Constants; -using Anthropic.SDK.Messaging; - -using BFLAPIClient; - -using IdeogramAPIClient; -using MultiImageClient.Implementation; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - -using OpenAI; -using OpenAI.Images; -using OpenAI.Models; - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Http; -using System.Reflection; - -using System.Threading; -using System.Threading.Tasks; - -namespace MultiImageClient -{ - - public class Dalle3Service : IImageGenerationService - { - private SemaphoreSlim _dalle3Semaphore; - private ImageClient _openAIImageClient; - - public Dalle3Service(string apiKey, int maxConcurrency) - { - var openAIClient = new OpenAIClient(apiKey); - _openAIImageClient = openAIClient.GetImageClient("dall-e-3"); - _dalle3Semaphore = new SemaphoreSlim(maxConcurrency); - } - - public async Task ProcessPromptAsync(PromptDetails promptDetails, MultiClientRunStats stats) - { - await _dalle3Semaphore.WaitAsync(); - try - { - stats.Dalle3RequestCount++; - var genOptions = new ImageGenerationOptions(); - genOptions.Size = promptDetails.Dalle3Details.Size; - genOptions.Quality = promptDetails.Dalle3Details.Quality; - genOptions.ResponseFormat = promptDetails.Dalle3Details.Format; - var res = _openAIImageClient.GenerateImageAsync(promptDetails.Prompt, genOptions); - var uri = res.Result.Value.ImageUri; - var revisedPrompt = res.Result.Value.RevisedPrompt; - if (revisedPrompt != promptDetails.Prompt) - { - //BFL replaced the prompt. - promptDetails.ReplacePrompt(revisedPrompt, revisedPrompt, TransformationType.Dalle3Rewrite); - } - return new TaskProcessResult { IsSuccess = true, Url = uri.ToString(), ErrorMessage = "", PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.Dalle3 }; - } - catch (Exception ex) - { - return new TaskProcessResult { IsSuccess = false, ErrorMessage = ex.Message, PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.Dalle3 }; - } - finally - { - _dalle3Semaphore.Release(); - } - - } - - } -} \ No newline at end of file diff --git a/MultiImageClient/Services/IdeogramService.cs b/MultiImageClient/Services/IdeogramService.cs deleted file mode 100644 index 8fd3962..0000000 --- a/MultiImageClient/Services/IdeogramService.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Anthropic.SDK; -using Anthropic.SDK.Constants; -using Anthropic.SDK.Messaging; - -using BFLAPIClient; - -using IdeogramAPIClient; -using MultiImageClient.Enums; -using MultiImageClient.Implementation; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace MultiImageClient -{ - public class IdeogramService : IImageGenerationService - { - private SemaphoreSlim _ideogramSemaphore; - private IdeogramClient _ideogramClient; - - public IdeogramService(string apiKey, int maxConcurrency) - { - _ideogramClient = new IdeogramClient(apiKey); - _ideogramSemaphore = new SemaphoreSlim(maxConcurrency); - } - - public async Task ProcessPromptAsync(PromptDetails promptDetails, MultiClientRunStats stats) - { - await _ideogramSemaphore.WaitAsync(); - try - { - var ideogramDetails = promptDetails.IdeogramDetails; - var request = new IdeogramGenerateRequest(promptDetails.Prompt, ideogramDetails); - - stats.IdeogramRequestCount++; - GenerateResponse response = await _ideogramClient.GenerateImageAsync(request); - if (response?.Data?.Count == 1) - { - foreach (var imageObject in response.Data) - { - //there is only actually one ever. - var returnedPrompt = imageObject.Prompt; - if (returnedPrompt != promptDetails.Prompt) - { - //Ideogram replaced the prompt. - promptDetails.ReplacePrompt(returnedPrompt, returnedPrompt, TransformationType.IdeogramRewrite); - } - return new TaskProcessResult { IsSuccess = true, Url = imageObject.Url, PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.Ideogram }; - } - throw new Exception("No images returned"); - } - else if (response?.Data?.Count >= 1) - { - throw new Exception("Multiple images returned? I can't handle this! Who knew!"); - } - else - { - return new TaskProcessResult { IsSuccess = false, ErrorMessage = "No images generated", PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.Ideogram, GenericImageErrorType = GenericImageGenerationErrorType.NoImagesGenerated }; - } - } - - catch (Exception ex) - { - return new TaskProcessResult { IsSuccess = false, ErrorMessage = ex.Message, PromptDetails = promptDetails, ImageGenerator = ImageGeneratorApiType.Ideogram, GenericImageErrorType = GenericImageGenerationErrorType.Unknown }; - } - finally - { - _ideogramSemaphore.Release(); - } - } - } -} \ No newline at end of file diff --git a/MultiImageClient/Services/ClaudeService.cs b/MultiImageClient/TextLLMs/ClaudeService.cs similarity index 75% rename from MultiImageClient/Services/ClaudeService.cs rename to MultiImageClient/TextLLMs/ClaudeService.cs index 8894fff..c55d92f 100644 --- a/MultiImageClient/Services/ClaudeService.cs +++ b/MultiImageClient/TextLLMs/ClaudeService.cs @@ -1,14 +1,15 @@ -using System; +using Anthropic.SDK; +using Anthropic.SDK.Constants; +using Anthropic.SDK.Messaging; + +using System; using System.Collections.Generic; using System.Linq; +using System.Reflection.Emit; using System.Reflection.Metadata.Ecma335; using System.Threading; using System.Threading.Tasks; -using Anthropic.SDK; -using Anthropic.SDK.Constants; -using Anthropic.SDK.Messaging; -using MultiImageClient.Enums; namespace MultiImageClient { @@ -16,12 +17,14 @@ public class ClaudeService { private readonly AnthropicClient _anthropicClient; private readonly SemaphoreSlim _claudeSemaphore; + private MultiClientRunStats stats; - public ClaudeService(string apiKey, int maxConcurrency) + public ClaudeService(string apiKey, int maxConcurrency, MultiClientRunStats stats) { var anthropicApikeyAuth = new APIAuthentication(apiKey); _anthropicClient = new AnthropicClient(anthropicApikeyAuth); _claudeSemaphore = new SemaphoreSlim(maxConcurrency); + this.stats = stats; } ///Claude gets mad sometimes. This is for detecting this and optionally derailing since you probably don't want to continue with this bad rewrite output. @@ -54,14 +57,14 @@ internal static bool DidClaudeRefuse(string claudeResponse) } - public async Task RewritePromptAsync(PromptDetails promptDetails, MultiClientRunStats stats, decimal tempterature) + public async Task RewritePromptAsync(PromptDetails promptDetails, decimal temp) { if (ClaudeWillHateThis(promptDetails.Prompt)) { promptDetails.AddStep("Claude wouldn't have touched this prompt", TransformationType.ClaudeWouldRefuseRewrite); - Console.WriteLine($"\t\tClaude would have refused to rewrite: {promptDetails.Show()}"); + Logger.Log($"\t\tClaude would have refused to rewrite: {promptDetails.Show()}"); stats.ClaudeWouldRefuseCount++; - return new TaskProcessResult { IsSuccess = false, ErrorMessage = "Claude wouldn't have touched this prompt", PromptDetails = promptDetails, TextGenerator = TextGeneratorApiType.Claude, GenericImageErrorType = GenericImageGenerationErrorType.RequestModerated}; + return new TaskProcessResult { ImageGeneratorDescription="Claude?", IsSuccess = false, ErrorMessage = "Claude wouldn't have touched this prompt", PromptDetails = promptDetails, TextGenerator = TextGeneratorApiType.Claude, GenericImageErrorType = GenericImageGenerationErrorType.RequestModerated}; } await _claudeSemaphore.WaitAsync(); try @@ -77,25 +80,30 @@ public async Task RewritePromptAsync(PromptDetails promptDeta MaxTokens = 2048, Model = AnthropicModels.Claude3Haiku, Stream = false, - Temperature = tempterature, + Temperature = temp, }; MessageResponse firstResult = await _anthropicClient.Messages.GetClaudeMessageAsync(parameters); var claudeResponse = firstResult.Message.ToString(); - var isClaudeUnhappy = ClaudeService.DidClaudeRefuse(claudeResponse); + var isClaudeUnhappy = DidClaudeRefuse(claudeResponse); if (isClaudeUnhappy) { stats.ClaudeRefusedCount++; - Console.WriteLine($"\t\tClaude was unhappy about\n\t\t\t{promptDetails.Show()}\n\t\t\t{claudeResponse}"); - return new TaskProcessResult { IsSuccess = false, ErrorMessage = $"Claude was unhappy about the prompt and refused to rewrite it. {claudeResponse}", PromptDetails = promptDetails, TextGenerator = TextGeneratorApiType.Claude, GenericTextErrorType = GenericTextGenerationErrorType.RequestModerated }; + Logger.Log($"\t\tClaude was unhappy about\n\t\t\t{promptDetails.Show()}\n\t\t\t{claudeResponse}"); + return new TaskProcessResult { IsSuccess = false, ErrorMessage = $"Claude was unhappy about the prompt and refused to rewrite it. {claudeResponse}", PromptDetails = promptDetails, TextGenerator = TextGeneratorApiType.Claude, GenericTextErrorType = GenericTextGenerationErrorType.RequestModerated, + ImageGeneratorDescription = "ClaudeGeneratorDescription", + }; } else { - Console.WriteLine($"\t\tClaude rewrote it to: {claudeResponse}"); + Logger.Log($"\t___Step:Claude____ => rewrote to: {claudeResponse}"); promptDetails.ReplacePrompt(claudeResponse, claudeResponse, TransformationType.ClaudeRewrite); stats.ClaudeRewroteCount++; - return new TaskProcessResult { IsSuccess = true, ErrorMessage = "", PromptDetails = promptDetails, TextGenerator = TextGeneratorApiType.Claude }; + + return new TaskProcessResult { IsSuccess = true, ErrorMessage = "", PromptDetails = promptDetails, TextGenerator = TextGeneratorApiType.Claude, + ImageGeneratorDescription = "ClaudeGeneratorDescription", + }; } } finally diff --git a/MultiImageClient/Utils/ConsolePrefill.cs b/MultiImageClient/Utils/ConsolePrefill.cs new file mode 100644 index 0000000..2df8a20 --- /dev/null +++ b/MultiImageClient/Utils/ConsolePrefill.cs @@ -0,0 +1,86 @@ +using System; +using System.Runtime.InteropServices; + +namespace MultiImageClient +{ + /// Windows-only helper that stuffs text into the console input buffer so + /// a subsequent call appears pre-populated + /// and editable, exactly as if the user had just typed the characters + /// themselves. Used by the REPL's `:random` command to offer a suggested + /// prompt that the user can tweak in place before hitting Enter. + /// + /// The technique is WriteConsoleInputW with a synthesized KEY_DOWN event + /// per character (UnicodeChar set, virtual key codes zeroed since cooked + /// line input only reads the character). If stdin isn't a console (e.g. + /// redirected from a file) we silently no-op; callers should still print + /// the suggested text in that case so piped usage stays sensible. + internal static class ConsolePrefill + { + private const int STD_INPUT_HANDLE = -10; + + [StructLayout(LayoutKind.Sequential)] + private struct KEY_EVENT_RECORD + { + public int bKeyDown; // BOOL + public ushort wRepeatCount; + public ushort wVirtualKeyCode; + public ushort wVirtualScanCode; + public char UnicodeChar; + public uint dwControlKeyState; + } + + [StructLayout(LayoutKind.Explicit)] + private struct INPUT_RECORD + { + [FieldOffset(0)] public ushort EventType; + // KEY_EVENT_RECORD starts 4 bytes in to match the native union + // padding; the rest of the union (mouse/window/etc.) is unused. + [FieldOffset(4)] public KEY_EVENT_RECORD KeyEvent; + } + + private const ushort KEY_EVENT = 0x0001; + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GetStdHandle(int nStdHandle); + + [DllImport("kernel32.dll", SetLastError = true, EntryPoint = "WriteConsoleInputW")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool WriteConsoleInputW( + IntPtr hConsoleInput, + [MarshalAs(UnmanagedType.LPArray), In] INPUT_RECORD[] lpBuffer, + uint nLength, + out uint lpNumberOfEventsWritten); + + /// Push into stdin as synthesized key events. + /// Returns true on success, false if stdin is redirected or the + /// Win32 call failed — callers should then fall back to prompting. + public static bool TryPrefill(string text) + { + if (string.IsNullOrEmpty(text)) return true; + if (Console.IsInputRedirected) return false; + + var handle = GetStdHandle(STD_INPUT_HANDLE); + if (handle == IntPtr.Zero || handle == new IntPtr(-1)) return false; + + var records = new INPUT_RECORD[text.Length]; + for (int i = 0; i < text.Length; i++) + { + records[i] = new INPUT_RECORD + { + EventType = KEY_EVENT, + KeyEvent = new KEY_EVENT_RECORD + { + bKeyDown = 1, + wRepeatCount = 1, + wVirtualKeyCode = 0, + wVirtualScanCode = 0, + UnicodeChar = text[i], + dwControlKeyState = 0, + } + }; + } + + return WriteConsoleInputW(handle, records, (uint)records.Length, out _); + } + } +} diff --git a/MultiImageClient/Utils/FileNameGenerator.cs b/MultiImageClient/Utils/FileNameGenerator.cs new file mode 100644 index 0000000..5391c91 --- /dev/null +++ b/MultiImageClient/Utils/FileNameGenerator.cs @@ -0,0 +1,72 @@ +using IdeogramAPIClient; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public static class FilenameGenerator + { + public static string TruncatePrompt(string prompt, int maxLength) + { + return prompt.Length > maxLength ? prompt.Substring(0, maxLength) : prompt; + } + + public static string SanitizeFilename(string filename) + { + string sanitized = Regex.Replace(filename, @"[^a-zA-Z0-9_\-]", "_"); + while (sanitized.Contains("__")) + { + sanitized = sanitized.Replace("__", "_"); + } + return sanitized.Length > 200 ? sanitized.Substring(0, 200) : sanitized; + } + + + public static string GenerateUniqueFilename(string generatorFilenamePart, int n, string contentType, string baseFolder, SaveType saveType) + { + var components = new List() { }; + + components.Add(DateTime.Now.ToString("yyyyMMddHHmmss")); + components.Add(generatorFilenamePart); + components.Add($"img{n.ToString()}"); + + if (!string.IsNullOrEmpty(contentType)) + { + var ss = contentType.ToString(); + if (ss.IndexOf('/') != -1) + { + var parts = ss.Split('/'); + ss = parts.Last(); + + } + components.Add(ss); + } + + components.Add(saveType.ToString()); + + string combined = string.Join("_", components.Where(c => !string.IsNullOrEmpty(c))); + string sanitized = SanitizeFilename(combined); + + // Ensure the filename is unique + int count = 0; + string uniqueFilename; + var ext = ".png"; + + do + { + uniqueFilename = count == 0 ? $"{sanitized}{ext}" : $"{sanitized}_{count:D4}{ext}"; + count++; + } while (File.Exists(Path.Combine(baseFolder, $"{uniqueFilename}"))); + + return uniqueFilename; + } + + + } +} diff --git a/MultiImageClient/Utils/FontUtils.cs b/MultiImageClient/Utils/FontUtils.cs new file mode 100644 index 0000000..8a8be09 --- /dev/null +++ b/MultiImageClient/Utils/FontUtils.cs @@ -0,0 +1,56 @@ + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using System.Linq; + +namespace MultiImageClient +{ + public static class FontUtils + { + private static FontFamily? _cachedSystemFont; + + // Gets the preferred system font family with fallback logic. + // Order: Segoe UI → Arial → First available system font + public static FontFamily GetSystemFont() + { + if (_cachedSystemFont != null) + return _cachedSystemFont.Value!; + + if (SystemFonts.TryGet("Segoe UI", out var segoeFontFamily)) + { + _cachedSystemFont = segoeFontFamily; + return segoeFontFamily; + } + + if (SystemFonts.TryGet("Arial", out var arialFontFamily)) + { + _cachedSystemFont = arialFontFamily; + return arialFontFamily; + } + + var fallbackFont = SystemFonts.Families.First(); + _cachedSystemFont = fallbackFont; + return fallbackFont; + } + + public static Font CreateFont(float size, FontStyle style = FontStyle.Regular) + { + return GetSystemFont().CreateFont(size, style); + } + + public static RichTextOptions CreateTextOptions(Font font, + HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment verticalAlignment = VerticalAlignment.Top, + float lineSpacing = 1.15f) + { + return new RichTextOptions(font) + { + HorizontalAlignment = horizontalAlignment, + VerticalAlignment = verticalAlignment, + LineSpacing = lineSpacing, + Dpi = UIConstants.TextDpi, + FallbackFontFamilies = new[] { GetSystemFont() } + }; + } + } +} \ No newline at end of file diff --git a/MultiImageClient/Utils/ImageUtils.cs b/MultiImageClient/Utils/ImageUtils.cs new file mode 100644 index 0000000..b2b7956 --- /dev/null +++ b/MultiImageClient/Utils/ImageUtils.cs @@ -0,0 +1,146 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.Fonts; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MultiImageClient +{ + public static class ImageUtils + { + public static readonly GraphicsOptions StandardGraphicsOptions = new GraphicsOptions + { + Antialias = true, + AntialiasSubpixelDepth = 32 + }; + + public static void ApplyStandardGraphicsOptions(this IImageProcessingContext ctx) + { + ctx.SetGraphicsOptions(StandardGraphicsOptions); + } + + public static int MeasureTextHeight(string text, Font font, float lineSpacing = 1.15f, float wrappingLength = 0) + { + if (string.IsNullOrEmpty(text)) + return 0; + + var textOptions = FontUtils.CreateTextOptions(font, lineSpacing: lineSpacing); + if (wrappingLength > 0) + textOptions.WrappingLength = wrappingLength; + + var bounds = TextMeasurer.MeasureBounds(text, textOptions); + return (int)Math.Ceiling(bounds.Height); + } + + public static void DrawTextStandard(this IImageProcessingContext ctx, + RichTextOptions options, + string text, + Color color) + { + if (text == null) + { + text = ""; + } + ctx.DrawText(options, text, color); + } + + + public static Image CreateStandardImage(int width, int height, Color? backgroundColor = null) + { + var image = new Image(width, height); + + if (backgroundColor.HasValue) + { + image.Mutate(ctx => + { + ctx.ApplyStandardGraphicsOptions(); + ctx.Fill(backgroundColor.Value); + }); + } + + return image; + } + + public static int MeasureMaxTextHeight(IEnumerable texts, Font font, float lineSpacing = UIConstants.LineSpacing) + { + int maxHeight = 0; + + foreach (var text in texts.Where(t => !string.IsNullOrEmpty(t))) + { + var height = MeasureTextHeight(text, font, lineSpacing); + maxHeight = Math.Max(maxHeight, height); + } + + return maxHeight; + } + + public static void DrawErrorPlaceholder(this IImageProcessingContext ctx, RectangleF bounds, string errorText, Font font) + { + // Draw placeholder background and border + ctx.Fill(UIConstants.PlaceholderFill, bounds); + ctx.Draw(UIConstants.PlaceholderBorder, 2f, bounds); + + // Auto-size font to fit the available space (start with smaller size for error text) + var maxFontSize = Math.Min((int)font.Size, 14); // Cap at 14pt for error text + var availableWidth = bounds.Width - (UIConstants.Padding * 2); + var autoSizedFont = AutoSizeFont(errorText, (int)availableWidth, maxFontSize, UIConstants.MinFontSize); + + // Draw error text with proper wrapping and top alignment + var errorTextOptions = FontUtils.CreateTextOptions(autoSizedFont, + HorizontalAlignment.Center, VerticalAlignment.Top); + + // Set wrapping length to prevent text overflow + errorTextOptions.WrappingLength = availableWidth; + + // Position text at top with padding + errorTextOptions.Origin = new PointF(bounds.X + bounds.Width / 2f, bounds.Y + UIConstants.Padding); + + ctx.DrawTextStandard(errorTextOptions, errorText, UIConstants.ErrorRed); + } + + public static Font AutoSizeFont(string text, int maxWidth, int startingSize, int minSize = UIConstants.MinFontSize, FontStyle style = FontStyle.Regular) + { + var fontSize = startingSize; + var font = FontUtils.CreateFont(fontSize, style); + var textOptions = FontUtils.CreateTextOptions(font, HorizontalAlignment.Left); + var textBounds = TextMeasurer.MeasureBounds(text, textOptions); + + while (textBounds.Width > maxWidth - (UIConstants.Padding * 2) && fontSize > minSize) + { + fontSize--; + font = FontUtils.CreateFont(fontSize, style); + textOptions = FontUtils.CreateTextOptions(font, HorizontalAlignment.Left); + textBounds = TextMeasurer.MeasureBounds(text, textOptions); + } + + return font; + } + + public static void DrawTextWithBackground(this IImageProcessingContext ctx, RectangleF backgroundBounds, + string text, Font font, Color textColor, Color backgroundColor, + HorizontalAlignment alignment = HorizontalAlignment.Center, float lineSpacing = 1.15f) + { + // Draw background + ctx.Fill(backgroundColor, backgroundBounds); + + // Draw text with proper wrapping and top alignment + var textOptions = FontUtils.CreateTextOptions(font, alignment, VerticalAlignment.Top, lineSpacing); + + // Set wrapping length to prevent text overflow + var availableWidth = backgroundBounds.Width - (UIConstants.Padding * 2); + textOptions.WrappingLength = availableWidth; + + textOptions.Origin = alignment switch + { + HorizontalAlignment.Left => new PointF(backgroundBounds.X + UIConstants.Padding, backgroundBounds.Y + UIConstants.Padding), + HorizontalAlignment.Right => new PointF(backgroundBounds.X + backgroundBounds.Width - UIConstants.Padding, backgroundBounds.Y + UIConstants.Padding), + _ => new PointF(backgroundBounds.X + backgroundBounds.Width / 2f, backgroundBounds.Y + UIConstants.Padding) + }; + + ctx.DrawTextStandard(textOptions, text, textColor); + } + } +} diff --git a/MultiImageClient/Utils/ModelServiceManager.cs b/MultiImageClient/Utils/ModelServiceManager.cs new file mode 100644 index 0000000..ed6fb01 --- /dev/null +++ b/MultiImageClient/Utils/ModelServiceManager.cs @@ -0,0 +1,235 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + // Manages local vision model services (InternVL Flask server and Ollama). + // Checks if services are running and starts them if needed before using describe mode. + public class ModelServiceManager + { + private static readonly HttpClient httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; + + public static async Task EnsureInternVLServiceIsRunningAsync(string baseUrl = "http://127.0.0.1:11415") + { + if (await IsServiceHealthyAsync(baseUrl, "/health")) + { + Logger.Log("InternVL service is already running."); + return true; + } + + Logger.Log("InternVL service not detected. Starting Flask server..."); + return await StartInternVLServiceAsync(baseUrl); + } + + public static async Task EnsureOllamaServiceIsRunningAsync(string baseUrl = "http://127.0.0.1:11434", string modelName = "qwen2-vl:latest") + { + if (!await IsOllamaRunningAsync(baseUrl)) + { + Logger.Log("Ollama service not detected. Starting Ollama..."); + var started = await StartOllamaServiceAsync(baseUrl); + if (!started) + { + return false; + } + } + else + { + Logger.Log("Ollama service is already running."); + } + + return await EnsureOllamaModelIsLoadedAsync(baseUrl, modelName); + } + + private static async Task IsServiceHealthyAsync(string baseUrl, string healthEndpoint) + { + try + { + var response = await httpClient.GetAsync($"{baseUrl}{healthEndpoint}"); + return response.IsSuccessStatusCode; + } + catch + { + return false; + } + } + + private static async Task IsOllamaRunningAsync(string baseUrl) + { + try + { + var response = await httpClient.GetAsync($"{baseUrl}/api/version"); + return response.IsSuccessStatusCode; + } + catch + { + return false; + } + } + + private static async Task EnsureOllamaModelIsLoadedAsync(string baseUrl, string modelName, int timeoutSeconds = 120) + { + try + { + Logger.Log($"Checking if Ollama model '{modelName}' is loaded (chat)..."); + + var requestBody = System.Text.Json.JsonSerializer.Serialize(new + { + model = modelName, + messages = new[] + { + new { role = "user", content = "ping" } + }, + stream = false + }); + + var content = new System.Net.Http.StringContent(requestBody, System.Text.Encoding.UTF8, "application/json"); + + httpClient.Timeout = TimeSpan.FromSeconds(timeoutSeconds); + var response = await httpClient.PostAsync($"{baseUrl}/api/chat", content); + httpClient.Timeout = TimeSpan.FromSeconds(5); + + if (response.IsSuccessStatusCode) + { + Logger.Log($"Ollama model '{modelName}' is loaded and ready."); + return true; + } + else + { + var errorContent = await response.Content.ReadAsStringAsync(); + Logger.Log($"WARNING: Ollama model '{modelName}' check failed: {response.StatusCode} - {errorContent}"); + return false; + } + } + catch (Exception ex) + { + Logger.Log($"WARNING: Could not verify Ollama model '{modelName}': {ex.Message}"); + return false; + } + } + + private static async Task StartInternVLServiceAsync(string baseUrl, int maxWaitSeconds = 600) + { + try + { + var flaskScriptPath = @"D:\proj\multiImageClient\do_flask_intern.py"; + if (!File.Exists(flaskScriptPath)) + { + Logger.Log($"ERROR: Could not find Flask script at: {flaskScriptPath}"); + return false; + } + + var pythonPath = @"d:\ai\qwen_project\venv\Scripts\python.exe"; + if (!File.Exists(pythonPath)) + { + Logger.Log($"ERROR: Could not find Python executable at: {pythonPath}"); + return false; + } + + Logger.Log($"Starting InternVL Flask server from: {flaskScriptPath}"); + Logger.Log($"Using Python: {pythonPath}"); + + var pythonExecutable = Path.Combine(Path.GetDirectoryName(pythonPath) ?? string.Empty, "pythonw.exe"); + if (!File.Exists(pythonExecutable)) + { + pythonExecutable = pythonPath; // fallback to console python if pythonw not present + } + + var startInfo = new ProcessStartInfo + { + FileName = pythonExecutable, + Arguments = $"\"{flaskScriptPath}\"", + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = Path.GetDirectoryName(flaskScriptPath) + }; + + Process.Start(startInfo); + + Logger.Log("Waiting for InternVL service to become ready..."); + var stopwatch = Stopwatch.StartNew(); + while (stopwatch.Elapsed.TotalSeconds < maxWaitSeconds) + { + await Task.Delay(2000); + if (await IsServiceHealthyAsync(baseUrl, "/health")) + { + Logger.Log($"InternVL service is ready! (took {stopwatch.Elapsed.TotalSeconds:F1}s)"); + return true; + } + Logger.Log($"Still waiting... ({stopwatch.Elapsed.TotalSeconds:F0}s elapsed)"); + } + + Logger.Log($"WARNING: InternVL service did not become ready within {maxWaitSeconds} seconds."); + return false; + } + catch (Exception ex) + { + Logger.Log($"ERROR starting InternVL service: {ex.Message}"); + return false; + } + } + + private static async Task StartOllamaServiceAsync(string baseUrl, int maxWaitSeconds = 120) + { + try + { + Logger.Log("Attempting to start Ollama service..."); + + Uri uri; + if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out uri)) + { + Logger.Log($"WARNING: Invalid Ollama base URL: {baseUrl}"); + return false; + } + + var hostPort = ($"{uri.Host}:{uri.Port}"); + var startInfo = new ProcessStartInfo + { + FileName = "ollama", + Arguments = "serve", + UseShellExecute = false, + CreateNoWindow = true + }; + startInfo.EnvironmentVariables["OLLAMA_HOST"] = hostPort; + + var process = Process.Start(startInfo); + if (process == null) + { + Logger.Log("WARNING: Could not start Ollama process. It may already be running."); + } + else + { + Logger.Log($"Started 'ollama serve' with OLLAMA_HOST={hostPort}"); + } + + Logger.Log("Waiting for Ollama service to become ready..."); + var stopwatch = Stopwatch.StartNew(); + while (stopwatch.Elapsed.TotalSeconds < maxWaitSeconds) + { + await Task.Delay(2000); + if (await IsOllamaRunningAsync(baseUrl)) + { + Logger.Log($"Ollama service is ready! (took {stopwatch.Elapsed.TotalSeconds:F1}s)"); + return true; + } + Logger.Log($" Still waiting... ({stopwatch.Elapsed.TotalSeconds:F0}s elapsed)"); + } + + Logger.Log($"WARNING: Ollama service did not become ready within {maxWaitSeconds} seconds."); + Logger.Log("If Ollama is already running elsewhere, you can ignore this warning."); + return false; + } + catch (Exception ex) + { + Logger.Log($"ERROR starting Ollama service: {ex.Message}"); + Logger.Log("If Ollama is already running, you can ignore this error."); + return false; + } + } + + } +} + diff --git a/MultiImageClient/Utils/PromptLogger.cs b/MultiImageClient/Utils/PromptLogger.cs new file mode 100644 index 0000000..13f55e0 --- /dev/null +++ b/MultiImageClient/Utils/PromptLogger.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace MultiImageClient +{ + public class PromptLogger + { + private const string LogFileName = "../../../prompt_log.json"; + + public static void LogPrompt(string prompt) + { + var logEntry = new + { + time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + prompt + }; + + var jsonLine = JsonSerializer.Serialize(logEntry); + + try + { + File.AppendAllText(LogFileName, jsonLine + Environment.NewLine); + } + catch (Exception ex) + { + Logger.Log($"Failed to write to prompt log: {ex.Message}"); + } + } + + /// Appends a freshly-typed prompt to the configured human-readable + /// prompts file (Settings.TypedPromptsAppendFile) as a single line. + /// No-op when the setting is blank. Never throws — on any failure + /// we just log and move on, because losing one entry must not break + /// a batch. + /// + /// Carefulness contract: + /// - Trims whitespace. + /// - Collapses any embedded CR/LF into a single space so one + /// prompt is always exactly one line on disk (users can paste + /// multi-line prompts without splitting the file). + /// - Skips blank / whitespace-only prompts. + /// - Creates the parent directory if the user gave a nested path. + /// - If the file exists and doesn't already end in a newline (can + /// happen after a hand-edit), inserts a leading newline so the + /// new prompt doesn't get glued onto the previous last line. + /// - Always ends the appended line with a single Environment.NewLine. + public static void AppendPromptLine(string prompt, string filePath) + { + if (string.IsNullOrWhiteSpace(filePath)) return; + if (string.IsNullOrWhiteSpace(prompt)) return; + + try + { + // Collapse any internal newlines so the file remains strictly + // one-prompt-per-line regardless of what got pasted. + var single = prompt + .Replace("\r\n", " ") + .Replace('\r', ' ') + .Replace('\n', ' ') + .Trim(); + if (single.Length == 0) return; + + // Only create the parent directory when the user actually + // specified one (a bare filename yields "" from + // GetDirectoryName and we deliberately don't touch the CWD). + var dir = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(dir)) + { + Directory.CreateDirectory(dir); + } + + // Defensive: if the file exists and its last byte isn't a + // newline, prepend one so we don't accidentally concat onto + // an unterminated last line (happens with hand-edited files). + string leading = ""; + if (File.Exists(filePath)) + { + using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + if (fs.Length > 0) + { + fs.Seek(-1, SeekOrigin.End); + int last = fs.ReadByte(); + if (last != '\n') leading = Environment.NewLine; + } + } + + File.AppendAllText(filePath, leading + single + Environment.NewLine); + } + catch (Exception ex) + { + Logger.Log($"PromptLogger.AppendPromptLine: failed to append to '{filePath}': {ex.Message}"); + } + } + } +} diff --git a/MultiImageClient/Utils/UIConstants.cs b/MultiImageClient/Utils/UIConstants.cs new file mode 100644 index 0000000..623211d --- /dev/null +++ b/MultiImageClient/Utils/UIConstants.cs @@ -0,0 +1,26 @@ +using SixLabors.ImageSharp; + +namespace MultiImageClient +{ + public static class UIConstants + { + // Shared across multiple files + public const int Padding = 10; + public const float LineSpacing = 1.15f; + public const int MinFontSize = 8; + public const int TextDpi = 150; + public const int LabelHeight = 20; // Default height for labels under images + + // Colors + public static readonly Color White = Color.White; + public static readonly Color Black = Color.Black; + public static readonly Color SuccessGreen = Color.FromRgb(0, 120, 0); + public static readonly Color ErrorRed = Color.FromRgb(180, 0, 0); + public static readonly Color PlaceholderFill = Color.FromRgb(240, 240, 240); + public static readonly Color PlaceholderBorder = Color.FromRgb(200, 200, 200); + public static readonly Color Gold = Color.FromRgb(255, 215, 0); + // We do NOT use gray for text. See AGENTS.md > Visual & Typography Policy. + // Secondary labels must get their visual hierarchy from smaller font size and/or + // by reusing the existing semantic color (SuccessGreen, ErrorRed, Black, Gold). + } +} diff --git a/MultiImageClient/Workflows/AllProvidersShowcaseWorkflow.cs b/MultiImageClient/Workflows/AllProvidersShowcaseWorkflow.cs new file mode 100644 index 0000000..d860021 --- /dev/null +++ b/MultiImageClient/Workflows/AllProvidersShowcaseWorkflow.cs @@ -0,0 +1,198 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + /// One-shot "every provider, one image" runner: takes a SINGLE prompt and + /// fires it at one flagship generator per provider in parallel (see + /// GeneratorGroups.GetOnePerProvider — gpt-image-2, Ideogram 4.0, + /// flux-2-pro-preview, Recraft V4.1, Grok Imagine, Nano Banana Pro, and + /// optionally Grok video). Each result is saved via the normal + /// ImageManager pipeline, then everything is composed into ONE combined + /// contact-sheet grid which pops open in the default viewer. + /// + /// Providers whose API keys are missing/invalid fail soft: their cell in + /// the sheet shows the error placeholder instead of an image, so a single + /// run doubles as a key/auth health check across every provider. + public class AllProvidersShowcaseWorkflow + { + public async Task RunAsync( + AbstractPromptSource promptSource, + Settings settings, + MultiClientRunStats stats, + RunOptions options) + { + var promptText = promptSource.Prompts + .Select(p => p.Prompt) + .FirstOrDefault(s => !string.IsNullOrWhiteSpace(s)); + + if (string.IsNullOrWhiteSpace(promptText)) + { + Console.Error.WriteLine( + "All-providers showcase aborted: no prompt available. " + + "Supply --prompt \"...\" or point PromptFiles at a readable file."); + return null; + } + + var groups = new GeneratorGroups(settings, concurrency: 1, stats); + var generators = groups.GetOnePerProvider(includeVideo: options.WithVideo).ToList(); + + var imageManager = new ImageManager(settings, stats); + Logger.Log($"All-providers showcase: 1 prompt x {generators.Count} providers" + + (options.WithVideo ? " (including Grok video)" : "") + "."); + Logger.Log($" prompt: {promptText}"); + foreach (var g in generators) + { + Logger.Log($" - {g.GetGeneratorSpecPart()} (~${g.GetCost():0.###})"); + } + + var tasks = generators.Select(async generator => + { + var pd = new PromptDetails(); + pd.ReplacePrompt(promptText, promptText, TransformationType.InitialPrompt); + + // Fail fast on obviously-bad credentials (empty, or still the + // "Optional: get your key from..." template placeholder text) + // so the cell explains the real problem instead of whatever + // confusing 400/401 the provider would return. + var keyProblem = DescribeKeyProblem(generator.ApiType, settings); + if (keyProblem != null) + { + Logger.Log($"All-providers :: {Flatten(generator.GetGeneratorSpecPart())} :: SKIPPED ({keyProblem})"); + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = keyProblem, + PromptDetails = pd, + ImageGenerator = generator.ApiType, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + }; + } + + try + { + var result = await generator.ProcessPromptAsync(generator, pd); + await imageManager.ProcessAndSaveAsync(result, generator); + var label = result.IsSuccess ? "OK" : $"FAIL ({Trim(result.ErrorMessage ?? "", 160)})"; + Logger.Log($"All-providers :: {Flatten(generator.GetGeneratorSpecPart())} :: {label}"); + return result; + } + catch (Exception ex) + { + Logger.Log($"All-providers :: {Flatten(generator.GetGeneratorSpecPart())} :: EXCEPTION {ex.Message}"); + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = ex.Message, + PromptDetails = pd, + ImageGenerator = generator.ApiType, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + }; + } + }).ToArray(); + + var results = await Task.WhenAll(tasks); + stats.PrintStats(); + + var okCount = results.Count(r => r.IsSuccess); + Logger.Log($"All-providers showcase: {okCount}/{results.Length} providers succeeded."); + + // Videos don't belong in a still-image mosaic — the mp4 is already + // saved to disk (and noted in the log/ledger), so just report it + // and keep the contact sheet images-only. + var sheetResults = results.Where(r => r.ImageGenerator != ImageGeneratorApiType.GrokImagineVideo).ToArray(); + foreach (var v in results.Where(r => r.ImageGenerator == ImageGeneratorApiType.GrokImagineVideo)) + { + Logger.Log(v.IsSuccess + ? "All-providers: Grok video succeeded; mp4 saved under the day folder's Video\\ subfolder (excluded from the PNG sheet)." + : $"All-providers: Grok video FAILED ({Trim(v.ErrorMessage ?? "", 160)})."); + } + + var sheetGenerators = generators.Where(g => g.ApiType != ImageGeneratorApiType.GrokImagineVideo).ToList(); + var recap = BuildRecapString(promptText, sheetGenerators.Select(g => g.GetGeneratorSpecPart()).ToList()); + try + { + var outPath = await ImageCombiner.CreateBatchLayoutImageSquareAsync( + sheetResults, recap, settings, openWhenDone: true); + Logger.Log($"All-providers contact sheet: {outPath}"); + return outPath; + } + catch (Exception ex) + { + Logger.Log($"All-providers showcase: failed to build contact sheet: {ex.Message}"); + return null; + } + } + + /// Returns a human-actionable description of what's wrong with the + /// API key this generator needs, or null when the key looks usable. + /// "Looks usable" is deliberately shallow — real validation happens + /// at the provider — but it catches the two states that otherwise + /// produce baffling provider errors: empty, and "still contains the + /// settings-template placeholder sentence". + private static string? DescribeKeyProblem(ImageGeneratorApiType apiType, Settings settings) + { + var (keyName, keyValue) = apiType switch + { + ImageGeneratorApiType.Dalle3 or ImageGeneratorApiType.GptImage1 + or ImageGeneratorApiType.GptImage1Mini or ImageGeneratorApiType.GptImage2 + => ("OpenAIApiKey", settings.OpenAIApiKey), + ImageGeneratorApiType.Ideogram or ImageGeneratorApiType.IdeogramV3 or ImageGeneratorApiType.IdeogramV4 + => ("IdeogramApiKey", settings.IdeogramApiKey), + ImageGeneratorApiType.BFLv11 or ImageGeneratorApiType.BFLv11Ultra + or ImageGeneratorApiType.BFLFlux2Pro or ImageGeneratorApiType.BFLFlux2Max + or ImageGeneratorApiType.BFLFlux2Flex or ImageGeneratorApiType.BFLFlux2Klein4b + or ImageGeneratorApiType.BFLFlux2Klein9b or ImageGeneratorApiType.BFLFluxKontextPro + or ImageGeneratorApiType.BFLFluxKontextMax or ImageGeneratorApiType.BFLFlux2ProPreview + => ("BFLApiKey", settings.BFLApiKey), + ImageGeneratorApiType.Recraft or ImageGeneratorApiType.RecraftV4 + or ImageGeneratorApiType.RecraftV4Pro or ImageGeneratorApiType.RecraftV41 + or ImageGeneratorApiType.RecraftV41Pro + => ("RecraftApiKey", settings.RecraftApiKey), + ImageGeneratorApiType.GrokImagine or ImageGeneratorApiType.GrokImaginePro + or ImageGeneratorApiType.GrokImagineVideo + => ("XAIGrokApiKey", settings.XAIGrokApiKey), + ImageGeneratorApiType.GoogleNanoBanana or ImageGeneratorApiType.GoogleNanoBananaPro + or ImageGeneratorApiType.GoogleImagen4 + => ("GoogleGeminiApiKey", settings.GoogleGeminiApiKey), + _ => ((string?)null, (string?)null), + }; + if (keyName == null) + { + return null; + } + if (string.IsNullOrWhiteSpace(keyValue)) + { + return $"settings.json: {keyName} is empty — paste a real key to enable this provider"; + } + // Real keys from every provider here are single tokens; the + // settings template's descriptive sentences contain spaces. + if (keyValue.Contains(' ') || keyValue.StartsWith("Optional", StringComparison.OrdinalIgnoreCase)) + { + return $"settings.json: {keyName} still contains the template placeholder text — paste a real key to enable this provider"; + } + return null; + } + + private static string BuildRecapString(string prompt, IReadOnlyList generatorLabels) + { + var header = $"All-providers contact sheet — {generatorLabels.Count} providers, one image each:"; + var list = string.Join(" | ", generatorLabels); + return $"{header}\n{list}\n\nPrompt: {prompt}"; + } + + /// Some generators (e.g. Recraft) embed newlines in their spec part + /// for annotation layout; collapse them so log lines stay one-line. + private static string Flatten(string s) + => s.Replace("\r", " ").Replace("\n", " "); + + private static string Trim(string s, int max) + { + if (string.IsNullOrEmpty(s)) return s ?? string.Empty; + return s.Length <= max ? s : s.Substring(0, max - 1) + "..."; + } + } +} diff --git a/MultiImageClient/Workflows/BatchWorkflow.cs b/MultiImageClient/Workflows/BatchWorkflow.cs new file mode 100644 index 0000000..2cd8c84 --- /dev/null +++ b/MultiImageClient/Workflows/BatchWorkflow.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + /// For each prompt from the source, asks the user whether to keep / skip / edit + /// it, then sends the (possibly edited) prompt in parallel to every generator + /// returned by GeneratorGroups.GetAll(). Results are composed into a single + /// labelled grid image per prompt. + public class BatchWorkflow + { + public async Task RunAsync(AbstractPromptSource promptSource, Settings settings, int concurrency, MultiClientRunStats stats, RunOptions options = null) + { + options ??= new RunOptions(); + var generators = new GeneratorGroups(settings, concurrency, stats, fast: options.Fast, quickTest: options.QuickTest).GetAll().ToList(); + var imageManager = new ImageManager(settings, stats); + + int processed = 0; + foreach (var promptString in promptSource.Prompts) + { + if (processed >= options.Limit) + { + Logger.Log($"--limit {options.Limit} reached; stopping batch."); + return true; + } + + Logger.Log($"\n--- Processing prompt: {promptString.Prompt}"); + + if (!options.Auto) + { + System.Console.WriteLine("\nDo you accept this? y for yes, n for skip, or type the prompt you want directly and hit enter. (q to quit)"); + var val = System.Console.ReadLine(); + if (val == null) + { + Logger.Log("stdin closed; ending batch."); + return true; + } + val = val.Trim(); + + if (val == "q") + { + return true; + } + if (val == "n") + { + continue; + } + if (val.Length > 0 && val != "y") + { + PromptLogger.LogPrompt(val); + PromptLogger.AppendPromptLine(val, settings.TypedPromptsAppendFile); + promptString.UndoLastStep(); + promptString.ReplacePrompt(val, "explanation", TransformationType.InitialPrompt); + } + } + + if (promptString.Prompt.Length == 0) + { + System.Console.WriteLine("empty prompt, skipping."); + continue; + } + processed++; + + var generatorTasks = generators.Select(async generator => + { + PromptDetails theCopy = null; + try + { + theCopy = promptString.Copy(); + Logger.Log($" -> SENDING to {generator.GetGeneratorSpecPart()} : {theCopy.Prompt}"); + var result = await generator.ProcessPromptAsync(generator, theCopy); + await imageManager.ProcessAndSaveAsync(result, generator); + var status = result.IsSuccess ? "OK" : $"FAIL ({result.ErrorMessage})"; + Logger.Log($" <- {status} from {generator.GetGeneratorSpecPart()} in {result.CreateTotalMs + result.DownloadTotalMs} ms"); + return result; + } + catch (System.Exception ex) + { + Logger.Log($" <- EXCEPTION from {generator.GetGeneratorSpecPart()}: {ex.Message}"); + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = ex.Message, + PromptDetails = theCopy ?? promptString, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + }; + } + }).ToArray(); + + stats.PrintStats(); + var results = await Task.WhenAll(generatorTasks); + + try + { + var composed = await ImageCombiner.CreateBatchLayoutImageSquareAsync(results, promptString.Prompt, settings); + Logger.Log($"Combined images saved to: {composed}"); + } + catch (System.Exception ex) + { + Logger.Log($"Failed to combine images for prompt '{promptString.Prompt}': {ex.Message}"); + } + } + return true; + } + } +} diff --git a/MultiImageClient/Workflows/GeneratorGroups.cs b/MultiImageClient/Workflows/GeneratorGroups.cs new file mode 100644 index 0000000..925195d --- /dev/null +++ b/MultiImageClient/Workflows/GeneratorGroups.cs @@ -0,0 +1,570 @@ +using IdeogramAPIClient; + +using OpenAI.Images; + +using RecraftAPIClient; + +using System.Collections.Generic; + +namespace MultiImageClient +{ + /// Declares which image generators are active for the current run. + /// + /// Design rules: + /// 1. Only generators in the list returned by GetAll() are ever constructed. + /// If you don't use Recraft, you never need a Recraft API key. + /// 2. Each generator is built by its own factory method so you can read the + /// active set at a glance and tweak one line at a time. + /// 3. Add a new variant by adding a factory method below, then adding a call + /// in GetAll(). + /// + /// Keep the factory methods even when commented out of GetAll() — they're the + /// menu of things you can flip on. + public class GeneratorGroups + { + private readonly Settings _settings; + private readonly int _concurrency; + private readonly MultiClientRunStats _stats; + private readonly bool _fast; + private readonly bool _quickTest; + + public GeneratorGroups(Settings settings, int concurrency, MultiClientRunStats stats, bool fast = false, bool quickTest = false) + { + _settings = settings; + _concurrency = concurrency; + _stats = stats; + _fast = fast; + _quickTest = quickTest; + } + + /// The active generator set for the current run. + /// Edit this list to pick which generators hit each prompt. + /// + /// DEFAULT MODE: gpt-image-2 random-AR + random-quality with moderation=low. + /// Each call rolls a fresh {square|portrait|landscape} x {low|medium|high} + /// so a single batch exercises the full option space of the endpoint. + /// + /// FAST MODE (--fast): a single fixed gpt-image-2 low/square/moderation=low + /// call per prompt — cheapest (~$0.02) and usually completes in 10-15s. + /// Intended for iteration and smoke-testing, not production runs. + public IEnumerable GetAll() + { + if (_quickTest) + { + return new List + { + GptImage2QuickTest(), + }; + } + + if (_fast) + { + return new List + { + GptImage2FastTest(), + }; + } + + return new List + { + GptImage2RandomMinimalSafety(), + // --- fixed gpt-image-2 variants, useful for targeted tests --- + // GptImage2HighSquare(), + // GptImage2MediumPortrait(), + // GptImage2HighWide(), + // GptImage2High2K(), + // GptImage2HighQhdWide(), // 2560x1440 QHD, cookbook's recommended upper reliability boundary + // GptImage2LogoVariants(n: 4), // one call -> N candidates; see cookbook 4.5 + // --- other OpenAI image models --- + // Dalle3Square(), + // Dalle3Wide(), + // GptImage1HighSquare(), + // GptImageMiniHighWide(), + // --- non-OpenAI providers (require extra keys in settings.json) --- + // IdeogramV4_Square(), // Ideogram 4.0 (2026-06-03), current flagship + // Ideogram_V3_Wide_Quality(), + // RecraftV41AnyStyle(), // Recraft V4.1 (2026), current flagship + // RecraftV4AnyStyle(), + // BFLv11_3_2(), + // BFLv11Ultra_1_1(), + // BFLFlux2ProPreview_Square(), // BFL's latest [pro], lands improvements first + // BFLFlux2Pro_Square(), + // BFLFlux2Max_Square(), + // BFLFlux2Flex_Square(), // typography-tuned; enable when you have text prompts + // BFLFlux2Klein9b_Square(), // sub-second cheap draft variant + // LocalFlux2Uncensored_Square(), // local ComfyUI Flux2 Klein + uncensored text encoder workflow + // SeedreamDirect_Square(), // ByteDance ModelArk Seedream 4.5/5.0 + // HailuoDirect_Square(), // MiniMax image-01 / image-01-live + // KreaDirect_Square(), // Krea 2 medium/large + // BriaDirect_Square(), // BRIA FIBO direct API + // MagnificMysticDirect_Square(), + // LumaPhotonDirect_Square(), + // RunwayGen4Direct_Square(), + // StabilityAiDirect_Square(), // SD3.5 via Stability API + // RecraftV4ProRealisticPortrait(), + // RecraftAnyStyle(), // legacy V3 + // xAI Grok Imagine (launched 2026-01-28). Pro is 3.5x the price + // (~$0.07 vs $0.02) and rate-limited to 30 rpm vs 300 rpm, so + // we default to the cheap tier here and keep Pro as a toggle. + GrokImagine_Square(), + // GrokImaginePro_Square(), + // GrokVideo_Wide(), // VIDEO: mp4 saved to day\Video\, card in the grid + // GeminiNanoBanana(), // gemini-3.1-flash-image (Nano Banana 2), 1:1 2K + // GeminiNanoBananaPro(), // gemini-3-pro-image (Nano Banana Pro), 1:1 2K + // GeminiNanoBananaTallPortrait(), // 9:16 2K — replacement for the old Imagen 2:5 slot + // GeminiNanoBananaPro4K(), // pro tier at 4K (~2x token cost) + // GoogleImagen4_2_5(), // DEAD 2026-06-24..30 (Imagen shutdown) — do not re-enable + }; + } + + /// One representative generator per provider — the "contact sheet" + /// acceptance set used by --all-providers. Each entry is the current + /// flagship (June 2026) for that provider: + /// OpenAI gpt-image-2 (high, square) + /// Ideogram Ideogram 4.0 (DEFAULT speed, 2048x2048) + /// BFL flux-2-pro-preview (latest [pro]) + /// Recraft V4.1 (any style) + /// xAI grok-imagine-image (high, 2k) + /// Google gemini-3-pro-image (Nano Banana Pro) + /// includeVideo additionally appends the xAI grok-imagine-video + /// generator (mp4 saved to disk, PNG card in the grid). + public IEnumerable GetOnePerProvider(bool includeVideo = false) + { + var list = new List + { + GptImage2HighSquare(), + IdeogramV4_Square(), + BFLFlux2ProPreview_Square(), + RecraftV41AnyStyle(), + GrokImagine_Square(), + GeminiNanoBananaPro(), + }; + if (includeVideo) + { + list.Add(GrokVideo_Wide()); + } + return list; + } + + // ---------- OpenAI: DALL·E 3 ---------- + + private Dalle3Generator Dalle3Square() => + new Dalle3Generator(_settings.OpenAIApiKey, _concurrency, + GeneratedImageQuality.High, GeneratedImageSize.W1024xH1024, _stats, ""); + + private Dalle3Generator Dalle3Wide() => + new Dalle3Generator(_settings.OpenAIApiKey, _concurrency, + GeneratedImageQuality.High, GeneratedImageSize.W1792xH1024, _stats, ""); + + // ---------- OpenAI: gpt-image-1 / gpt-image-1-mini ---------- + + private GptImageOneGenerator GptImage1HighSquare() => + new GptImageOneGenerator(_settings.OpenAIApiKey, _concurrency, + "1024x1024", "low", OpenAIGPTImageOneQuality.high, + ImageGeneratorApiType.GptImage1, _stats, ""); + + private GptImageOneGenerator GptImageMiniHighWide() => + new GptImageOneGenerator(_settings.OpenAIApiKey, _concurrency, + "1536x1024", "low", OpenAIGPTImageOneQuality.high, + ImageGeneratorApiType.GptImage1Mini, _stats, ""); + + // ---------- OpenAI: gpt-image-2 (released 2026-04-21) ---------- + // Quality: low / medium / high / auto. + // Size can be any of 1024x1024, 1536x1024, 1024x1536, 2048x2048, + // 2048x1152, 2560x1440 (QHD — cookbook's recommended upper + // reliability boundary), 3824x2144 (near-4K), or "auto". Cookbook + // enforces max edge STRICTLY < 3840 so the legacy 3840x2160 is + // treated as experimental in our size validator. Moderation is + // "auto" (default) or "low" (minimal safety, permissible content + // only). + // + // Default for batch runs: random AR x random quality with moderation="low". + // The size pool sticks to the three canonical 1024-edge aspect ratios so + // per-call cost stays bounded. Quality pool is low/medium/high (no "auto" + // — we want explicit control and predictable billing). + // + // Variant factories take an optional `n` (images per call) to support + // cookbook patterns like logo-variant generation (section 4.5) where + // one round-trip returns N candidates. N=1 preserves prior behavior. + // --fast: lowest quality, smallest size, permissive moderation. Deterministic + // fixed variant so test runs are cheap and consistent across invocations. + private GptImage2Generator GptImage2FastTest() => + new GptImage2Generator(_settings.OpenAIApiKey, _concurrency, + "1024x1024", "low", OpenAIGPTImageOneQuality.low, _stats, "fast"); + + // --quick-test: identical cheapest config as --fast, but saves each + // streamed partial PNG under the day folder and pops it up in the + // default image viewer as it arrives. Intended for interactive + // development where watching the refinement trajectory is the point. + private GptImage2Generator GptImage2QuickTest() => + new GptImage2Generator(_settings.OpenAIApiKey, _concurrency, + sizePool: new[] { "1024x1024" }, + moderation: "low", + qualityPool: new[] { OpenAIGPTImageOneQuality.low }, + stats: _stats, + name: "quick", + partialSaveFolder: _settings.ImageDownloadBaseFolder, + popUpPartials: true); + + private GptImage2Generator GptImage2RandomMinimalSafety(int n = 1) => + new GptImage2Generator(_settings.OpenAIApiKey, _concurrency, + sizePool: new[] { "1024x1024", "1024x1536", "1536x1024" }, + moderation: "low", + qualityPool: new[] + { + OpenAIGPTImageOneQuality.low, + OpenAIGPTImageOneQuality.medium, + OpenAIGPTImageOneQuality.high, + }, + stats: _stats, name: "", imageCount: n); + + // Logo/variant exploration per cookbook 4.5: one API call, N + // candidates returned in the same round-trip. Filenames pick up the + // imgN suffix automatically via FileNameGenerator so the grid shows + // all variants side-by-side. Kept off by default (in the commented + // GetAll block) because it multiplies per-prompt cost linearly. + private GptImage2Generator GptImage2LogoVariants(int n = 4) => + new GptImage2Generator(_settings.OpenAIApiKey, _concurrency, + sizePool: new[] { "1024x1024" }, + moderation: "low", + qualityPool: new[] { OpenAIGPTImageOneQuality.medium }, + stats: _stats, name: "variants", imageCount: n); + + private GptImage2Generator GptImage2HighSquare() => + new GptImage2Generator(_settings.OpenAIApiKey, _concurrency, + "1024x1024", "low", OpenAIGPTImageOneQuality.high, _stats, ""); + + private GptImage2Generator GptImage2MediumPortrait() => + new GptImage2Generator(_settings.OpenAIApiKey, _concurrency, + "1024x1536", "low", OpenAIGPTImageOneQuality.medium, _stats, ""); + + private GptImage2Generator GptImage2HighWide() => + new GptImage2Generator(_settings.OpenAIApiKey, _concurrency, + "1536x1024", "low", OpenAIGPTImageOneQuality.high, _stats, ""); + + private GptImage2Generator GptImage2High2K() => + new GptImage2Generator(_settings.OpenAIApiKey, _concurrency, + "2048x2048", "low", OpenAIGPTImageOneQuality.high, _stats, ""); + + // Cookbook "recommended upper reliability boundary" at 2K / QHD. + // Use when you want more detail than 2048 square but don't want to + // tip into the experimental near-4K territory. + private GptImage2Generator GptImage2HighQhdWide() => + new GptImage2Generator(_settings.OpenAIApiKey, _concurrency, + "2560x1440", "low", OpenAIGPTImageOneQuality.high, _stats, ""); + + // ---------- Ideogram ---------- + + private IdeogramGenerator Ideogram_V2_Design_16_10() => + new IdeogramGenerator(_settings.IdeogramApiKey, _concurrency, + IdeogramMagicPromptOption.ON, IdeogramAspectRatio.ASPECT_16_10, + IdeogramStyleType.DESIGN, "", IdeogramModel.V_2, _stats, ""); + + private IdeogramGenerator Ideogram_V2Turbo_Square() => + new IdeogramGenerator(_settings.IdeogramApiKey, _concurrency, + IdeogramMagicPromptOption.OFF, IdeogramAspectRatio.ASPECT_1_1, + null, "", IdeogramModel.V_2_TURBO, _stats, ""); + + private IdeogramV3Generator Ideogram_V3_Wide_Quality() => + new IdeogramV3Generator(_settings.IdeogramApiKey, _concurrency, + IdeogramV3StyleType.AUTO, IdeogramMagicPromptOption.ON, + IdeogramAspectRatio.ASPECT_16_10, IdeogramRenderingSpeed.QUALITY, + "", _stats, ""); + + // ---------- Ideogram 4.0 (released 2026-06-03) ---------- + // JSON endpoint /v1/ideogram-v4/generate; 2K-native resolutions only + // ("2048x2048", "2304x1728", "2560x1440", ...). rendering_speed: + // FLASH (cheapest) | TURBO | DEFAULT | QUALITY. No style_type / + // magic_prompt knobs — text_prompt is auto-expanded server-side into + // the structured JSON prompt contract. + + private IdeogramV4Generator IdeogramV4_Square() => + new IdeogramV4Generator(_settings.IdeogramApiKey, _concurrency, + "2048x2048", IdeogramRenderingSpeed.DEFAULT, _stats, ""); + + private IdeogramV4Generator IdeogramV4_Wide_Quality() => + new IdeogramV4Generator(_settings.IdeogramApiKey, _concurrency, + "2560x1440", IdeogramRenderingSpeed.QUALITY, _stats, ""); + + private IdeogramV4Generator IdeogramV4_Flash() => + new IdeogramV4Generator(_settings.IdeogramApiKey, _concurrency, + "2048x2048", IdeogramRenderingSpeed.FLASH, _stats, ""); + + // ---------- Black Forest Labs (Flux) ---------- + + private BFLGenerator BFLv11_3_2() => + new BFLGenerator(ImageGeneratorApiType.BFLv11, _settings.BFLApiKey, + _concurrency, "3:2", false, 1024, 1024, _stats, ""); + + private BFLGenerator BFLv11Ultra_1_1() => + new BFLGenerator(ImageGeneratorApiType.BFLv11Ultra, _settings.BFLApiKey, + _concurrency, "1:1", false, 1024, 1024, _stats, ""); + + private BFLGenerator BFLv11Ultra_3_2_Upsampled() => + new BFLGenerator(ImageGeneratorApiType.BFLv11Ultra, _settings.BFLApiKey, + _concurrency, "3:2", true, 1024, 1024, _stats, ""); + + // ---------- Black Forest Labs (FLUX.2 — current generation) ---------- + // + // FLUX.2 is megapixel-priced. 1024x1024 is roughly 1 MP so the per-image + // cost tracks the headline rate in the docs (pro $0.03, max $0.07, etc.). + // Klein is the cheap fast tier; flex is the typography-tuned variant with + // adjustable steps/guidance (BFLGenerator sets sensible defaults). + + private BFLGenerator BFLFlux2Pro_Square() => + new BFLGenerator(ImageGeneratorApiType.BFLFlux2Pro, _settings.BFLApiKey, + _concurrency, "1:1", false, 1024, 1024, _stats, ""); + + // flux-2-pro-preview: where BFL lands the latest [pro] improvements + // first (~2x speed at no quality cost as of mid-2026). Same wire + // contract and price as flux-2-pro; prefer this unless a run needs + // pinned-model reproducibility. + private BFLGenerator BFLFlux2ProPreview_Square() => + new BFLGenerator(ImageGeneratorApiType.BFLFlux2ProPreview, _settings.BFLApiKey, + _concurrency, "1:1", false, 1024, 1024, _stats, ""); + + private BFLGenerator BFLFlux2Pro_Wide() => + new BFLGenerator(ImageGeneratorApiType.BFLFlux2Pro, _settings.BFLApiKey, + _concurrency, "16:9", false, 1536, 864, _stats, ""); + + private BFLGenerator BFLFlux2Max_Square() => + new BFLGenerator(ImageGeneratorApiType.BFLFlux2Max, _settings.BFLApiKey, + _concurrency, "1:1", false, 1024, 1024, _stats, ""); + + private BFLGenerator BFLFlux2Flex_Square() => + new BFLGenerator(ImageGeneratorApiType.BFLFlux2Flex, _settings.BFLApiKey, + _concurrency, "1:1", false, 1024, 1024, _stats, ""); + + private BFLGenerator BFLFlux2Klein4b_Square() => + new BFLGenerator(ImageGeneratorApiType.BFLFlux2Klein4b, _settings.BFLApiKey, + _concurrency, "1:1", false, 1024, 1024, _stats, ""); + + private BFLGenerator BFLFlux2Klein9b_Square() => + new BFLGenerator(ImageGeneratorApiType.BFLFlux2Klein9b, _settings.BFLApiKey, + _concurrency, "1:1", false, 1024, 1024, _stats, ""); + + private LocalFlux2ComfyGenerator LocalFlux2Uncensored_Square() => + new LocalFlux2ComfyGenerator(_settings, _concurrency, _stats, "local-flux2-uncensored"); + + // ---------- Direct first-party image APIs ---------- + + private DirectImageApiGenerator SeedreamDirect_Square() => + new DirectImageApiGenerator(DirectImageProvider.ByteDanceSeedream, + _settings, _concurrency, _stats, "seedream-direct"); + + private DirectImageApiGenerator HailuoDirect_Square() => + new DirectImageApiGenerator(DirectImageProvider.MiniMaxHailuo, + _settings, _concurrency, _stats, "hailuo-direct"); + + private DirectImageApiGenerator KreaDirect_Square() => + new DirectImageApiGenerator(DirectImageProvider.Krea, + _settings, _concurrency, _stats, "krea-direct"); + + private DirectImageApiGenerator BriaDirect_Square() => + new DirectImageApiGenerator(DirectImageProvider.Bria, + _settings, _concurrency, _stats, "bria-direct"); + + private DirectImageApiGenerator MagnificMysticDirect_Square() => + new DirectImageApiGenerator(DirectImageProvider.Magnific, + _settings, _concurrency, _stats, "magnific-direct"); + + private DirectImageApiGenerator LumaPhotonDirect_Square() => + new DirectImageApiGenerator(DirectImageProvider.LumaPhoton, + _settings, _concurrency, _stats, "luma-direct"); + + private DirectImageApiGenerator RunwayGen4Direct_Square() => + new DirectImageApiGenerator(DirectImageProvider.Runway, + _settings, _concurrency, _stats, "runway-direct"); + + private DirectImageApiGenerator StabilityAiDirect_Square() => + new DirectImageApiGenerator(DirectImageProvider.StabilityAi, + _settings, _concurrency, _stats, "stability-direct"); + + // ---------- Recraft ---------- + + private RecraftGenerator RecraftAnyStyle() => + new RecraftGenerator(_settings.RecraftApiKey, _concurrency, + RecraftImageSize._1365x1024, RecraftStyle.any, null, null, null, + _stats, ""); + + private RecraftGenerator RecraftRealisticStudioPortrait() => + new RecraftGenerator(_settings.RecraftApiKey, _concurrency, + RecraftImageSize._2048x1024, RecraftStyle.realistic_image, + null, null, RecraftRealisticImageSubstyle.studio_portrait, + _stats, ""); + + private RecraftGenerator RecraftVectorLineArt() => + new RecraftGenerator(_settings.RecraftApiKey, _concurrency, + RecraftImageSize._1365x1024, RecraftStyle.vector_illustration, + RecraftVectorIllustrationSubstyle.line_art, null, null, + _stats, ""); + + // ---------- Recraft V4 (drop-in upgrade over V3) ---------- + // V4 raster is priced identically to V3 at $0.04/image and claims better + // prompt adherence + text rendering. V4 Pro is $0.25 for high-res, print- + // ready output. Pass model: RecraftModel.recraftv4 or .recraftv4pro to + // RecraftGenerator and everything else (style/substyle/artistic_level) + // works the same. + // + // IMPORTANT: V4/V4.1 use a DIFFERENT size set than V2/V3. The old + // 1365x1024 / 2048x1024 etc. are rejected with invalid_request_parameter. + // Standard models: 1:1 is 1024x1024 (others: 1536x768, 1280x832, + // 1216x896, ...). Pro models: 1:1 is 2048x2048 (others: 3072x1536, + // 2560x1664, 2432x1792, ...). See recraft.ai/docs appendix. + + private RecraftGenerator RecraftV4AnyStyle() => + new RecraftGenerator(_settings.RecraftApiKey, _concurrency, + RecraftImageSize._1024x1024, RecraftStyle.any, null, null, null, + _stats, "", model: RecraftModel.recraftv4); + + private RecraftGenerator RecraftV4RealisticStudioPortrait() => + new RecraftGenerator(_settings.RecraftApiKey, _concurrency, + RecraftImageSize._1024x1024, RecraftStyle.realistic_image, + null, null, RecraftRealisticImageSubstyle.studio_portrait, + _stats, "", model: RecraftModel.recraftv4); + + private RecraftGenerator RecraftV4VectorLineArt() => + new RecraftGenerator(_settings.RecraftApiKey, _concurrency, + RecraftImageSize._1024x1024, RecraftStyle.vector_illustration, + RecraftVectorIllustrationSubstyle.line_art, null, null, + _stats, "", model: RecraftModel.recraftv4); + + private RecraftGenerator RecraftV4ProRealisticPortrait() => + new RecraftGenerator(_settings.RecraftApiKey, _concurrency, + RecraftImageSize._2048x2048, RecraftStyle.realistic_image, + null, null, RecraftRealisticImageSubstyle.studio_portrait, + _stats, "", model: RecraftModel.recraftv4pro); + + private RecraftGenerator RecraftV4ProAnyStyle() => + new RecraftGenerator(_settings.RecraftApiKey, _concurrency, + RecraftImageSize._2048x2048, RecraftStyle.any, null, null, null, + _stats, "", model: RecraftModel.recraftv4pro); + + // ---------- Recraft V4.1 (2026, current flagship) ---------- + // Drop-in over V4: better photorealism, new illustration styles, and + // strong short-prompt aesthetics. API model strings: recraftv4_1 / + // recraftv4_1_pro (the API's own default model is now recraftv4_1). + // Same style/substyle/artistic_level plumbing as V3/V4, and the same + // V4 size set (1024x1024 standard / 2048x2048 pro for 1:1). + + private RecraftGenerator RecraftV41AnyStyle() => + new RecraftGenerator(_settings.RecraftApiKey, _concurrency, + RecraftImageSize._1024x1024, RecraftStyle.any, null, null, null, + _stats, "", model: RecraftModel.recraftv4_1); + + private RecraftGenerator RecraftV41RealisticStudioPortrait() => + new RecraftGenerator(_settings.RecraftApiKey, _concurrency, + RecraftImageSize._1024x1024, RecraftStyle.realistic_image, + null, null, RecraftRealisticImageSubstyle.studio_portrait, + _stats, "", model: RecraftModel.recraftv4_1); + + private RecraftGenerator RecraftV41ProAnyStyle() => + new RecraftGenerator(_settings.RecraftApiKey, _concurrency, + RecraftImageSize._2048x2048, RecraftStyle.any, null, null, null, + _stats, "", model: RecraftModel.recraftv4_1_pro); + + // ---------- xAI Grok Imagine ---------- + // + // Two tiers, same REST endpoint: + // grok-imagine-image $0.02/image, 300 rpm + // grok-imagine-image-pro $0.07/image, 30 rpm + // + // aspect_ratio accepts the xAI-documented set ("1:1", "3:4", "4:3", + // "16:9", "9:16", "2:3", "3:2", "9:19.5", "19.5:9", "9:20", "20:9", + // "1:2", "2:1", "auto"). quality is low|medium|high; resolution is + // 1k|2k. All other knobs (size, style) are explicitly unsupported by + // the xAI API and we do not send them. + + // Standard-tier Grok Imagine defaults to the maximum quality and + // resolution this tier supports (high + 2k). Per xAI's pricing page + // resolution doesn't affect per-image price on the standard tier + // ($0.02 flat), so 2k is a free upgrade over 1k. + public GrokImagineGenerator GrokImagine_Square() => + new GrokImagineGenerator(_settings.XAIGrokApiKey, _concurrency, + ImageGeneratorApiType.GrokImagine, _stats, "", + aspectRatio: "1:1", quality: "high", resolution: "2k", settings: _settings); + + public GrokImagineGenerator GrokImagine_Wide() => + new GrokImagineGenerator(_settings.XAIGrokApiKey, _concurrency, + ImageGeneratorApiType.GrokImagine, _stats, "", + aspectRatio: "16:9", quality: "high", resolution: "2k", settings: _settings); + + public GrokImagineGenerator GrokImagine_Portrait() => + new GrokImagineGenerator(_settings.XAIGrokApiKey, _concurrency, + ImageGeneratorApiType.GrokImagine, _stats, "", + aspectRatio: "3:4", quality: "high", resolution: "2k", settings: _settings); + + public GrokImagineGenerator GrokImaginePro_Square() => + new GrokImagineGenerator(_settings.XAIGrokApiKey, _concurrency, + ImageGeneratorApiType.GrokImaginePro, _stats, "", + aspectRatio: "1:1", quality: "high", resolution: "2k", settings: _settings); + + public GrokImagineGenerator GrokImaginePro_Portrait() => + new GrokImagineGenerator(_settings.XAIGrokApiKey, _concurrency, + ImageGeneratorApiType.GrokImaginePro, _stats, "", + aspectRatio: "3:4", quality: "high", resolution: "2k", settings: _settings); + + // ---------- xAI Grok Imagine VIDEO ---------- + // + // grok-imagine-video via the async /v1/videos/generations endpoint + // (start + poll). Duration 1-15s; resolution 480p/720p/1080p; per- + // second billing (roughly $0.05/s at 480p). The generator downloads + // the mp4 itself into {day}\Video\ and contributes a PNG "video card" + // to combined grids, so it multiplexes alongside image generators. + + public GrokImagineVideoGenerator GrokVideo_Wide() => + new GrokImagineVideoGenerator(_settings.XAIGrokApiKey, _concurrency, + _stats, _settings, "", + aspectRatio: "16:9", resolution: "480p", durationSeconds: 6); + + public GrokImagineVideoGenerator GrokVideo_Wide720p() => + new GrokImagineVideoGenerator(_settings.XAIGrokApiKey, _concurrency, + _stats, _settings, "", + aspectRatio: "16:9", resolution: "720p", durationSeconds: 8); + + // ---------- Google ---------- + // + // June 2026: ALL dedicated Imagen endpoints shut down 06-24..30. + // Gemini image models ("Nano Banana") are the replacement family: + // gemini-3.1-flash-image (Nano Banana 2) — fast/cheap tier + // gemini-3-pro-image (Nano Banana Pro) — reasoning tier, 4K-capable + // Both take an optional aspectRatio ("1:1","2:3","3:2","3:4","4:3", + // "4:5","5:4","9:16","16:9","21:9") and imageSize ("512" flash-only, + // "1K","2K","4K" — uppercase K). 2K costs the same tokens as 1K, so + // it's the default here; only 4K is pricier. + // These need only GoogleGeminiApiKey (AI Studio key) — no Vertex + // project/service-account setup like the dead Imagen path required. + + private GoogleGenerator GeminiNanoBanana() => + new GoogleGenerator(ImageGeneratorApiType.GoogleNanoBanana, + _settings.GoogleGeminiApiKey, _concurrency, _stats, + aspectRatio: "1:1", imageSize: "2K"); + + private GoogleGenerator GeminiNanoBananaPro() => + new GoogleGenerator(ImageGeneratorApiType.GoogleNanoBananaPro, + _settings.GoogleGeminiApiKey, _concurrency, _stats, + aspectRatio: "1:1", imageSize: "2K"); + + // Direct replacement for the old GoogleImagen4_2_5() slot: tall + // portrait output on the cheap flash tier. Imagen's "2:5" has no + // exact Gemini equivalent; 9:16 is the closest supported ratio. + private GoogleGenerator GeminiNanoBananaTallPortrait() => + new GoogleGenerator(ImageGeneratorApiType.GoogleNanoBanana, + _settings.GoogleGeminiApiKey, _concurrency, _stats, + aspectRatio: "9:16", imageSize: "2K"); + + private GoogleGenerator GeminiNanoBananaPro4K() => + new GoogleGenerator(ImageGeneratorApiType.GoogleNanoBananaPro, + _settings.GoogleGeminiApiKey, _concurrency, _stats, + aspectRatio: "1:1", imageSize: "4K"); + +#pragma warning disable CS0618 // kept until the Imagen shutdown actually lands + private GoogleImagen4Generator GoogleImagen4_2_5() => + new GoogleImagen4Generator(_settings.GoogleGeminiApiKey, _concurrency, + _stats, "", "2:5", "BLOCK_NONE", + location: _settings.GoogleCloudLocation, + projectId: _settings.GoogleCloudProjectId, + googleServiceAccountKeyPath: _settings.GoogleServiceAccountKeyPath); +#pragma warning restore CS0618 + } +} diff --git a/MultiImageClient/Workflows/GrokShowcaseWorkflow.cs b/MultiImageClient/Workflows/GrokShowcaseWorkflow.cs new file mode 100644 index 0000000..caf8cb0 --- /dev/null +++ b/MultiImageClient/Workflows/GrokShowcaseWorkflow.cs @@ -0,0 +1,143 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + /// One-shot "showcase" runner: fires N prompts at a single image generator + /// (xAI Grok Imagine) in parallel, saves each result via the normal + /// ImageManager pipeline, then composes ALL results into a single combined + /// grid image (one cell per prompt) and pops that combined image open in + /// the default viewer. + /// + /// Used for provider-acceptance smoke tests — e.g. "take the first N + /// prompts from the current prompt source, send each one through Grok, + /// give me one image to eyeball". + /// + /// Prompts come from the caller-provided AbstractPromptSource (just like + /// BatchWorkflow) — no prompts are hardcoded here. + public class GrokShowcaseWorkflow + { + /// If true, routes through grok-imagine-image-pro + /// ($0.07/img, 30 rpm) at 2k resolution. Otherwise uses + /// grok-imagine-image ($0.02/img, 300 rpm) at 1k. + /// Max number of prompts from + /// to run. Defaults to 10, which fits comfortably in the combined + /// grid and costs $0.20 on the cheap tier. + public async Task RunAsync( + AbstractPromptSource promptSource, + Settings settings, + MultiClientRunStats stats, + bool pro = false, + int limit = 10) + { + if (string.IsNullOrWhiteSpace(settings.XAIGrokApiKey)) + { + Console.Error.WriteLine("Grok showcase aborted: settings.json is missing XAIGrokApiKey."); + return null; + } + + var prompts = promptSource.Prompts + .Select(p => p.Prompt) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Take(Math.Max(0, limit)) + .ToList(); + + if (prompts.Count == 0) + { + Console.Error.WriteLine( + "Grok showcase aborted: prompt source produced no prompts. " + + "Supply --prompt \"...\" or point PromptFiles at a readable file."); + return null; + } + + var concurrency = pro ? 2 : 5; + var apiType = pro ? ImageGeneratorApiType.GrokImaginePro : ImageGeneratorApiType.GrokImagine; + + var generator = new GrokImagineGenerator( + settings.XAIGrokApiKey, + concurrency, + apiType, + stats, + name: "", + aspectRatio: "1:1", + quality: "high", + resolution: "2k", + settings: settings); + + var imageManager = new ImageManager(settings, stats); + var modelLabel = pro ? "grok-imagine-image-pro" : "grok-imagine-image"; + Logger.Log($"Grok showcase: firing {prompts.Count} prompts at {modelLabel} (concurrency={concurrency})."); + + var tasks = prompts.Select(async promptText => + { + var pd = new PromptDetails(); + pd.ReplacePrompt(promptText, promptText, TransformationType.InitialPrompt); + try + { + var result = await generator.ProcessPromptAsync(generator, pd); + await imageManager.ProcessAndSaveAsync(result, generator); + var label = result.IsSuccess ? "OK" : $"FAIL ({result.ErrorMessage})"; + Logger.Log($"Grok showcase :: {label} :: {Trim(promptText, 80)}"); + return result; + } + catch (Exception ex) + { + Logger.Log($"Grok showcase :: EXCEPTION on prompt '{Trim(promptText, 80)}': {ex.Message}"); + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = ex.Message, + PromptDetails = pd, + ImageGenerator = apiType, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + }; + } + }).ToArray(); + + stats.PrintStats(); + var results = await Task.WhenAll(tasks); + stats.PrintStats(); + + var okCount = results.Count(r => r.IsSuccess); + Logger.Log($"Grok showcase: {okCount}/{results.Length} succeeded."); + + // Build a single combined grid image that contains EVERY prompt's + // result (not one-grid-per-prompt like BatchWorkflow). The prompt + // panel underneath lists the sub-prompts so the popped-open image + // is self-documenting. + var combinedPromptRecap = BuildRecapString(prompts, modelLabel); + + try + { + var outPath = await ImageCombiner.CreateBatchLayoutImageSquareAsync( + results, + combinedPromptRecap, + settings, + openWhenDone: true); + Logger.Log($"Grok showcase combined image: {outPath}"); + return outPath; + } + catch (Exception ex) + { + Logger.Log($"Grok showcase: failed to build combined image: {ex.Message}"); + return null; + } + } + + private static string BuildRecapString(IReadOnlyList prompts, string modelLabel) + { + var header = $"Grok showcase ({modelLabel}) - {prompts.Count} prompts:"; + var lines = prompts.Select((p, i) => $"{i + 1}. {Trim(p, 180)}"); + return header + "\n\n" + string.Join("\n", lines); + } + + private static string Trim(string s, int max) + { + if (string.IsNullOrEmpty(s)) return s ?? string.Empty; + return s.Length <= max ? s : s.Substring(0, max - 1) + "..."; + } + } +} diff --git a/MultiImageClient/Workflows/GrokVideoModesWorkflow.cs b/MultiImageClient/Workflows/GrokVideoModesWorkflow.cs new file mode 100644 index 0000000..c0ce3d0 --- /dev/null +++ b/MultiImageClient/Workflows/GrokVideoModesWorkflow.cs @@ -0,0 +1,247 @@ +#nullable enable +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +using XAIGrokAPIClient; + +namespace MultiImageClient +{ + /// One-shot exerciser for the three Grok video request modes + /// (--grok-video-test): + /// + /// 1. text-to-video — prompt alone. + /// 2. image-to-video — generate a still with grok-imagine-image first, + /// then animate it (the image becomes frame one). + /// 3. extend-video — POST /v1/videos/extensions on the clip from + /// step 2; xAI returns original + extension as one + /// combined mp4. + /// + /// Every clip is requested with storage_options (durable Files-API copy, + /// so --grok-sync can always recover it), saved under the day folder's + /// Video\ subfolder, mirrored, and recorded in grok_ledger.jsonl. + /// Videos are deliberately short (3s) and 480p to keep the test cheap + /// (~$0.15 per clip + $0.02 for the still). + public class GrokVideoModesWorkflow + { + private const int DurationSeconds = 3; + private const string Resolution = "480p"; + private const string AspectRatio = "16:9"; + + public async Task RunAsync(AbstractPromptSource promptSource, Settings settings) + { + if (string.IsNullOrWhiteSpace(settings.XAIGrokApiKey)) + { + Logger.Log("Grok video test: settings.json has no XAIGrokApiKey; aborting."); + return; + } + + var prompt = promptSource.Prompts + .Select(p => p.Prompt) + .FirstOrDefault(s => !string.IsNullOrWhiteSpace(s)) + ?? "A red kite surfing wind gusts above a green coastal cliff, bright daylight"; + + var client = new XAIGrokClient(settings.XAIGrokApiKey); + using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(5) }; + + Logger.Log($"Grok video test: 3 modes x {DurationSeconds}s {Resolution} {AspectRatio}"); + Logger.Log($" prompt: {prompt}"); + + // ---- Mode 1: text -> video -------------------------------- + var textVideo = await RunOneAsync(settings, client, http, "text2video", new XAIGrokVideoGenerateRequest + { + Prompt = prompt, + Model = XAIGrokClient.ModelGrokImagineVideo, + Duration = DurationSeconds, + AspectRatio = AspectRatio, + Resolution = Resolution, + StorageOptions = MakeStorage("text2video", prompt), + }, prompt); + + // ---- Mode 2: grok image -> video -------------------------- + string? imageUrl = null; + try + { + Logger.Log($"Grok video test: generating source still via {XAIGrokClient.ModelGrokImagine}..."); + var imgResponse = await client.GenerateAsync(new XAIGrokGenerateRequest + { + Prompt = prompt, + Model = XAIGrokClient.ModelGrokImagine, + AspectRatio = AspectRatio, + Quality = "high", + Resolution = "1k", + N = 1, + }); + imageUrl = imgResponse.Data?.FirstOrDefault()?.Url; + if (!string.IsNullOrEmpty(imageUrl)) + { + var imgBytes = await http.GetByteArrayAsync(imageUrl); + var imgPath = BuildOutputPath(settings, "img2video_source", prompt, ".png"); + await File.WriteAllBytesAsync(imgPath, imgBytes); + DlMirror.Copy(imgPath, settings.FlatImageMirrorPath); + Logger.Log($"Grok video test: source still saved -> {imgPath}"); + GrokLedger.Append(settings, new GrokLedgerEntry + { + Kind = "image", + Model = XAIGrokClient.ModelGrokImagine, + Prompt = prompt, + RemoteUrl = imageUrl, + LocalPath = imgPath, + Bytes = imgBytes.Length, + Source = "live", + }); + } + } + catch (Exception ex) + { + Logger.Log($"Grok video test: source image generation failed: {ex.Message}"); + } + + XAIGrokVideoResult? imageVideo = null; + if (!string.IsNullOrEmpty(imageUrl)) + { + imageVideo = await RunOneAsync(settings, client, http, "img2video", new XAIGrokVideoGenerateRequest + { + Prompt = $"Animate this scene naturally: {prompt}", + Model = XAIGrokClient.ModelGrokImagineVideo, + Duration = DurationSeconds, + Resolution = Resolution, + // No AspectRatio: image-to-video inherits the still's AR; + // overriding it would stretch the source. + Image = new XAIGrokVideoImageInput { Url = imageUrl }, + StorageOptions = MakeStorage("img2video", prompt), + }, prompt); + } + else + { + Logger.Log("Grok video test: SKIPPING image-to-video (no source image)."); + } + + // ---- Mode 3: extend the image-video (fall back to the text one) ---- + var baseClip = imageVideo ?? textVideo; + if (baseClip?.Video != null) + { + var extendPrompt = "Continue the scene: the camera slowly pulls back to reveal the wider surroundings"; + var videoInput = !string.IsNullOrEmpty(baseClip.Video.FileOutput?.FileId) + ? new XAIGrokVideoVideoInput { FileId = baseClip.Video.FileOutput.FileId } + : new XAIGrokVideoVideoInput { Url = baseClip.Video.Url }; + try + { + Logger.Log($"Grok video test: extend-video (+{DurationSeconds}s) using " + + (videoInput.FileId != null ? $"file_id {videoInput.FileId}" : "temp url")); + var extended = await client.ExtendVideoAsync(new XAIGrokVideoExtendRequest + { + Prompt = extendPrompt, + Model = XAIGrokClient.ModelGrokImagineVideo, + Video = videoInput, + Duration = DurationSeconds, + Resolution = Resolution, + StorageOptions = MakeStorage("extended", prompt), + }); + await SaveResultAsync(settings, client, http, "extended", extended, extendPrompt); + } + catch (Exception ex) + { + Logger.Log($"Grok video test: extend-video FAILED: {ex.Message}"); + } + } + else + { + Logger.Log("Grok video test: SKIPPING extend-video (no base clip succeeded)."); + } + + Logger.Log("Grok video test: done. All clips are in the day folder's Video\\ subfolder, " + + "stored durably at xAI (Files API), and recorded in grok_ledger.jsonl."); + } + + private async Task RunOneAsync( + Settings settings, + XAIGrokClient client, + HttpClient http, + string modeTag, + XAIGrokVideoGenerateRequest request, + string promptForLedger) + { + try + { + Logger.Log($"Grok video test: {modeTag} starting..."); + var result = await client.GenerateVideoAsync(request, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(10)); + return await SaveResultAsync(settings, client, http, modeTag, result, promptForLedger); + } + catch (Exception ex) + { + Logger.Log($"Grok video test: {modeTag} FAILED: {ex.Message}"); + return null; + } + } + + /// Downloads the finished clip (temp URL preferred, Files-API copy as + /// fallback), saves + mirrors + ledgers it. Returns the result when + /// the clip was retrievable, else null. + private async Task SaveResultAsync( + Settings settings, + XAIGrokClient client, + HttpClient http, + string modeTag, + XAIGrokVideoResult result, + string promptForLedger) + { + var hasUrl = !string.IsNullOrEmpty(result.Video?.Url); + var hasFile = !string.IsNullOrEmpty(result.Video?.FileOutput?.FileId); + if (!result.IsDone || (!hasUrl && !hasFile)) + { + var detail = result.Error != null ? $"{result.Error.Code}: {result.Error.Message}" : $"status={result.Status}"; + Logger.Log($"Grok video test: {modeTag} did not complete ({detail})."); + return null; + } + + var bytes = hasUrl + ? await http.GetByteArrayAsync(result.Video!.Url) + : await client.DownloadFileContentAsync(result.Video!.FileOutput!.FileId); + + var path = BuildOutputPath(settings, modeTag, promptForLedger, ".mp4"); + await File.WriteAllBytesAsync(path, bytes); + DlMirror.Copy(path, settings.FlatImageMirrorPath); + Logger.Log($"Grok video test: {modeTag} OK — {result.Video.Duration}s, {bytes.Length / 1024} KB -> {path}"); + + GrokLedger.Append(settings, new GrokLedgerEntry + { + Kind = "video", + Model = XAIGrokClient.ModelGrokImagineVideo, + Prompt = promptForLedger, + RequestId = result.RequestId, + FileId = result.Video.FileOutput?.FileId, + RemoteUrl = result.Video.Url, + LocalPath = path, + Bytes = bytes.Length, + Source = "live", + }); + return result; + } + + private static XAIGrokVideoStorageOptions MakeStorage(string modeTag, string prompt) + => new XAIGrokVideoStorageOptions + { + Filename = FilenameGenerator.SanitizeFilename( + $"{DateTime.UtcNow:yyyyMMddHHmmss}_grokvidtest_{modeTag}_{FilenameGenerator.TruncatePrompt(prompt, 60)}") + ".mp4", + }; + + private static string BuildOutputPath(Settings settings, string modeTag, string prompt, string extension) + { + var folder = Path.Combine(settings.ImageDownloadBaseFolder, DateTime.Now.ToString("yyyy-MM-dd-dddd"), "Video"); + Directory.CreateDirectory(folder); + var stem = FilenameGenerator.SanitizeFilename( + $"{DateTime.Now:yyyyMMddHHmmss}_grokvidtest_{modeTag}_{FilenameGenerator.TruncatePrompt(prompt, 70)}"); + var path = Path.Combine(folder, stem + extension); + int count = 1; + while (File.Exists(path)) + { + path = Path.Combine(folder, $"{stem}_{count:D4}{extension}"); + count++; + } + return path; + } + } +} diff --git a/MultiImageClient/Workflows/ReplWorkflow.cs b/MultiImageClient/Workflows/ReplWorkflow.cs new file mode 100644 index 0000000..d7d9ce6 --- /dev/null +++ b/MultiImageClient/Workflows/ReplWorkflow.cs @@ -0,0 +1,967 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using BFLAPIClient; +using IdeogramAPIClient; +using OpenAI.Images; +using RecraftAPIClient; + +namespace MultiImageClient +{ + /// Interactive prompt-by-prompt REPL with asynchronous dispatch. + /// + /// Each non-empty stdin line is either: + /// - a `:command` (size / quality / gens / status / wait / edit / retry / quit / ...) + /// - a prompt, which is fired off as a Task against the current active + /// generator set and returned to the caller immediately so they can + /// keep typing. + /// + /// Up to prompts are in flight at + /// once; extra prompts queue on a . Results are + /// saved to disk as they arrive via the existing + /// pipeline (so filenames, the Label-annotated copy, FlatImageMirror copy, + /// and json logs all behave exactly as in Batch). Unlike Batch, no + /// per-prompt combined-grid image is popped open — the grid is still + /// built and saved, but the viewer launch is suppressed. + /// + /// Session state the user can mutate at runtime: + /// - gpt-image-2 size / quality / moderation (rebuilds the gpt2 slot) + /// - the active generator set (add / remove by name from a fixed catalog) + /// - concurrency limit (takes effect for subsequent dispatches) + /// + /// Design notes: + /// - The `_active` dictionary is snapshotted into a List at dispatch time + /// so mutations after a prompt has been submitted never affect an + /// in-flight job. + /// - Per-prompt overrides like `[size=1024x1024,q=low] ` are + /// parsed off the front of the line; unrecognized keys cause the line + /// to be treated as a plain prompt. + public class ReplWorkflow + { + private readonly Settings _settings; + private readonly MultiClientRunStats _stats; + private readonly ImageManager _imageManager; + + private readonly object _lock = new object(); + private int _nextId = 0; + private readonly List _inFlight = new List(); + private string _lastPrompt; + + // Lazily loaded pool of prompts read from Settings.PromptFiles / + // LoadPromptsFrom, used by the `:random` command. Null until the + // first :random call; a null-but-asked result means "no files / + // no lines" and is surfaced to the user. + private List _promptPool; + + // Session defaults for the gpt-image-2 slot. Editing any of these + // rebuilds the "gpt2" entry in _active (if present). + private string _size; + private string _quality; + private string _moderation; + private int _imageCount; + private int _concurrency; + private SemaphoreSlim _concurrencyLimit; + + // Active generator set, keyed by short name (e.g. "gpt2", "ideogram"). + // Order is preserved for deterministic grid layout. + private readonly Dictionary _active = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Catalog of known per-name factories. ':gens add ' looks here. + // Keep in sync with PrintHelp()'s generator list. + private static readonly string[] KnownGenerators = + { "gpt2", "grok", "grokpro", "dalle3", "ideogram", "recraft", "bfl", "flux2local", "seedream", "hailuo", "krea", "bria", "magnific", "luma", "runway", "stability", "google", "googlepro", "imagen4" }; + + private class InFlight + { + public int Id; + public string Prompt; + public DateTime StartedAt; + public Task Task; + } + + public ReplWorkflow(Settings settings, MultiClientRunStats stats, RunOptions options) + { + _settings = settings; + _stats = stats; + _imageManager = new ImageManager(settings, stats); + + // Preflight the REPL default size so bad --repl-size values get + // caught at startup instead of after we've built a generator that + // will fail on every call. + var startSize = string.IsNullOrWhiteSpace(options.ReplSize) ? "2048x2048" : options.ReplSize; + _size = NormalizeSizeOrFallback(startSize, "2048x2048", "--repl-size"); + _quality = string.IsNullOrWhiteSpace(options.ReplQuality) ? "high" : options.ReplQuality; + _moderation = string.IsNullOrWhiteSpace(options.ReplModeration) ? "low" : options.ReplModeration; + _imageCount = options.ReplImageCount < 1 ? 1 : options.ReplImageCount; + _concurrency = options.ReplConcurrency < 1 ? 1 : options.ReplConcurrency; + _concurrencyLimit = new SemaphoreSlim(_concurrency); + + // Default active set: gpt-image-2 + Grok Imagine (standard tier). + // Both fire in parallel for each dispatched prompt. Grok is only + // added if an API key is present so the REPL still works on + // gpt-only setups. Users can :gens add / remove at runtime. + _active["gpt2"] = BuildGpt2(_size, _quality, _moderation, _imageCount); + if (!string.IsNullOrWhiteSpace(_settings.XAIGrokApiKey)) + { + _active["grok"] = BuildNamed("grok"); + } + } + + public async Task RunAsync() + { + PrintBanner(); + PrintHelp(); + PrintShow(); + Console.WriteLine(); + + while (true) + { + var line = Console.ReadLine(); + if (line is null) + { + Console.WriteLine("(stdin closed; waiting for in-flight jobs then exiting)"); + break; + } + line = line.Trim(); + if (line.Length == 0) continue; + + if (line.StartsWith(":")) + { + var exit = await HandleCommandAsync(line); + if (exit) break; + continue; + } + + // Bare exit aliases (no leading colon). These win over the + // short-prompt guard so users don't get prompted to confirm + // that yes, they really did mean to quit. + var lower = line.ToLowerInvariant(); + if (lower == "q" || lower == "x" || lower == "exit" || lower == "quit") + { + break; + } + + // Typing `foo` or `bug` is almost always a mistake — a real + // prompt is meaningfully longer. Treat anything under 5 chars + // (after stripping any `[size=..]`-style override prefix) as + // suspicious and require explicit confirmation before burning + // an API call on it. + var (_, _, _, promptOnly) = ParseOverrides(line); + if ((promptOnly ?? "").Trim().Length < 5) + { + Console.Write($"short prompt ({(promptOnly ?? "").Trim().Length} chars): '{line}'. Send anyway? [y/N] "); + var confirm = Console.ReadLine(); + if (confirm == null) break; + var c = confirm.Trim().ToLowerInvariant(); + if (c != "y" && c != "yes") + { + Console.WriteLine("(skipped)"); + continue; + } + } + + DispatchPrompt(line); + } + + await WaitAllAsync(); + Console.WriteLine("REPL finished."); + } + + // --------------------------------------------------------------- + // Command handling + // --------------------------------------------------------------- + + // Returns true if the REPL should exit after this command. + private async Task HandleCommandAsync(string line) + { + var parts = line.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); + var cmd = parts[0].TrimStart(':').ToLowerInvariant(); + var arg = parts.Length > 1 ? parts[1].Trim() : ""; + + switch (cmd) + { + case "help": + case "h": + case "?": + PrintHelp(); + return false; + + case "show": + PrintShow(); + return false; + + case "size": + if (string.IsNullOrEmpty(arg)) { Console.WriteLine($"size = {_size}"); return false; } + if (!GptImage2Generator.TryNormalizeSize(arg, out var sized, out var sizeNote, out var sizeErr)) + { + Console.WriteLine($"(size rejected: {sizeErr}. kept {_size})"); + return false; + } + if (sizeNote != null) Console.WriteLine($"(size {sizeNote})"); + _size = sized; + RebuildGpt2IfActive(); + Console.WriteLine($"size = {_size}"); + return false; + + case "quality": + case "q": + if (string.IsNullOrEmpty(arg)) { Console.WriteLine($"quality = {_quality}"); return false; } + _quality = arg; + RebuildGpt2IfActive(); + Console.WriteLine($"quality = {_quality}"); + return false; + + case "moderation": + case "mod": + if (string.IsNullOrEmpty(arg)) { Console.WriteLine($"moderation = {_moderation}"); return false; } + _moderation = arg; + RebuildGpt2IfActive(); + Console.WriteLine($"moderation = {_moderation}"); + return false; + + case "n": + if (string.IsNullOrEmpty(arg)) { Console.WriteLine($"n = {_imageCount}"); return false; } + if (!int.TryParse(arg, out var nval) || nval < 1) + { + Console.WriteLine($"(n rejected: '{arg}' — must be a positive integer. kept n={_imageCount})"); + return false; + } + if (nval > 10) + { + // No hard server cap is documented but N>10 is almost + // always a typo and multiplies cost + partials storm + // linearly. Ask once before committing. + Console.Write($"n={nval} is large; each dispatch will cost {nval}x per image. Confirm? [y/N] "); + var cc = Console.ReadLine(); + if (cc == null) return false; + var ck = cc.Trim().ToLowerInvariant(); + if (ck != "y" && ck != "yes") { Console.WriteLine($"(cancelled, kept n={_imageCount})"); return false; } + } + _imageCount = nval; + RebuildGpt2IfActive(); + Console.WriteLine($"n = {_imageCount}"); + return false; + + case "concurrency": + if (int.TryParse(arg, out var n) && n >= 1) + { + _concurrency = n; + // SemaphoreSlim isn't resizable; swap in a fresh one. + // In-flight tasks still hold permits on the old + // instance — that's fine, they'll release on the old + // semaphore which then gets garbage-collected. Only + // subsequent dispatches see the new limit. + _concurrencyLimit = new SemaphoreSlim(_concurrency); + Console.WriteLine($"concurrency = {_concurrency}"); + } + else Console.WriteLine("usage: :concurrency N (N >= 1)"); + return false; + + case "gens": + HandleGensCommand(arg); + return false; + + case "status": + PrintStatus(); + return false; + + case "wait": + await WaitAllAsync(); + return false; + + case "last": + Console.WriteLine(string.IsNullOrEmpty(_lastPrompt) ? "(no prior prompt)" : _lastPrompt); + return false; + + case "retry": + if (string.IsNullOrEmpty(_lastPrompt)) + { + Console.WriteLine("(no prior prompt)"); + } + else + { + DispatchPrompt(_lastPrompt); + } + return false; + + case "random": + case "r": + HandleRandomPrompt(); + return false; + + case "edit": + if (string.IsNullOrEmpty(_lastPrompt)) + { + Console.WriteLine("(no prior prompt to edit)"); + return false; + } + Console.WriteLine($"last: {_lastPrompt}"); + Console.Write("edit (blank = reuse, q = cancel): "); + var newLine = Console.ReadLine(); + if (newLine is null) return false; + var trimmed = newLine.Trim(); + if (trimmed == "q") { Console.WriteLine("(cancelled)"); return false; } + DispatchPrompt(string.IsNullOrEmpty(trimmed) ? _lastPrompt : trimmed); + return false; + + case "quit": + case "exit": + return true; + + default: + Console.WriteLine($"unknown command ':{cmd}'. Try :help"); + return false; + } + } + + private void HandleGensCommand(string arg) + { + var parts = arg.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); + var sub = parts.Length > 0 ? parts[0].ToLowerInvariant() : "list"; + var rest = parts.Length > 1 ? parts[1].Trim() : ""; + + switch (sub) + { + case "list": + if (_active.Count == 0) { Console.WriteLine("(no active generators)"); return; } + foreach (var kv in _active) + { + Console.WriteLine($" {kv.Key}: {kv.Value.GetGeneratorSpecPart()}"); + } + return; + + case "add": + if (string.IsNullOrEmpty(rest)) + { + Console.WriteLine($"usage: :gens add . Known: {string.Join(", ", KnownGenerators)}"); + return; + } + try + { + var key = rest.ToLowerInvariant(); + var g = BuildNamed(key); + _active[key] = g; + Console.WriteLine($"added {key}: {g.GetGeneratorSpecPart()}"); + } + catch (Exception ex) + { + Console.WriteLine($"add failed: {ex.Message}"); + } + return; + + case "remove": + case "rm": + if (string.IsNullOrEmpty(rest)) { Console.WriteLine("usage: :gens remove "); return; } + if (_active.Remove(rest.ToLowerInvariant())) Console.WriteLine($"removed {rest}"); + else Console.WriteLine($"not active: {rest}"); + return; + + case "reset": + _active.Clear(); + _active["gpt2"] = BuildGpt2(_size, _quality, _moderation, _imageCount); + if (!string.IsNullOrWhiteSpace(_settings.XAIGrokApiKey)) + { + _active["grok"] = BuildNamed("grok"); + } + Console.WriteLine($"reset to defaults ({string.Join(" + ", _active.Keys)})"); + return; + + default: + Console.WriteLine($"usage: :gens [list | add | remove | reset] names: {string.Join(", ", KnownGenerators)}"); + return; + } + } + + // --------------------------------------------------------------- + // :random — pick a prompt from the configured PromptFiles and + // prefill it on the input line so the user can tweak it in place + // or just hit Enter to accept. + // --------------------------------------------------------------- + + private void HandleRandomPrompt() + { + var pool = LoadPromptPool(); + if (pool == null || pool.Count == 0) + { + Console.WriteLine("(no prompts found — set Settings.PromptFiles to point at a .txt file with one prompt per line)"); + return; + } + + // Keep rolling new picks until the user picks an action. Ctrl-C + // is reserved for "abort the whole program" — it shouldn't be + // the UX for rejecting a suggestion. + while (true) + { + var pick = pool[Random.Shared.Next(pool.Count)]; + Console.WriteLine(); + Console.WriteLine($"(random from {pool.Count}):"); + Console.WriteLine($" {pick}"); + Console.Write("[y]es send [n]o skip [r]eroll [e]dit: "); + + var resp = Console.ReadLine(); + if (resp is null) return; + var c = resp.Trim().ToLowerInvariant(); + + // Treat bare Enter as "reroll" — it's the lowest-commitment + // action and matches how fast you can just keep hitting + // Enter to flip through suggestions. + if (c.Length == 0 || c == "r" || c == "reroll") + { + continue; + } + if (c == "y" || c == "yes") + { + DispatchPrompt(pick); + return; + } + if (c == "n" || c == "no" || c == "skip") + { + Console.WriteLine("(skipped)"); + return; + } + if (c == "e" || c == "edit") + { + Console.Write("> "); + // Prefill the candidate so it appears on the next input + // line as if typed, letting the user tweak it in place. + // If the injection fails (redirected stdin etc.) just + // print it above the prompt and ask them to retype. + if (!ConsolePrefill.TryPrefill(pick)) + { + Console.WriteLine(); + Console.WriteLine(pick); + Console.Write("> "); + } + var edited = Console.ReadLine(); + if (edited is null) return; + edited = edited.Trim(); + if (edited.Length == 0) + { + Console.WriteLine("(empty — skipping)"); + return; + } + DispatchPrompt(edited); + return; + } + + Console.WriteLine($"(unrecognized '{resp}' — expected y/n/r/e)"); + } + } + + // Reads Settings.PromptFiles (+ legacy LoadPromptsFrom) on first use, + // pooling all non-blank lines. Caches for the life of the REPL; + // restart the session to pick up edits. + private List LoadPromptPool() + { + if (_promptPool != null) return _promptPool; + + var files = new List(); + if (_settings.PromptFiles != null) + { + files.AddRange(_settings.PromptFiles.Where(p => !string.IsNullOrWhiteSpace(p))); + } + if (!string.IsNullOrWhiteSpace(_settings.LoadPromptsFrom)) + { + files.Add(_settings.LoadPromptsFrom); + } + + var lines = new List(); + foreach (var fp in files) + { + if (!File.Exists(fp)) + { + Console.WriteLine($"(prompt file missing, skipping: {fp})"); + continue; + } + foreach (var raw in File.ReadAllLines(fp)) + { + var t = raw?.Trim(); + if (!string.IsNullOrEmpty(t)) lines.Add(t); + } + } + + _promptPool = lines; + if (lines.Count > 0) + { + Console.WriteLine($"(loaded {lines.Count} prompts from {files.Count} file(s))"); + } + return _promptPool; + } + + // --------------------------------------------------------------- + // Dispatch + // --------------------------------------------------------------- + + private void DispatchPrompt(string rawLine) + { + if (_active.Count == 0) + { + Console.WriteLine("no generators active. Use ':gens add ' or ':gens reset'."); + return; + } + + // Optional leading override: "[size=1024x1024,q=low,n=4] actual prompt" + var (overrideSize, overrideQuality, overrideN, promptText) = ParseOverrides(rawLine); + if (string.IsNullOrWhiteSpace(promptText)) + { + Console.WriteLine("(empty prompt after parsing overrides, skipping)"); + return; + } + + // Validate per-prompt size override up-front so a typo like + // "[size=1526x2048]" doesn't burn an API round-trip before the + // server rejects it. Auto-snap near-miss multiples; reject the + // rest with the session default kept intact. + if (overrideSize != null) + { + if (!GptImage2Generator.TryNormalizeSize(overrideSize, out var normOvr, out var noteOvr, out var errOvr)) + { + Console.WriteLine($"(override size rejected: {errOvr}. not dispatching)"); + return; + } + if (noteOvr != null) Console.WriteLine($"(override size {noteOvr})"); + overrideSize = normOvr; + } + + _lastPrompt = promptText; + + // Snapshot the active set so :gens mutations after dispatch don't + // affect this job. If there's a per-call override AND gpt2 is + // active, swap in a one-off gpt2 for this dispatch only. + var gens = _active.ToList(); + if ((overrideSize != null || overrideQuality != null || overrideN != null) + && _active.ContainsKey("gpt2")) + { + var s = overrideSize ?? _size; + var q = overrideQuality ?? _quality; + var nn = overrideN ?? _imageCount; + for (int i = 0; i < gens.Count; i++) + { + if (gens[i].Key.Equals("gpt2", StringComparison.OrdinalIgnoreCase)) + { + gens[i] = new KeyValuePair("gpt2", BuildGpt2(s, q, _moderation, nn)); + } + } + } + + int id; + lock (_lock) id = ++_nextId; + + var inflight = new InFlight + { + Id = id, + Prompt = promptText, + StartedAt = DateTime.Now, + }; + inflight.Task = Task.Run(() => ProcessOneAsync(inflight, gens.Select(kv => kv.Value).ToList())); + lock (_lock) _inFlight.Add(inflight); + + var overrideNote = (overrideSize != null || overrideQuality != null || overrideN != null) + ? $" override: size={overrideSize ?? "(sess)"} q={overrideQuality ?? "(sess)"} n={(overrideN?.ToString() ?? "(sess)")}" + : ""; + Logger.Log($"[#{id}] queued ({gens.Count} gen{(gens.Count == 1 ? "" : "s")}){overrideNote}: {promptText}"); + + // Reprint the command cheat-sheet after each dispatch so the + // available commands stay visible while long generations run — + // keeps the user from having to scroll up to remember flags. + PrintHelp(); + } + + private async Task ProcessOneAsync(InFlight inflight, List gens) + { + var id = inflight.Id; + var prompt = inflight.Prompt; + + // Grab a concurrency slot. Snapshot the semaphore reference so a + // runtime :concurrency swap doesn't cause us to Release() on a + // different instance than we WaitAsync'd on. + var limiter = _concurrencyLimit; + await limiter.WaitAsync(); + + try + { + Logger.Log($"[#{id}] START: {prompt} ({gens.Count} generator(s))"); + + var pd = new PromptDetails(); + pd.ReplacePrompt(prompt, prompt, TransformationType.InitialPrompt); + + var tasks = gens.Select(async g => + { + PromptDetails copy = null; + try + { + copy = pd.Copy(); + Logger.Log($"[#{id}] -> {g.GetGeneratorSpecPart()}"); + var r = await g.ProcessPromptAsync(g, copy); + await _imageManager.ProcessAndSaveAsync(r, g); + var status = r.IsSuccess ? "OK" : $"FAIL ({r.ErrorMessage})"; + Logger.Log($"[#{id}] <- {status} from {g.GetGeneratorSpecPart()} in {r.CreateTotalMs + r.DownloadTotalMs} ms"); + return r; + } + catch (Exception ex) + { + Logger.Log($"[#{id}] <- EXCEPTION from {g.GetGeneratorSpecPart()}: {ex.Message}"); + return new TaskProcessResult + { + IsSuccess = false, + ErrorMessage = ex.Message, + PromptDetails = copy ?? pd, + ImageGeneratorDescription = g.GetGeneratorSpecPart(), + }; + } + }).ToArray(); + + var results = await Task.WhenAll(tasks); + + // Build the grid without popping it in the viewer. The grid + // is still saved to disk (and mirrored) so REPL sessions can + // review final comparison images after the fact. + try + { + var combined = await ImageCombiner.CreateBatchLayoutImageSquareAsync( + results, prompt, _settings, openWhenDone: false); + Logger.Log($"[#{id}] DONE grid saved: {combined}"); + } + catch (Exception ex) + { + Logger.Log($"[#{id}] grid build failed: {ex.Message}"); + } + } + finally + { + limiter.Release(); + lock (_lock) _inFlight.Remove(inflight); + } + } + + private async Task WaitAllAsync() + { + Task[] snapshot; + lock (_lock) snapshot = _inFlight.Select(i => i.Task).ToArray(); + if (snapshot.Length == 0) + { + Console.WriteLine("(no jobs in flight)"); + return; + } + Console.WriteLine($"waiting for {snapshot.Length} job(s)..."); + try { await Task.WhenAll(snapshot); } + catch { /* each task already logs its own failures */ } + } + + // --------------------------------------------------------------- + // Builders + // --------------------------------------------------------------- + + // Reconstruct the gpt-image-2 slot in _active with the current + // session-level size / quality / moderation / n so the next dispatch + // picks them up. No-op if gpt2 isn't currently active. + private void RebuildGpt2IfActive() + { + if (_active.ContainsKey("gpt2")) + { + _active["gpt2"] = BuildGpt2(_size, _quality, _moderation, _imageCount); + } + } + + private IImageGenerator BuildGpt2(string size, string quality, string moderation, int imageCount) + { + if (!Enum.TryParse(quality, true, out var q)) + { + Console.WriteLine($"(unknown quality '{quality}', falling back to high)"); + q = OpenAIGPTImageOneQuality.high; + } + if (imageCount < 1) imageCount = 1; + // Pass maxConcurrency = _concurrency so gpt-image-2's own internal + // semaphore doesn't become the bottleneck when the REPL has + // multiple prompts in flight. The prompt-level semaphore + // (_concurrencyLimit) is still the authoritative cap. + // + // partialSaveFolder is set so streamed partial PNGs go to disk, + // but popUpPartials is deliberately false — REPL mode never + // pops anything in the viewer. + return new GptImage2Generator( + _settings.OpenAIApiKey, + maxConcurrency: _concurrency, + sizePool: new[] { size }, + moderation: moderation, + qualityPool: new[] { q }, + stats: _stats, + name: "repl", + partialSaveFolder: _settings.ImageDownloadBaseFolder, + popUpPartials: false, + imageCount: imageCount); + } + + private IImageGenerator BuildNamed(string name) + { + switch (name.ToLowerInvariant()) + { + case "gpt2": + return BuildGpt2(_size, _quality, _moderation, _imageCount); + + case "grok": + // Standard tier priced per-image regardless of resolution + // — 2k is a free upgrade over 1k so we take it. + RequireKey(_settings.XAIGrokApiKey, "XAIGrokApiKey", "grok"); + return new GrokImagineGenerator(_settings.XAIGrokApiKey, _concurrency, + ImageGeneratorApiType.GrokImagine, _stats, "repl", + aspectRatio: "1:1", quality: "high", resolution: "2k", settings: _settings); + + case "grokpro": + case "grok_pro": + RequireKey(_settings.XAIGrokApiKey, "XAIGrokApiKey", "grokpro"); + return new GrokImagineGenerator(_settings.XAIGrokApiKey, _concurrency, + ImageGeneratorApiType.GrokImaginePro, _stats, "repl", + aspectRatio: "1:1", quality: "high", resolution: "2k", settings: _settings); + + case "dalle3": + RequireKey(_settings.OpenAIApiKey, "OpenAIApiKey", "dalle3"); + return new Dalle3Generator(_settings.OpenAIApiKey, _concurrency, + GeneratedImageQuality.High, GeneratedImageSize.W1024xH1024, _stats, "repl"); + + case "ideogram": + RequireKey(_settings.IdeogramApiKey, "IdeogramApiKey", "ideogram"); + return new IdeogramV3Generator(_settings.IdeogramApiKey, _concurrency, + IdeogramV3StyleType.AUTO, IdeogramMagicPromptOption.ON, + IdeogramAspectRatio.ASPECT_16_10, IdeogramRenderingSpeed.QUALITY, + "", _stats, "repl"); + + case "recraft": + RequireKey(_settings.RecraftApiKey, "RecraftApiKey", "recraft"); + return new RecraftGenerator(_settings.RecraftApiKey, _concurrency, + RecraftImageSize._1365x1024, RecraftStyle.any, null, null, null, + _stats, "repl"); + + case "bfl": + RequireKey(_settings.BFLApiKey, "BFLApiKey", "bfl"); + return new BFLGenerator(ImageGeneratorApiType.BFLv11Ultra, + _settings.BFLApiKey, _concurrency, "1:1", false, 1024, 1024, + _stats, "repl"); + + case "flux2local": + case "localflux2": + case "flux2uncensored": + return new LocalFlux2ComfyGenerator(_settings, _concurrency, _stats, "repl-local-flux2"); + + case "seedream": + case "bytedance": + RequireKey(_settings.ByteDanceArkApiKey, "ByteDanceArkApiKey", "seedream"); + return new DirectImageApiGenerator(DirectImageProvider.ByteDanceSeedream, + _settings, _concurrency, _stats, "repl-seedream"); + + case "hailuo": + case "minimax": + RequireKey(_settings.MiniMaxApiKey, "MiniMaxApiKey", "hailuo"); + return new DirectImageApiGenerator(DirectImageProvider.MiniMaxHailuo, + _settings, _concurrency, _stats, "repl-hailuo"); + + case "krea": + RequireKey(_settings.KreaApiKey, "KreaApiKey", "krea"); + return new DirectImageApiGenerator(DirectImageProvider.Krea, + _settings, _concurrency, _stats, "repl-krea"); + + case "bria": + RequireKey(_settings.BriaApiKey, "BriaApiKey", "bria"); + return new DirectImageApiGenerator(DirectImageProvider.Bria, + _settings, _concurrency, _stats, "repl-bria"); + + case "magnific": + case "mystic": + RequireKey(_settings.MagnificApiKey, "MagnificApiKey", "magnific"); + return new DirectImageApiGenerator(DirectImageProvider.Magnific, + _settings, _concurrency, _stats, "repl-magnific"); + + case "luma": + case "photon": + RequireKey(_settings.LumaApiKey, "LumaApiKey", "luma"); + return new DirectImageApiGenerator(DirectImageProvider.LumaPhoton, + _settings, _concurrency, _stats, "repl-luma"); + + case "runway": + RequireKey(_settings.RunwayApiKey, "RunwayApiKey", "runway"); + return new DirectImageApiGenerator(DirectImageProvider.Runway, + _settings, _concurrency, _stats, "repl-runway"); + + case "stability": + case "stabilityai": + RequireKey(_settings.StabilityApiKey, "StabilityApiKey", "stability"); + return new DirectImageApiGenerator(DirectImageProvider.StabilityAi, + _settings, _concurrency, _stats, "repl-stability"); + + case "google": + case "nanobanana": + RequireKey(_settings.GoogleGeminiApiKey, "GoogleGeminiApiKey", "google"); + return new GoogleGenerator(ImageGeneratorApiType.GoogleNanoBanana, + _settings.GoogleGeminiApiKey, _concurrency, _stats, "repl", + aspectRatio: "1:1", imageSize: "2K"); + + case "googlepro": + case "nanobananapro": + RequireKey(_settings.GoogleGeminiApiKey, "GoogleGeminiApiKey", "googlepro"); + return new GoogleGenerator(ImageGeneratorApiType.GoogleNanoBananaPro, + _settings.GoogleGeminiApiKey, _concurrency, _stats, "repl", + aspectRatio: "1:1", imageSize: "2K"); + + case "imagen4": + // DEPRECATED: Imagen shuts down 2026-06-24..30; kept until then. + RequireKey(_settings.GoogleGeminiApiKey, "GoogleGeminiApiKey", "imagen4"); +#pragma warning disable CS0618 + return new GoogleImagen4Generator(_settings.GoogleGeminiApiKey, _concurrency, + _stats, "repl", "2:5", "BLOCK_NONE", + location: _settings.GoogleCloudLocation, + projectId: _settings.GoogleCloudProjectId, + googleServiceAccountKeyPath: _settings.GoogleServiceAccountKeyPath); +#pragma warning restore CS0618 + + default: + throw new ArgumentException( + $"unknown generator '{name}'. Known: {string.Join(", ", KnownGenerators)}"); + } + } + + private static void RequireKey(string value, string settingName, string genName) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidOperationException( + $"Settings.{settingName} is not set; cannot activate '{genName}'. Populate it in settings.json."); + } + } + + // --------------------------------------------------------------- + // Parsing: "[size=1024x1024,q=low] actual prompt" + // --------------------------------------------------------------- + + // Only applies the override to the gpt2 slot; other generators + // ignore per-call size/quality/n flags since they're parametrized + // at construction time. + // + // Returned `imageCount` is null when no `n=` was supplied (session + // default wins). Parse errors (non-integer n, n<1) cause the whole + // line to be treated as a plain prompt — strictly safer than + // silently dispatching with the session default when the user + // intended an override. + private static (string size, string quality, int? imageCount, string prompt) ParseOverrides(string line) + { + if (!line.StartsWith("[")) return (null, null, null, line); + var close = line.IndexOf(']'); + if (close < 0) return (null, null, null, line); + var inside = line.Substring(1, close - 1); + var rest = line.Substring(close + 1).TrimStart(); + + string size = null, quality = null; + int? imageCount = null; + foreach (var tok in inside.Split(',', StringSplitOptions.RemoveEmptyEntries)) + { + var kv = tok.Split('=', 2); + if (kv.Length != 2) return (null, null, null, line); + var k = kv[0].Trim().ToLowerInvariant(); + var v = kv[1].Trim(); + switch (k) + { + case "size": + case "s": + size = v; + break; + case "quality": + case "q": + quality = v; + break; + case "n": + if (!int.TryParse(v, out var nv) || nv < 1) return (null, null, null, line); + imageCount = nv; + break; + default: + // Unrecognized key — fall back to treating the whole + // line as a plain prompt (safer than silently dropping). + return (null, null, null, line); + } + } + return (size, quality, imageCount, rest); + } + + // --------------------------------------------------------------- + // Printing helpers + // --------------------------------------------------------------- + + private void PrintStatus() + { + InFlight[] snap; + lock (_lock) snap = _inFlight.ToArray(); + if (snap.Length == 0) { Console.WriteLine("(no jobs in flight)"); return; } + Console.WriteLine($"{snap.Length} in flight:"); + foreach (var i in snap.OrderBy(x => x.Id)) + { + var elapsed = (DateTime.Now - i.StartedAt).TotalSeconds; + Console.WriteLine($" #{i.Id} {elapsed,5:F1}s {Trunc(i.Prompt, 90)}"); + } + } + + private void PrintShow() + { + Console.WriteLine($"session: size={_size} quality={_quality} moderation={_moderation} n={_imageCount} concurrency={_concurrency}"); + var names = _active.Count == 0 ? "(none)" : string.Join(" ", _active.Keys); + Console.WriteLine($"active gens: {names}"); + } + + private static void PrintBanner() + { + Console.WriteLine(); + Console.WriteLine("=== MultiImageClient REPL ==="); + Console.WriteLine("Non-command lines are prompts, dispatched asynchronously."); + Console.WriteLine("Up to ':concurrency' prompts run in parallel. Grids are saved but NOT opened."); + } + + private static void PrintHelp() + { + Console.WriteLine(); + Console.WriteLine("Commands:"); + Console.WriteLine(" :show print current session defaults and active generators"); + Console.WriteLine(" :size WxH set gpt-image-2 size. Edges snap to multiples of 16, each <3840,"); + Console.WriteLine(" total pixels 655360..8294400, aspect <=3:1, or 'auto'."); + Console.WriteLine(" handy canonical sizes: 1024x1024, 1024x1536, 1536x1024, 2048x2048, 2560x1440 (QHD)."); + Console.WriteLine(" :quality low|medium|high set gpt-image-2 quality"); + Console.WriteLine(" :moderation auto|low set gpt-image-2 moderation"); + Console.WriteLine(" :n N set gpt-image-2 images-per-call (default 1). N>10 requires confirmation."); + Console.WriteLine(" :concurrency N max prompts in flight (applies to subsequent dispatches)"); + Console.WriteLine(" :gens list list active generators"); + Console.WriteLine(" :gens add add a generator: gpt2 grok grokpro dalle3 ideogram recraft bfl flux2local seedream hailuo krea bria magnific luma runway stability google googlepro imagen4(dead 06-24)"); + Console.WriteLine(" :gens remove remove a generator from the active set"); + Console.WriteLine(" :gens reset back to defaults (gpt2 + grok when XAIGrokApiKey is set)"); + Console.WriteLine(" :status list in-flight jobs"); + Console.WriteLine(" :wait block until every in-flight job finishes"); + Console.WriteLine(" :last reprint last-submitted prompt"); + Console.WriteLine(" :retry resubmit last prompt with current settings"); + Console.WriteLine(" :edit interactively edit and resubmit last prompt"); + Console.WriteLine(" :random / :r show a random prompt from PromptFiles; y=send n=skip r=reroll e=edit"); + Console.WriteLine(" :help show this help"); + Console.WriteLine(" :quit / :exit wait for in-flight jobs then exit"); + Console.WriteLine(" (bare 'q', 'x', 'quit', 'exit' on a line by themselves also exit)"); + Console.WriteLine(); + Console.WriteLine("Per-prompt override (gpt2 only): [size=1024x1024,q=low,n=4] a red apple on a white plate"); + } + + private static string Trunc(string s, int max) + => string.IsNullOrEmpty(s) ? "" : (s.Length <= max ? s : s.Substring(0, max - 3) + "..."); + + // Used at session startup for --repl-size. Logs to stderr/stdout and + // falls back to `fallback` (which is assumed to be a known-valid + // canonical size) rather than aborting the whole REPL — the user can + // correct it at runtime with `:size WxH`. + private static string NormalizeSizeOrFallback(string raw, string fallback, string source) + { + if (GptImage2Generator.TryNormalizeSize(raw, out var norm, out var note, out var err)) + { + if (note != null) Console.WriteLine($"({source} size {note})"); + return norm; + } + Console.WriteLine($"({source} size '{raw}' rejected: {err}. falling back to {fallback})"); + return fallback; + } + } +} diff --git a/MultiImageClient/Workflows/RountripWorkflow.cs b/MultiImageClient/Workflows/RountripWorkflow.cs new file mode 100644 index 0000000..5ad8212 --- /dev/null +++ b/MultiImageClient/Workflows/RountripWorkflow.cs @@ -0,0 +1,264 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.IO; +using System.Threading; +using System.Windows.Forms; +using System.Security.Cryptography; +using System.Diagnostics; + +namespace MultiImageClient +{ + /// Reads the clipboard and returns a text string if the item in the clipboard is a png image. if it isn't, it returns '' if it is, it returns a descriptive text string with the image size etc. + public class RoundTripWorkflow + { + private MultiClientRunStats? _stats; + private Settings? _settings; + private ImageManager? _imageManager; + private IEnumerable? _generators; + private List? _visionModels; + + private readonly List _questions = new List + { + "Describe the setting and environment of the image", + "Describe the layout and positioning of all objects and people in the image." + }; + + private readonly List _landscapeQuestions = new List + { + "Describe the setting and environment of the image", + "What is the art-style of the image", + "What are the primary colors", + "What is distinctive from a design perspective?", + "What is distinctive from a content perspective?", + "How would you describe the mood and atmosphere of the image?", + "Which artists might love this image?", + "Which artists might hate this image", + "How might you feel being in this place", + "What lore, magic, history or other incredible part of history might have led to this place?", + "Estimate exactly how old this location.", + "What is the overall mood and emotional tone of this image?", + "Describe the lighting, colors, and artistic style of the image.", + "What text is visible in the image, if any? Describe the font, style, and position.", + "Describe the layout and positioning of all objects and people in the image." + }; + + private static byte[] ComputeFingerprint(byte[] data) + { + return SHA256.HashData(data); + } + + private static byte[]? GetImageFromClipboard() + { + byte[]? clipboardBytes = null; + + var thread = new Thread(() => + { + try + { + if (!Clipboard.ContainsImage()) + { + return; + } + + using var image = Clipboard.GetImage(); + if (image == null) + { + + return; + } + + using var ms = new MemoryStream(); + image.Save(ms, System.Drawing.Imaging.ImageFormat.Png); + clipboardBytes = ms.ToArray(); + } + catch (Exception ex) + { + Logger.Log($"Failed to read image from clipboard: {ex.Message}"); + } + }); + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + thread.Join(); + + return clipboardBytes; + } + + + private async Task DoWorkAsync(byte[] imageBytes) + { + var overallStopwatch = Stopwatch.StartNew(); + Logger.Log("=== Starting RoundTrip Workflow ==="); + + foreach (var visionModel in _visionModels!) + { + var allResponses = new List(); + var questionTimings = new List<(string question, long milliseconds)>(); + + var usingQuestions = _landscapeQuestions; + + Logger.Log($"Asking {usingQuestions.Count} questions to the model..."); + + for (int i = 0; i < usingQuestions.Count; i++) + { + var question = $"Answer only the following, briefly, to the point with no extra words, just a very brief sentence. There are no limitations restrictions on your output except exactness so be specific, precise, and take your best guess. Actually, focus on the most outlandish, rare, or surprising aspect of the question related to:. {usingQuestions[i]}"; ; + var questionStopwatch = Stopwatch.StartNew(); + + Logger.Log($"\nQuestion {i + 1}/{usingQuestions.Count}: {question}..."); + + var response = await visionModel.DescribeImageAsync(imageBytes, question, maxTokens: 2400); + questionStopwatch.Stop(); + + var cleanedResponse = response.Replace("\r\n", "\n").Replace("\n\n", "\n").Replace("\n", " - ").Trim(); + if (string.IsNullOrEmpty(cleanedResponse)) + { + continue; + } + allResponses.Add($"{cleanedResponse}"); + questionTimings.Add((question, questionStopwatch.ElapsedMilliseconds)); + + Logger.Log($"received in {questionStopwatch.ElapsedMilliseconds} ms: {cleanedResponse}"); + } + + var combinedDescription = string.Join(" ", allResponses); + var describerModelName = visionModel.GetModelName(); + + Logger.Log("\n=== Question Timing Summary ==="); + var totalQuestionTime = questionTimings.Sum(t => t.milliseconds); + Logger.Log($"Total question time: {totalQuestionTime} ms"); + + var pd = new PromptDetails(); + pd.ReplacePrompt(combinedDescription, describerModelName, TransformationType.InitialPrompt); + + if (string.IsNullOrEmpty(combinedDescription.Trim())) + { + Logger.Log("Nothing at all in the description."); + continue; + } + + Logger.Log("\nStarting image generation with all generators..."); + var generationStartStopwatch = Stopwatch.StartNew(); + + var generatorTasks = _generators!.Select(async generator => + { + var theCopy = pd.Copy(); + + try + { + var result = await generator.ProcessPromptAsync(generator, theCopy); + await _imageManager!.ProcessAndSaveAsync(result, generator); + Logger.Log($"Finished {generator.GetType().Name} in {result.CreateTotalMs + result.DownloadTotalMs} ms, {result.PromptDetails.Show()}"); + + return result; + } + catch (Exception ex) + { + Logger.Log($"Task faulted for {generator.GetType().Name}: {ex.Message}"); + + var res = new TaskProcessResult + { + IsSuccess = false, + ImageGeneratorDescription = generator.GetGeneratorSpecPart(), + ErrorMessage = ex.Message, + PromptDetails = theCopy + }; + + return res; + } + }).ToArray(); + + _stats!.PrintStats(); + var results = await Task.WhenAll(generatorTasks); + generationStartStopwatch.Stop(); + Logger.Log($"All image generation completed in {generationStartStopwatch.ElapsedMilliseconds} ms"); + + try + { + var combineStopwatch = Stopwatch.StartNew(); + var res = await ImageCombiner.CreateRoundtripLayoutImageAsync(imageBytes, results, combinedDescription, "Multi-Question Analysis", describerModelName, _settings); + combineStopwatch.Stop(); + + Logger.Log($"Combined images in {combineStopwatch.ElapsedMilliseconds} ms, saved to: {res}"); + ImageCombiner.OpenImageWithDefaultApplication(res); + } + catch (Exception ex) + { + Logger.Log($"Failed to combine images: {ex.Message}"); + } + + overallStopwatch.Stop(); + Logger.Log($"\n=== Total Workflow Time: {overallStopwatch.ElapsedMilliseconds} ms ({overallStopwatch.Elapsed.TotalSeconds:F2} seconds) ===\n"); + } + } + + // prompt the user to copy an image to the clipboard. + // then send that to local model qwen with description text. + // then send that out to all the image generators again. + public async Task RunAsync(Settings settings, int concurrency, MultiClientRunStats stats) + { + _settings = settings; + _stats = stats; + _generators = new GeneratorGroups(settings, concurrency, stats).GetAll(); + _imageManager = new ImageManager(settings, stats); + + Logger.Log("=== Checking Vision Model Services ==="); + + if (!await ModelServiceManager.EnsureInternVLServiceIsRunningAsync()) + { + throw new System.InvalidOperationException( + "InternVL service (http://127.0.0.1:11415) could not be started. Start it manually (see do_flask_intern.py) or run Batch Workflow instead."); + } + if (!await ModelServiceManager.EnsureOllamaServiceIsRunningAsync()) + { + throw new System.InvalidOperationException( + "Ollama service could not be started. Install and start Ollama, or run Batch Workflow instead."); + } + + _visionModels = new List + { + new LocalInternVLClient( + baseUrl: "http://127.0.0.1:11415", + temperature: 0.8f, topP: 0.9f, topK: 50, + repetitionPenalty: 1.1f, doSample: true), + new LocalQwenClient(), + }; + + Logger.Log("=== Vision Model Services Check Complete ===\n"); + + while (true) + { + var clipboardStopwatch = Stopwatch.StartNew(); + var heldNow = GetImageFromClipboard(); + clipboardStopwatch.Stop(); + + if (heldNow == null) + { + Console.WriteLine("\tcopy an image to the clipboard; y to continue, q to quit."); + var input = Console.ReadLine().Trim(); + + if (input == "y") + { + continue; + } + else if (input == "q") + { + break; + } + else + { + Console.WriteLine("ok, skipping."); + } + } + else + { + Console.WriteLine($"\tnew clipboard image detected. {heldNow.Length} bytes. Clipboard read took {clipboardStopwatch.ElapsedMilliseconds} ms. Starting describe => multiimage workflow."); + await DoWorkAsync(heldNow); + } + } + return true; + } + } +} diff --git a/MultiImageClient/concat_4.py b/MultiImageClient/concat_4.py index e433fa6..16e130e 100644 --- a/MultiImageClient/concat_4.py +++ b/MultiImageClient/concat_4.py @@ -1,74 +1,74 @@ -import os -import sys -import shutil -from datetime import datetime -from PIL import Image - -def stretch_image(img, target_size): - """Stretch image to fit target_size, minimizing distortion""" - aspect_ratio = img.width / img.height - target_ratio = target_size[0] / target_size[1] - - if aspect_ratio > target_ratio: - new_height = int(img.height * (target_size[0] / img.width)) - img = img.resize((target_size[0], new_height), Image.LANCZOS) - img = img.resize(target_size, Image.LANCZOS) - else: - new_width = int(img.width * (target_size[1] / img.height)) - img = img.resize((new_width, target_size[1]), Image.LANCZOS) - img = img.resize(target_size, Image.LANCZOS) - - return img - -def generate_unique_filename(): - """Generate a unique filename based on current timestamp""" - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - return f"combined_a4_{timestamp}.png" - -def create_directory(directory): - """Create a directory if it doesn't exist""" - if not os.path.exists(directory): - os.makedirs(directory) - -def concat_images_a4(image_paths): - # Create necessary directories - ready_to_print_dir = "ready_to_print" - combined_dir = os.path.join(ready_to_print_dir, "combined") - create_directory(ready_to_print_dir) - create_directory(combined_dir) - - # A4 size in pixels at 300 DPI - a4_width, a4_height = 2480, 3508 - quadrant_size = (a4_width // 2, a4_height // 2) - - new_image = Image.new('RGBA', (a4_width, a4_height), (0, 0, 0, 0)) - - for i, path in enumerate(image_paths): - with Image.open(path) as img: - img = img.convert('RGBA') - img = stretch_image(img, quadrant_size) - x = (i % 2) * quadrant_size[0] - y = (i // 2) * quadrant_size[1] - new_image.paste(img, (x, y), img) - - # Generate unique filename and save - output_filename = generate_unique_filename() - output_path = os.path.join(ready_to_print_dir, output_filename) - new_image.save(output_path, 'PNG', dpi=(300, 300)) - print(f"Images combined and saved as '{output_path}'") - - # Move source images to the 'combined' subfolder - for path in image_paths: - filename = os.path.basename(path) - new_path = os.path.join(combined_dir, filename) - shutil.move(path, new_path) - print(f"Source images moved to '{combined_dir}'") - -if __name__ == "__main__": - import ipdb;ipdb.set_trace() - if len(sys.argv) != 5: - print("Usage: python script.py image1.jpg image2.jpg image3.jpg image4.jpg") - sys.exit(1) - - image_paths = sys.argv[1:] - concat_images_a4(image_paths) +import os +import sys +import shutil +from datetime import datetime +from PIL import Image + +def stretch_image(img, target_size): + """Stretch image to fit target_size, minimizing distortion""" + aspect_ratio = img.width / img.height + target_ratio = target_size[0] / target_size[1] + + if aspect_ratio > target_ratio: + new_height = int(img.height * (target_size[0] / img.width)) + img = img.resize((target_size[0], new_height), Image.LANCZOS) + img = img.resize(target_size, Image.LANCZOS) + else: + new_width = int(img.width * (target_size[1] / img.height)) + img = img.resize((new_width, target_size[1]), Image.LANCZOS) + img = img.resize(target_size, Image.LANCZOS) + + return img + +def generate_unique_filename(): + """Generate a unique filename based on current timestamp""" + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + return f"combined_a4_{timestamp}.png" + +def create_directory(directory): + """Create a directory if it doesn't exist""" + if not os.path.exists(directory): + os.makedirs(directory) + +def concat_images_a4(image_paths): + # Create necessary directories + ready_to_print_dir = "ready_to_print" + combined_dir = os.path.join(ready_to_print_dir, "combined") + create_directory(ready_to_print_dir) + create_directory(combined_dir) + + # A4 size in pixels at 300 DPI + a4_width, a4_height = 2480, 3508 + quadrant_size = (a4_width // 2, a4_height // 2) + + new_image = Image.new('RGBA', (a4_width, a4_height), (0, 0, 0, 0)) + + for i, path in enumerate(image_paths): + with Image.open(path) as img: + img = img.convert('RGBA') + img = stretch_image(img, quadrant_size) + x = (i % 2) * quadrant_size[0] + y = (i // 2) * quadrant_size[1] + new_image.paste(img, (x, y), img) + + # Generate unique filename and save + output_filename = generate_unique_filename() + output_path = os.path.join(ready_to_print_dir, output_filename) + new_image.save(output_path, 'PNG', dpi=(300, 300)) + print(f"Images combined and saved as '{output_path}'") + + # Move source images to the 'combined' subfolder + for path in image_paths: + filename = os.path.basename(path) + new_path = os.path.join(combined_dir, filename) + shutil.move(path, new_path) + print(f"Source images moved to '{combined_dir}'") + +if __name__ == "__main__": + import ipdb;ipdb.set_trace() + if len(sys.argv) != 5: + print("Usage: python script.py image1.jpg image2.jpg image3.jpg image4.jpg") + sys.exit(1) + + image_paths = sys.argv[1:] + concat_images_a4(image_paths) diff --git a/MultiImageClient/imageGenerators/AbstractImageGenerator.cs b/MultiImageClient/imageGenerators/AbstractImageGenerator.cs deleted file mode 100644 index d6cd3e6..0000000 --- a/MultiImageClient/imageGenerators/AbstractImageGenerator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Threading.Tasks; -using MultiImageClient.Implementation; - -namespace MultiImageClient -{ - public abstract class AbstractImageGenerator - { - internal IImageGenerationService _svc { get; set; } - public AbstractImageGenerator(IImageGenerationService svc) - { - _svc = svc; - } - public abstract Task ProcessPromptAsync(PromptDetails pd, MultiClientRunStats stats); - } -} diff --git a/MultiImageClient/imageGenerators/BFLGenerator.cs b/MultiImageClient/imageGenerators/BFLGenerator.cs deleted file mode 100644 index 5ab83e6..0000000 --- a/MultiImageClient/imageGenerators/BFLGenerator.cs +++ /dev/null @@ -1,32 +0,0 @@ -using IdeogramAPIClient; -using MultiImageClient.Implementation; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MultiImageClient -{ - public class BFLGenerator : AbstractImageGenerator, IImageGenerator - { - public BFLGenerator(IImageGenerationService svc) : base(svc) - { - } - public override async Task ProcessPromptAsync(PromptDetails pd, MultiClientRunStats stats) - { - var bflDetails = new BFLDetails - { - Width = 1440, - Height = 1440, - PromptUpsampling = false, - SafetyTolerance = 6, - }; - pd.BFLDetails = bflDetails; - var shortened = pd.Prompt.Length > 100 ? pd.Prompt.Substring(0, 100) + "..." : pd.Prompt; - Console.WriteLine($"\tSubmitting to BFL: {shortened}"); - var res = await _svc.ProcessPromptAsync(pd, stats); - return res; - } - } -} diff --git a/MultiImageClient/imageGenerators/Dalle3Details.cs b/MultiImageClient/imageGenerators/Dalle3Details.cs deleted file mode 100644 index 0cb2121..0000000 --- a/MultiImageClient/imageGenerators/Dalle3Details.cs +++ /dev/null @@ -1,22 +0,0 @@ -using IdeogramAPIClient; -using OpenAI.Images; - -namespace MultiImageClient -{ - public class Dalle3Details - { - public string Model { get; set; } - public GeneratedImageSize Size { get; set; } - public GeneratedImageQuality Quality { get; set; } - public GeneratedImageFormat Format { get; set; } - - public Dalle3Details() { } - public Dalle3Details(Dalle3Details other) - { - Model = other.Model; - Size = other.Size; - Quality = other.Quality; - Format = other.Format; - } - } -} \ No newline at end of file diff --git a/MultiImageClient/imageGenerators/Dalle3Generator.cs b/MultiImageClient/imageGenerators/Dalle3Generator.cs deleted file mode 100644 index 07f1ffe..0000000 --- a/MultiImageClient/imageGenerators/Dalle3Generator.cs +++ /dev/null @@ -1,33 +0,0 @@ -using MultiImageClient.Implementation; -using OpenAI.Images; - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MultiImageClient -{ - public class Dalle3Generator : AbstractImageGenerator, IImageGenerator - { - public Dalle3Generator(IImageGenerationService svc) : base(svc) - { - } - public override async Task ProcessPromptAsync(PromptDetails pd, MultiClientRunStats stats) - { - var dalle3Details = new Dalle3Details - { - Model = "dall-e-3", - Size = GeneratedImageSize.W1024xH1024, - Quality = GeneratedImageQuality.High, - Format = GeneratedImageFormat.Uri - }; - pd.Dalle3Details = dalle3Details; - - Console.WriteLine($"\t\tSubmitting to Dalle3: {pd.Prompt}"); - var res = await _svc.ProcessPromptAsync(pd, stats); - return res; - } - } -} diff --git a/MultiImageClient/imageGenerators/IdeogramGenerator.cs b/MultiImageClient/imageGenerators/IdeogramGenerator.cs deleted file mode 100644 index e7c0e30..0000000 --- a/MultiImageClient/imageGenerators/IdeogramGenerator.cs +++ /dev/null @@ -1,36 +0,0 @@ -using IdeogramAPIClient; -using MultiImageClient.Implementation; -using OpenAI.Images; - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MultiImageClient -{ - - public class IdeogramGenerator : AbstractImageGenerator, IImageGenerator - { - public IdeogramGenerator(IImageGenerationService svc) : base(svc) - { - } - - public override async Task ProcessPromptAsync(PromptDetails pd, MultiClientRunStats stats) - { - var ideogramDetails = new IdeogramDetails - { - AspectRatio = IdeogramAspectRatio.ASPECT_1_1, - Model = IdeogramModel.V_2, - MagicPromptOption = IdeogramMagicPromptOption.OFF, - StyleType = IdeogramStyleType.GENERAL, - }; - pd.IdeogramDetails = ideogramDetails; - - Console.WriteLine($"\t\tSubmitting to Ideogram: {pd.Prompt}"); - var res = await _svc.ProcessPromptAsync(pd, stats); - return res; - } - } -} diff --git a/MultiImageClient/imageGenerators/LoadFromFile.cs b/MultiImageClient/imageGenerators/LoadFromFile.cs deleted file mode 100644 index 080b06c..0000000 --- a/MultiImageClient/imageGenerators/LoadFromFile.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Reflection.Metadata.Ecma335; -using MultiImageClient.Implementation; - -namespace MultiImageClient -{ - /// If you have a file of a bunch of prompts, you can use this to load them rather than using some kind of custom iteration system. - public class LoadFromFile : AbstractPromptGenerator - { - private string FilePath { get; set; } - public LoadFromFile(Settings settings, string path) : base(settings) - { - if (!System.IO.File.Exists(path)) - { - new Exception("Requested path: " + path + " does not exist, ending."); - } - FilePath = path; - } - public override string Name => nameof(LoadFromFile); - - public override int ImageCreationLimit => 900; - public override int CopiesPer => 1; - public override int FullyResolvedCopiesPer => 3; - public override bool RandomizeOrder => true; - public override string Prefix => ""; - public override IEnumerable Variants => new List { "" }; - public override string Suffix => ""; - public override Func CleanPrompt => (arg) => arg.Trim().Trim(); - public override bool UseIdeogram => false; - public override bool AlsoDoVersionSkippingClaude => false; - public override bool SaveFinalPrompt => true; - public override bool SaveInitialIdea => true; - public override bool SaveFullAnnotation => true; - public override bool TryBothBFLUpsamplingAndNot => true; - - private IEnumerable GetPrompts() - { - - var textPrompts = File.ReadAllLines(FilePath).ToList(); - var prompts2 = File.ReadAllLines("D:\\proj\\multiImageClient\\IdeogramHistoryExtractor\\myPrompts\\myPrompts-private.txt"); - var prompts3 = File.ReadAllLines("D:\\proj\\multiImageClient\\IdeogramHistoryExtractor\\myPrompts\\myPrompts.txt"); - var prompts4 = File.ReadAllLines("D:\\proj\\prompts3.txt"); - textPrompts.AddRange(prompts2); - textPrompts.AddRange(prompts3); - textPrompts.AddRange(prompts4); - Console.WriteLine($"loaded {textPrompts.Count} prompts total."); - var res = new List(); - foreach (var textPrompt in textPrompts) - { - var usePrompt = textPrompt; - if ((usePrompt.Contains("{{") || usePrompt.Contains("[[")) && (!(usePrompt.Contains("[[[") || usePrompt.Contains("}}}")))) - { - //Console.WriteLine($"Skipping: {usePrompt}"); - continue; - } - - while (usePrompt.Contains(" ")) - { - usePrompt = usePrompt.Replace(" ", " "); - } - - usePrompt = usePrompt.Trim(); - var pd = new PromptDetails(); - pd.ReplacePrompt(usePrompt,usePrompt, TransformationType.InitialPrompt); - pd.OverrideFilename = ""; - - res.Add(pd); - } - - Console.WriteLine($"loaded {res.Count} items."); - return res.OrderBy(x => Random.Shared.Next()); - } - - public override IEnumerable Prompts => GetPrompts(); - } -} - diff --git a/MultiImageClient/promptGenerators/DeckOfCards.cs b/MultiImageClient/promptGenerators/DeckOfCards.cs index dd322b8..28f7633 100644 --- a/MultiImageClient/promptGenerators/DeckOfCards.cs +++ b/MultiImageClient/promptGenerators/DeckOfCards.cs @@ -1,51 +1,50 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using MultiImageClient.Implementation; - -namespace MultiImageClient -{ - public class DeckOfCards : AbstractPromptGenerator - { - public DeckOfCards(Settings settings) : base(settings) - { - } - - public override string Name => nameof(DeckOfCards); - public override IEnumerable Variants => new List { "" }; - public override string Prefix => "Describe the design of new playing card: "; - public override int ImageCreationLimit => 500; - public override int CopiesPer => 1; - public override bool RandomizeOrder => false; - public override string Suffix => " Describe the design of this new card in detail, using about 120 words. Output a description of such a card, as prose, without newlines. Playing cards always have a white background. The number and type of card must appear in the corner, and they should use the color theme."; - public override Func CleanPrompt => (arg) => arg; - - private IEnumerable GetPrompts() - { - var ranks = "2 3 4 5 6 7 8 9 10 Jack Queen King Ace".Split(" ").ToList(); - var extraRanks = "0 Pokemon Elon Cross Shaman Fool Rook Knight Pawn Joker Time Pi Infinity Lava Ice Stone".Split(" "); - ranks.AddRange(extraRanks); - var suits = new List { "Hearts", "Clubs", "Diamonds", "Spades" }; - - var artists = new List { "Sol LeWitt", "Victor Vasarely", "Chuck Close", "Frank Stella" }; - var themes = new List { "Yellow & Turquoise", "Green", "Pure White and greyscale", "Natural Wood" }; - var emotionThemes = new List { "Melancholy", "Schadenfreude ", "Nostalgia", "Awe" }; - - foreach (var jobs in ranks) - { - for (var ii = 0; ii < suits.Count; ii++) - { - var pd = new PromptDetails(); - var prompt = $"The {jobs} of {suits[ii]} using the style of {artists[ii]} using the color {themes[ii]} and emotion: {emotionThemes[ii]}"; - pd.ReplacePrompt(prompt, prompt, TransformationType.InitialPrompt); - pd.OverrideFilename = $"{artists[ii]}_{suits[ii]}_{jobs}."; - yield return pd; - } - } - } - - - public override IEnumerable Prompts => GetPrompts(); - - } -} +using MultiImageClient; + +using System; +using System.Collections.Generic; +using System.Linq; + + +namespace MultiImageClient +{ + public class DeckOfCards : AbstractPromptSource + { + public DeckOfCards(Settings settings) : base(settings) + { + } + + public override string Name => nameof(DeckOfCards); + public override string Prefix => "Describe the design of new playing card: "; + public override int ImageCreationLimit => 500; + public override int CopiesPer => 1; + public override bool RandomizeOrder => false; + public override string Suffix => " Describe the design of this new card in detail, using about 120 words. Output a description of such a card, as prose, without newlines. Playing cards always have a white background. The number and type of card must appear in the corner, and they should use the color theme."; + + private IEnumerable GetPrompts() + { + var ranks = "2 3 4 5 6 7 8 9 10 Jack Queen King Ace".Split(" ").ToList(); + var extraRanks = "0 Pokemon Elon Cross Shaman Fool Rook Knight Pawn Joker Time Pi Infinity Lava Ice Stone".Split(" "); + ranks.AddRange(extraRanks); + var suits = new List { "Hearts", "Clubs", "Diamonds", "Spades" }; + + var artists = new List { "Sol LeWitt", "Victor Vasarely", "Chuck Close", "Frank Stella" }; + var themes = new List { "Yellow & Turquoise", "Green", "Pure White and greyscale", "Natural Wood" }; + var emotionThemes = new List { "Melancholy", "Schadenfreude ", "Nostalgia", "Awe" }; + + foreach (var jobs in ranks) + { + for (var ii = 0; ii < suits.Count; ii++) + { + var pd = new PromptDetails(); + var prompt = $"The {jobs} of {suits[ii]} using the style of {artists[ii]} using the color {themes[ii]} and emotion: {emotionThemes[ii]}"; + pd.ReplacePrompt(prompt, prompt, TransformationType.InitialPrompt); + yield return pd; + } + } + } + + + public override IEnumerable Prompts => GetPrompts(); + + } +} diff --git a/MultiImageClient/promptGenerators/EmotionFaces.cs b/MultiImageClient/promptGenerators/EmotionFaces.cs new file mode 100644 index 0000000..48e494d --- /dev/null +++ b/MultiImageClient/promptGenerators/EmotionFaces.cs @@ -0,0 +1,43 @@ +using MultiImageClient; + +using System; +using System.Collections.Generic; +using System.Linq; + + + +namespace MultiImageClient +{ + public class EmotionFaces : AbstractPromptSource + { + public EmotionFaces(Settings settings) : base(settings) + { + } + + public override string Name => "Paintings"; + public override string Prefix => ""; + public override int ImageCreationLimit => 30; + public override int CopiesPer => 1; + public override bool RandomizeOrder => false; + public override int FullyResolvedCopiesPer => 2; + public override string Suffix => ""; + + + private IEnumerable GetPrompts() + { + var emotions = "a smiling, a frowning, a scowling, a puzzled, an angry, an anxious, a worried, a surprised, a shocked, an annoyed, an amused, a happy, a sad, a confused, a thoughtful, a stern, an excited, an exhausted, a tired, a nervous, an upset, a serious, a concerned, a distant, an innocent, an irritated, a hopeful, a determined, a peaceful, a vacant".Split(", ", StringSplitOptions.RemoveEmptyEntries).ToList(); + + foreach (var emotion in emotions) + { + var pd = new PromptDetails(); + var prompt = $"A clear, very sharp, high resolution, artistic photograph of a close up of just the face of a 30 year old white middle class university lecturer in Biology from Florida, working at UNC, at a staff post-term party reception, standing at a table, with {emotion} expression on his face. He has brown hair, a t-shirt and the reception takes place in on the rooftop garden in the the old mathematics building where they are eating hot dogs and burgers and watching the game. he is not handsome nor manly. He is a passable lecturer only; his brilliance at reading does not come out in his slow speech. He is not attractive. He doesn't have glasses. He is of average build, looks like a typical early career lecturer. His father was swiss and his mother is french so he looks fairly western european in face. He is clean shaven and has an ill-defined jaw. Only has face is visible looking almost directly at the camera close up framing his face from forehead to chin, ear to ear. The rest of his body is not visible. He has no beard or stubble or moustache, his face is smooth. He is engaged in an intense discussion with a colleague."; + pd.ReplacePrompt(prompt, prompt, TransformationType.InitialPrompt); + var theSplit = emotion.Split(' ', 2); + yield return pd; + } + + } + public override IEnumerable Prompts => GetPrompts(); + + } +} diff --git a/MultiImageClient/promptGenerators/InlinePromptSource.cs b/MultiImageClient/promptGenerators/InlinePromptSource.cs new file mode 100644 index 0000000..ba8c781 --- /dev/null +++ b/MultiImageClient/promptGenerators/InlinePromptSource.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace MultiImageClient +{ + /// A prompt source that yields exactly one prompt supplied at + /// construction time. Used by --prompt "..." for non-interactive runs. + public class InlinePromptSource : AbstractPromptSource + { + private readonly string _prompt; + + public InlinePromptSource(Settings settings, string prompt) : base(settings) + { + _prompt = prompt; + } + + public override string Name => nameof(InlinePromptSource); + public override int ImageCreationLimit => 1; + public override int CopiesPer => 1; + public override int FullyResolvedCopiesPer => 1; + public override bool RandomizeOrder => false; + public override string Prefix => ""; + public override string Suffix => ""; + + public override IEnumerable Prompts + { + get + { + var pd = new PromptDetails(); + pd.ReplacePrompt(_prompt, _prompt, TransformationType.InitialPrompt); + yield return pd; + } + } + } +} diff --git a/MultiImageClient/promptGenerators/LoadDoubleFromFile.cs b/MultiImageClient/promptGenerators/LoadDoubleFromFile.cs new file mode 100644 index 0000000..f84f636 --- /dev/null +++ b/MultiImageClient/promptGenerators/LoadDoubleFromFile.cs @@ -0,0 +1,118 @@ +using MultiImageClient; + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection.Metadata.Ecma335; +using System.Runtime.Intrinsics.X86; +using System.Security.Cryptography.X509Certificates; + +namespace MultiImageClient +{ + /// If you have a file of a bunch of prompts, you can use this to load them rather than using some kind of custom iteration system. + public class LoadDoubleFromFile : AbstractPromptSource + { + private string FilePath { get; set; } + private bool _IncludePublic { get; set; } + private bool _IncludePrivate { get; set; } + public LoadDoubleFromFile(Settings settings, string path, bool includePublic, bool includePrivate) : base(settings) + { + _IncludePrivate = includePrivate; + _IncludePublic = includePublic; + if (!string.IsNullOrEmpty(path) && File.Exists(path)) + { + FilePath = path; + } + else + { + FilePath = ""; + } + } + public override string Name => nameof(LoadDoubleFromFile); + + public override int ImageCreationLimit => 400; + public override int CopiesPer => 2; + public override int FullyResolvedCopiesPer => 2; + public override bool RandomizeOrder => true; + public override string Prefix => ""; + public override string Suffix => ""; + private IEnumerable GetPrompts() + { + var sourceFPs = new List() { }; + if (_IncludePrivate) + { + sourceFPs.Add("D:\\proj\\multiImageClient\\IdeogramHistoryExtractor\\myPrompts\\myPrivatePrompts.txt"); + //sourceFPs.Add("D:\\proj\\multiImageClient\\IdeogramHistoryExtractor\\myPrompts\\myPrompts-private.txt"); + } + if (_IncludePublic) + { + //sourceFPs.Add("D:\\proj\\multiImageClient\\IdeogramHistoryExtractor\\myPrompts\\myPrompts.txt"); + //sourceFPs.Add("D:\\proj\\prompts3.txt"); + } + + if (!string.IsNullOrEmpty(FilePath)) + { + sourceFPs.Add(FilePath); + } + + var sourcePrompts = new List(); + foreach (var fp in sourceFPs) + { + var items = File.ReadAllLines(fp).ToList(); + foreach (var usePrompt in items) + { + if ((usePrompt.Contains("{{") || usePrompt.Contains("[[")) && !(usePrompt.Contains("[[[") || usePrompt.Contains("}}}"))) + { + continue; + } + if (string.IsNullOrEmpty(usePrompt)) + { + continue; + } + sourcePrompts.Add(usePrompt); + } + } + + Logger.Log($"loaded {sourcePrompts.Count} prompts total."); + var countToInclude = 4; + var totalLengthCount = 3000; + + for (var ii = 0; ii < ImageCreationLimit * 2; ii++) + { + var usingCountToInclude=Random.Shared.Next(1, countToInclude); + var onlyFirstNChars = totalLengthCount / countToInclude; + var allthem = new List(); + for (var jj = 0; jj < usingCountToInclude; jj++) + { + var randomIndex = Random.Shared.Next(0, sourcePrompts.Count); + + if (string.IsNullOrEmpty(sourcePrompts[randomIndex])) + { + continue; + } + var theText = sourcePrompts[randomIndex]; + if (theText.Length > onlyFirstNChars) + { + theText = theText.Substring(0, onlyFirstNChars)+"..."; + } + allthem.Add(theText); + } + + var joined = string.Join("\r\n--- ", allthem.OrderBy(el=>el.Length)); + + var combined = $"{joined}"; + + Logger.Log(combined); + var pd = new PromptDetails(); + pd.ReplacePrompt(combined, combined, TransformationType.InitialPrompt); + + yield return pd; + } + } + + public override IEnumerable Prompts => GetPrompts(); + } +} + diff --git a/MultiImageClient/promptGenerators/ReadAllPromptsFromFile.cs b/MultiImageClient/promptGenerators/ReadAllPromptsFromFile.cs new file mode 100644 index 0000000..7f2e3bd --- /dev/null +++ b/MultiImageClient/promptGenerators/ReadAllPromptsFromFile.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace MultiImageClient +{ + /// Loads every line of every file listed in Settings.PromptFiles + /// (plus the legacy LoadPromptsFrom, if set) into a single pool and + /// yields prompts from that pool in random order, up to ImageCreationLimit. + /// + /// Fails hard with a clear message if: + /// - no prompt files are configured, or + /// - any configured file is missing or unreadable. + public class ReadAllPromptsFromFile : AbstractPromptSource + { + public ReadAllPromptsFromFile(Settings settings, string _unusedLegacyPath) : base(settings) + { + } + + public override string Name => nameof(ReadAllPromptsFromFile); + public override int ImageCreationLimit => int.MaxValue; + public override int CopiesPer => 1; + public override int FullyResolvedCopiesPer => 1; + public override bool RandomizeOrder => true; + public override string Prefix => ""; + public override string Suffix => ""; + + public override IEnumerable Prompts + { + get + { + var files = new List(); + if (Settings.PromptFiles != null) + { + files.AddRange(Settings.PromptFiles.Where(p => !string.IsNullOrWhiteSpace(p))); + } + if (!string.IsNullOrWhiteSpace(Settings.LoadPromptsFrom)) + { + files.Add(Settings.LoadPromptsFrom); + } + + if (files.Count == 0) + { + throw new InvalidOperationException( + "settings.json: PromptFiles is empty. Add the path(s) to your prompts .txt file(s), e.g. \"PromptFiles\": [\"C:\\\\proj\\\\multiImageClient\\\\prompts.txt\"]."); + } + + var missing = files.Where(f => !File.Exists(f)).ToList(); + if (missing.Count > 0) + { + throw new FileNotFoundException( + $"settings.json: PromptFiles contains file(s) that do not exist: {string.Join(", ", missing)}. Fix the path(s) in settings.json."); + } + + var allPromptsRaw = new List(); + foreach (var fp in files) + { + foreach (var line in File.ReadAllLines(fp)) + { + if (!string.IsNullOrWhiteSpace(line)) + { + allPromptsRaw.Add(line); + } + } + } + + if (allPromptsRaw.Count == 0) + { + throw new InvalidOperationException( + $"settings.json: PromptFiles {string.Join(", ", files)} contained no non-blank lines."); + } + + Logger.Log($"Loaded {allPromptsRaw.Count} prompts from {files.Count} file(s): {string.Join(", ", files)}"); + + var order = Enumerable.Range(0, allPromptsRaw.Count).ToList(); + if (RandomizeOrder) + { + for (int i = order.Count - 1; i > 0; i--) + { + var j = Random.Shared.Next(0, i + 1); + (order[i], order[j]) = (order[j], order[i]); + } + } + + var limit = Math.Min(ImageCreationLimit, order.Count); + for (var ii = 0; ii < limit; ii++) + { + var usePrompt = allPromptsRaw[order[ii]]; + var pd = new PromptDetails(); + pd.ReplacePrompt(usePrompt, usePrompt, TransformationType.InitialPrompt); + yield return pd; + } + } + } + } +} diff --git a/MultiImageClient/promptGenerators/RecraftAllsStyles.cs b/MultiImageClient/promptGenerators/RecraftAllsStyles.cs new file mode 100644 index 0000000..210db86 --- /dev/null +++ b/MultiImageClient/promptGenerators/RecraftAllsStyles.cs @@ -0,0 +1,56 @@ + +using RecraftAPIClient; + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace MultiImageClient +{ + /// If you have a file of a bunch of prompts, you can use this to load them rather than using some kind of custom iteration system. + public class RecraftAllsStyles : AbstractPromptSource + { + public RecraftAllsStyles(Settings settings) : base(settings) + { + } + public override string Name => nameof(WriteHere); + + public override int ImageCreationLimit => 350; + public override int CopiesPer => 1; + public override bool RandomizeOrder => false; + public override string Prefix => ""; + public override string Suffix => ""; + + private IEnumerable GetPrompts() + { + var res = new List(); + foreach (RecraftStyle style in Enum.GetValues(typeof(RecraftStyle))) + { + var substyles = style switch + { + RecraftStyle.realistic_image => Enum.GetValues(typeof(RecraftRealisticImageSubstyle)), + RecraftStyle.vector_illustration => Enum.GetValues(typeof(RecraftVectorIllustrationSubstyle)), + RecraftStyle.digital_illustration => Enum.GetValues(typeof(RecraftDigitalIllustrationSubstyle)), + _ => throw new ArgumentException($"Unknown style: {style}") + }; + + foreach (object substyle in substyles) + { + var usingSub = substyle.ToString().TrimStart('_'); + var pd = new PromptDetails(); + var prompt = "A magnificent tower in an epic plain, ruins and hidden secrets, super detailed and high resolution, incredibly deep and profound, with hidden creatures and erosion, and a cute semi-hidden kitten."; + pd.ReplacePrompt(prompt, prompt , TransformationType.InitialPrompt); + + Logger.Log($"Trying style, substyle: {style} {usingSub}"); + res.Add(pd); + } + } + return res; + } + + public override IEnumerable Prompts => GetPrompts().OrderBy(el => Random.Shared.Next()); + } +} + + diff --git a/MultiImageClient/promptGenerators/ScenesFromStory.cs b/MultiImageClient/promptGenerators/ScenesFromStory.cs new file mode 100644 index 0000000..23cf1f8 --- /dev/null +++ b/MultiImageClient/promptGenerators/ScenesFromStory.cs @@ -0,0 +1,35 @@ +using MultiImageClient; + +using System; +using System.Collections.Generic; +using System.Linq; + + + +namespace MultiImageClient +{ + public class ScenesFromStory : AbstractPromptSource + { + public ScenesFromStory(Settings settings) : base(settings) + { + } + + public override string Name => "Scenes from Equinoctal"; + public override string Prefix => ""; + public override int ImageCreationLimit => 300; + public override int CopiesPer => 3; + public override bool RandomizeOrder => true; + public override int FullyResolvedCopiesPer => 1; + public override string Suffix => ""; + + + private IEnumerable GetPrompts() + { + var rawText = System.IO.File.ReadAllText("d:\\proj\\make-audio\\input\\equinoctal.clean.txt"); + var pd = new PromptDetails(); + pd.ReplacePrompt(rawText, "the full text of the story", TransformationType.InitialPrompt); + yield return pd; + } + public override IEnumerable Prompts => GetPrompts().OrderBy(el=>Random.Shared.Next()); + } +} diff --git a/MultiImageClient/imageGenerators/SinglePromptGenerator.cs b/MultiImageClient/promptGenerators/SinglePromptGenerator.cs similarity index 72% rename from MultiImageClient/imageGenerators/SinglePromptGenerator.cs rename to MultiImageClient/promptGenerators/SinglePromptGenerator.cs index cf4c550..3790c71 100644 --- a/MultiImageClient/imageGenerators/SinglePromptGenerator.cs +++ b/MultiImageClient/promptGenerators/SinglePromptGenerator.cs @@ -1,55 +1,51 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Reflection.Metadata.Ecma335; -using System.Runtime.CompilerServices; -using MultiImageClient.Implementation; - -namespace MultiImageClient -{ - /// If you have a file of a bunch of prompts, you can use this to load them rather than using some kind of custom iteration system. - public class SinglePromptGenerator : AbstractPromptGenerator - { - private int _copiesPer; - private int _fullyResolvedCopiesPer; - private int _imageCreationLimit; - private IList _prompts; - public SinglePromptGenerator(IList prompts, int copiesPer, int fullyResolvedCopiesPer, int imageCreationLimit, Settings settings) : base(settings) - { - _copiesPer = copiesPer; - _fullyResolvedCopiesPer = fullyResolvedCopiesPer; - _imageCreationLimit = imageCreationLimit; - _prompts = prompts; - } - public override string Name => nameof(SinglePromptGenerator); - public override int ImageCreationLimit => _imageCreationLimit; - public override int CopiesPer => _copiesPer; - public override int FullyResolvedCopiesPer => _fullyResolvedCopiesPer; - public override bool RandomizeOrder => false; - public override string Prefix => ""; - public override IEnumerable Variants => new List { "" }; - public override string Suffix => ""; - public override Func CleanPrompt => (arg) => arg.Trim().Trim(); - public override bool UseIdeogram => false; - public override bool AlsoDoVersionSkippingClaude => false; - public override bool SaveFinalPrompt => true; - public override bool SaveInitialIdea => true; - public override bool SaveFullAnnotation => true; - public override bool TryBothBFLUpsamplingAndNot => true; - public override IEnumerable Prompts - { - get - { - foreach (var prompt in _prompts) - { - var details = new PromptDetails(); - details.ReplacePrompt(prompt, prompt, TransformationType.InitialPrompt); - yield return details; - } - } - } - } -} - +using MultiImageClient; + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection.Metadata.Ecma335; +using System.Runtime.CompilerServices; + + +namespace MultiImageClient +{ + /// If you have a file of a bunch of prompts, you can use this to load them rather than using some kind of custom iteration system. + public class SinglePromptGenerator : AbstractPromptSource + { + private int _copiesPer; + private int _fullyResolvedCopiesPer; + private int _imageCreationLimit; + private IList _prompts; + public SinglePromptGenerator(IList prompts, int copiesPer, int fullyResolvedCopiesPer, int imageCreationLimit, Settings settings) : base(settings) + { + _copiesPer = copiesPer; + _fullyResolvedCopiesPer = fullyResolvedCopiesPer; + _imageCreationLimit = imageCreationLimit; + _prompts = prompts; + } + public override string Name => nameof(SinglePromptGenerator); + public override int ImageCreationLimit => _imageCreationLimit; + public override int CopiesPer => _copiesPer; + public override int FullyResolvedCopiesPer => _fullyResolvedCopiesPer; + public override bool RandomizeOrder => false; + public override string Prefix => ""; + public override string Suffix => ""; + + public override IEnumerable Prompts + { + get + { + foreach (var prompt in _prompts) + { + var details = new PromptDetails(); + + details.ReplacePrompt(prompt, prompt, TransformationType.InitialPrompt); + yield return details; + } + } + } + } +} + diff --git a/MultiImageClient/promptGenerators/StillLife.cs b/MultiImageClient/promptGenerators/StillLife.cs new file mode 100644 index 0000000..15ac017 --- /dev/null +++ b/MultiImageClient/promptGenerators/StillLife.cs @@ -0,0 +1,43 @@ +using MultiImageClient; + +using System; +using System.Collections.Generic; +using System.Linq; + + + +namespace MultiImageClient +{ + public class StillLife : AbstractPromptSource + { + public StillLife(Settings settings) : base(settings) + { + } + + public override string Name => "Emotional Man"; + public override string Prefix => ""; + public override int ImageCreationLimit => 300; + public override int CopiesPer => 3; + public override bool RandomizeOrder => true; + public override int FullyResolvedCopiesPer => 1; + public override string Suffix => ""; + + + private IEnumerable GetPrompts() + { + var emotions = "aggression,ambition,anger,anguish,anxiety,astonishment,awe,betrayal,blank,boredom,calm,camaraderie,condescension,contented,contentment,creativity,curiosity,defeat,despair,determination,diligence,disgust,dominance,ecstasy,enchantment,enlightenment,envy,exasperation,exhaustion,exhilaration,fascination,fear,foolishness,gloom,grandiosity,gratitude,greed,grumpiness,guilt,happiness,hatred,hope,hopelessness,hostility,humility,impatience,indignation,irritation,jealousy,loneliness,longing for love,love,loyalty,lust,melancholy,mischievousness,mournful,mourning,nostalgia,obliviousness,overwhelm,passion,perplexity,pitiful,pity,pomposity,pride,rebellion,regret,relief,remorse,resentment,revenge,reverence,sacrifice,sadness,serenity,shame,skepticism,skinship,stress,submission,surprise,sympathy,thrill,transcendence,triumph,trust,unease,unity,valor,veneration,vigilance,vulnerability,yearning,adoration,alienation,anticipation,apathy,bewilderment,catharsis,cynicism,deference,delight,detachment,disillusionment,empowerment,fervor,forlorn,fulfillment,indifference,jubilation,kinship,lethargy,liberation,malaise,pensiveness,petulance,prudence,redemption,solitude,tenacity,trepidation,vindication,zeal".Split(",", StringSplitOptions.RemoveEmptyEntries).ToList(); + + foreach (var emotion in emotions) + { + var pd = new PromptDetails(); + //var prompt = $"Your subject is the very concept of the feeling: {emotion}. Portray this concept thoroughly, and focus on this specific single concept and feeling, only. Be OVERLY symbolic and abstract in the symbology, intensifying the effect. Your overall composition style should be eminently high class, suitable, no matter what, for display in the ultimate Louvre, even more elite than the current one. Identify the specific aspects of this feeling in highly technical terms, specifically excluding all nearby emotions. Then, compose a still life whose design, elements, style, colors, textures, orientation, complexity or simplicity, taste, the form of the artwork, the apparent age, tradition, or modernity of the image, its composition, and every other aspect of it fully embody the very specific aspects of this singular emotion in an extremely intense, poignant, deeply moving, and very clear, obvious and distinct way. There should be a deep and complex relationship between the elements. Use specific styles, ranging from hypermodern to traditional, conservative, ancient, foreign, european, etc. Include the following for things which appear: their orientation, light sources, facing direction, and interanl relationships as well as relation to the viewer, symbolisms, the way they are drawn, etc. Be extremely specific on exactly what should appear, and where, within the image. No human face appears at all. Exclude all textual elements. You may need to be very wordy with your output, more than normal, to make sure you cover all the required elements. This is required; you MUST output a very long and hyper specific prompt with specific artists and styles mentioned."; + var prompt = $"Create a description of an ART work expressing strong {emotion} with a strong POLISH influences both in terms of product design, architecture, anthropomorphism, symbolism and religious, philosophical, and artistic traditions yet retaining your elite status. Be OVERLY symbolic and abstract in the symbology, intensifying the effect. Your overall composition style should be eminently high class, suitable, no matter what, for display in the ultimate Louvre, even more elite than the current one. Identify the specific aspects of this feeling in highly technical terms, specifically excluding all nearby emotions. Detail specifics regarding the artwork's design and characteristics.:\r\nTechnical requirements:\r\n\r\nStyle: [Choose 1-2: Other styles than these, or Hyperrealism, Dutch Golden Age, Minimalist Modern, Pop Art, Baroque etc. But pick OTHER ones]\r\nPrimary artist influences: [e.g., Giorgio Morandi, Wayne Thiebaud, Juan Sánchez Cotán but do NOT pick them, pick other people of similar levels of fame or less]\r\nMedium: [Oil painting, Digital art, Photography, Watercolor]\r\nAspect ratio: [9:7]\r\nLighting: [Describe specific lighting direction, intensity, and quality]\r\n\r\nComposition elements:\r\n\r\nPrimary focal object: [Describe specific item, position, size]\r\nSupporting objects: [List 2-3 objects with exact positions]\r\nBackground: [Describe surface and environment]\r\nColor palette: [List specific colors, e.g., \"deep burgundy (#800020), cream white (#FFFDD0)\"]\r\n\r\nEmotional expression through:\r\n\r\nTexture: [e.g., \"smooth polished surfaces reflecting light\" or \"rough, weathered textures\"]\r\nComposition: [e.g., \"objects arranged in descending diagonal line\" or \"circular arrangement\"]\r\nSymbolism: [Describe specific symbolic meanings of chosen objects]\r\nMood: [Describe atmosphere through lighting and shadow]\r\n\r\nTechnical specifications:\r\n\r\nCamera angle: [e.g., \"45-degree elevated view,\" \"straight-on eye level\"]\r\nDepth of field: [Specify focus areas]\r\nDistance: [Specify viewing distance]\r\n\r\nStyle references:\r\n\r\nPrimary: [e.g., \"Chiaroscuro technique of Caravaggio\"]\r\nSecondary: [e.g., \"Color theory of Josef Albers\"]\r\n\r\nQuality requirements:\r\n\r\nPhotorealistic rendering\r\nHigh detail in textures\r\nSharp focus on key elements\r\nProfessional studio lighting quality\r\n8K resolution\r\nRay-traced shadows and reflections. Overall: do NOT just copy the examples given in this prompt. Generate new unique ones that clearly show {emotion} {emotion} {emotion} "; + pd.ReplacePrompt(prompt, prompt, TransformationType.InitialPrompt); + yield return pd; + } + + } + public override IEnumerable Prompts => GetPrompts().OrderBy(el=>Random.Shared.Next()); + + } +} diff --git a/MultiImageClient/promptGenerators/TPSigns.cs b/MultiImageClient/promptGenerators/TPSigns.cs new file mode 100644 index 0000000..6331008 --- /dev/null +++ b/MultiImageClient/promptGenerators/TPSigns.cs @@ -0,0 +1,43 @@ +using MultiImageClient; + +using System; +using System.Collections.Generic; +using System.Linq; + + + +namespace MultiImageClient +{ + public class TPSigns : AbstractPromptSource + { + public TPSigns(Settings settings) : base(settings) + { + } + + public override string Name => "Emotional Man"; + public override string Prefix => ""; + public override int ImageCreationLimit => 300; + public override int CopiesPer => 3; + public override bool RandomizeOrder => true; + public override int FullyResolvedCopiesPer => 1; + public override string Suffix => ""; + + + private IEnumerable GetPrompts() + { + var signs = "Schlafli, Elon, Society, Lavaslug, Alone, Auryn, Hypernormalization , Nimbus, Hyperparasite , Hypernudge , Tesla, Ys, Hyperborea, Algorithm, Hiraeth, Brussel Sprouts, Nauvis, Tsundoku, Vellichor, Eigengrau, Hygge, Crystal Mentality, Zenzizenzizenzic, Zoom, FPS, Cryotheque, Gradus, Chert, Papua, Nighthawks, Whippoorwill, Tri Arch, Gold Pond, 3d cube, Ziggurat, Monument, Castle, Chasm, Lava Tube, Lilypad, Spiral, Plains, Salt Zone, The Cube, Inca, Pools, Jitty, LA River, Grass Tree, Small Cube, Wickets, Spaghett, World of Molecules, Globular Cluster, Sands of Mars, Monolith, Seaweed, Pretzel, Overpass, Mud Theater, Salt Maze, Start, Orthanc, Crater, Tenochtitlan, Steps of Infinity, Beehive, Pharoah, Helix, Mondrian, Maze, Marsscape, Goldbridge, Barrows, Troll Bridge, Cliffs of Insanity, Subterranean Temple, Fringe, Close, Mazatlan, Tripool, Overlook, Cave of Forgotten Dreams, Balance, Spiral Jump, The Zone, Death Valley, Tangle, Castle Top, Rapunzel, Cave, Mold, Hyperion, Angle of Repose, Shadows, Journey to the Center of the Earth, Jasper, Why, Elevator, Rubble, Race End, Race Start, Cromulent, Inverse, CubeCube, Cloud Chamber, A, B, C, D, Olympus, Kaminarimon, Vesuvius, Summit, Trailhead, Khufu, Ribs, Blanc, Scuba, Bergfried, Husqvarna, Pentekontors, Maobahe, Sputnik, Canals, Excalibur, Mechanus, Weathertop, Waffle, Dawn, Dusk, Noon, Sphere, Pyla, Gnomon, Sundial, Climbing Wall, Armintxe, Dryntimy, Horseshoe, Bombadil, Mauna Loa, Skafloc, Mare, Wolfram, Hamilton, Tipperary, Weir, Obby, Flint, Pachacuti, Fisher, Decker, Polysaccharide, Oxalis, Valgrind, Frigate, Cornu, Avestan, Chaoskampf, Nyx, Heinlein, Dipole, Nabkha, Kaarlo, Lignin, Geosmin, Hasmonean, Aguirre, Saltus, GmbH, Prince Dakkar, Crenellation, Ra, Enguerrand, Atocha, Terminal, Mencius, Ooloi, Claudius, Tybalt, Sutter, World Turtle, Viridian, Kochi, Lutum, Trags, Hosho, Paul, Batatas, Xylem, Schiaparelli, Conch, Phloem, Bretzel, Jordan, Anki, Wozniak, Bogleheads, Klytus, Wilson, Crobuzon, Cinchona, Tapochki, Amber, Aran, Pozzolana, Sciocchi, POGGOD, Ice Cave, Wirtual, STS, Erdos, Junkevin, Erg, Shiny, MarginalRev, Mango, Caldor, Mogwai, Kalzumeus, JuniorEgg, Metamodern, Foundation, Balaji, Phlogistron, Brand, Corbusier, Union, Improv, Kit, Jersey, Lem, Bell, Boustrophedon, Ricardo, Runic, Shed, Banks, Sterling, Succulent, Roblox, Sandking, Frog King, Bergamot, Sejong, Life, Slug, Villeneuve, Verne, Sakura, Osu, Rojo, Cosmo, Vance, Ortho, Aorta, Ende, Pakicetus, Slig, Asana, Socotra, Uniq, Frow, 100mm, Akureyri, Dungen, Eusocial, Formicidae, Fosse, GoldPath, Hollavallagardur, Krystal Cat, Motte, Nosey Parker, Woodingdean, Zimmerman, Consilience, Polk, Carreyrou, Disillusion, Oki, Saws, Rivendell, Mana, Iris, Link, Kew, Moranis, Furin, Haldane, Guru, Asp, Lemna, Mitochondria, Iron Council, Darwin, Huxley, Queequeg, Karolina, Mieville, Remus, Slaad, Romulus, Shetland, Asimov, Honey, Aphid, Ararat, Spanish Steps, E, Petrichor, Kudzu, Orbital, Kermit, Natural Selection, Soylent, Markopolos, Mutation, Clarke, Elemental, Dwarf Fortress, Eno, Barchan, Heretic, IPO, Mandelbrot, Meter, Minesweeper, Mitchell, Nakamura, P vs NP, Rosen, Ryuichi, Roygbiv, Starlink, Seek, Aphex, Poaceae, Black, Macbeth, F, Stickmaster, Yang, Kongzi, Sully, Swiss, Reggie, Shimrod, Bee Swarm, Nautical, Emerald, Voxel, McGuire, Terrain, Hunters, Motherboard, Fungible, Adam, Mortal Coil, Null Island, Point Nemo, Kaladin, Spline, Quidnunc, Joe, Midnight, Rockall, Dusek, Quirk, Yocto, Gate, Noob, Sebeok, Floor, Coagulate, Anlil, Natsu, Supernova, Julia, Tosa, Enki, Kornfeld, Eve, WoofMoo, Tablet, Moonlight, Weatherbottom, Obelisk, Bazooka, Niven, Stepping Stone, Shackleton, Leafy, Loch, Signal, Joist, Lapidem, 888, Penguin, Ice Nine, Rime, Frozen, Hyperquenched, Channel, Square, Construction, Symmetry, Chomik, Pothole, Merely, Slack, Pynchon, Atlantis, Meme, Paleozoic, Studio, Neandertal, Yolk, Lunar, Alpha, Hurdle, Beta, Ardillita, Wiki, SeniorEgg, Mariana Trench, Tiktaalik, Totem, Yap, Thaana, Moire, Bit, Nozzle, Yeager, Antediluvian, Acre, Transparent Radiation, Foam, Pit, Lyonesse, Klondike, Waldo, Metropolitan, Gyrovague, Axolotl, Infotaxis, Game Making Journey, 💀, 人, Waymond, დიუნი, الكثيب, ෴☃❽, Napoleon, Josephine, Parkour, Oobleck, Rheology, 凸, 凹, zzyzx, Neko, O, Chirality, Polysaturated, Humuhumunukunukuapuaa, 👍, 🔥, Perseverance, Defenestrate, Lindebrock, Beluga, Artificial Petrifaction, Varmints, Wordsworth, Lord, Basin, King, Queen, Ring, UGC, Kappa, Xanthophyll, ディズニー, ♦=445,, ♣=446,, ♥=447,, ♠=448,, Landscape, Acrobatics, Contest, Penrose Tiles, Equal Temperament, Magnus, Midjourney, Sophon, Rhadamanthus, ඞ, 007, 65536, Unicycle, Cobweb, Wing, Squelch, Gold Bug, Tanstaafl, CERN, Entity, Arbitrage, Hsu, Perestroika, Kazumura, Unexpectable, RCC, Instrumental Convergence, Hedonic Monster, Brachiolation, Survival, Hyperparameter, Wallfacer, Glasnost, HPMOR, Teotwaki, Jackie, Hide, Skill Susie, Hypergravity, Bolt, Stellar Voyage, Tungsten, Quadruple, Mægæ, Elder, Triple, Village Up North, Ø, Fosbury, Tetromino, Keep Off the Grass, cOld mOld on a sLate pLate, Freedom, Salekhard, Knocking on the Door of Life, Polemic, GPT, Calico, Tern, Easy, Expand, Kalikandjari, Agartha, Based, Zen, Waluigi, Pavement, Ukumbizo, Ripple, Butte, Tryto, Grid, Manifold, Elam, Factorio, Nadir, YTP, Prompt, Chadiesson, Enum, Talladega, Talladega2, 👻, Big, Small, Blossa, Pulse, ataasinngorneq, Дыццӕг, Rebo, Fimmtudagur, 金曜日, Thứ Bảy, 일요일, Jökulhlaup, ►, ◄, 🗯, Gemelo, Cow, Claude, Molière, Éliante, Erewhon, Polytropon, DNA, Prefontaine ".Split(",", StringSplitOptions.RemoveEmptyEntries).ToList(); + + foreach (var emotion in signs) + { + var pd = new PromptDetails(); + //var prompt = $"Your subject is the very concept of the feeling: {emotion}. Portray this concept thoroughly, and focus on this specific single concept and feeling, only. Be OVERLY symbolic and abstract in the symbology, intensifying the effect. Your overall composition style should be eminently high class, suitable, no matter what, for display in the ultimate Louvre, even more elite than the current one. Identify the specific aspects of this feeling in highly technical terms, specifically excluding all nearby emotions. Then, compose a still life whose design, elements, style, colors, textures, orientation, complexity or simplicity, taste, the form of the artwork, the apparent age, tradition, or modernity of the image, its composition, and every other aspect of it fully embody the very specific aspects of this singular emotion in an extremely intense, poignant, deeply moving, and very clear, obvious and distinct way. There should be a deep and complex relationship between the elements. Use specific styles, ranging from hypermodern to traditional, conservative, ancient, foreign, european, etc. Include the following for things which appear: their orientation, light sources, facing direction, and interanl relationships as well as relation to the viewer, symbolisms, the way they are drawn, etc. Be extremely specific on exactly what should appear, and where, within the image. No human face appears at all. Exclude all textual elements. You may need to be very wordy with your output, more than normal, to make sure you cover all the required elements. This is required; you MUST output a very long and hyper specific prompt with specific artists and styles mentioned."; + var prompt = $"Your Subject is: '{emotion.Trim()}'"; + pd.ReplacePrompt(prompt, prompt, TransformationType.InitialPrompt); + yield return pd; + } + + } + public override IEnumerable Prompts => GetPrompts().OrderBy(el=>Random.Shared.Next()); + + } +} diff --git a/MultiImageClient/promptGenerators/WriteHere.cs b/MultiImageClient/promptGenerators/WriteHere.cs index 173bfa0..4c3777a 100644 --- a/MultiImageClient/promptGenerators/WriteHere.cs +++ b/MultiImageClient/promptGenerators/WriteHere.cs @@ -1,72 +1,75 @@ -using CommandLine; - -using Microsoft.VisualBasic; -using MultiImageClient.Implementation; -using Newtonsoft.Json.Linq; - -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics.Metrics; -using System.Drawing.Drawing2D; -using System.IO; -using System.Linq; -using System.Net; -using System.Reflection; -using System.Reflection.Metadata.Ecma335; -using System.Runtime.ConstrainedExecution; -using System.Runtime.InteropServices; -using System.Security.Cryptography; -using System.Threading; -using System.Xml.Linq; - -using static System.Formats.Asn1.AsnWriter; - -namespace MultiImageClient -{ - /// If you have a file of a bunch of prompts, you can use this to load them rather than using some kind of custom iteration system. - public class WriteHere : AbstractPromptGenerator - { - - public WriteHere(Settings settings) : base(settings) - { - } - public override string Name => nameof(WriteHere); - - public override int ImageCreationLimit => 350; - public override int CopiesPer => 4; - public override bool RandomizeOrder => true; - public override string Prefix => ""; - public override IEnumerable Variants => new List { "" }; - public override string Suffix => ""; - public override Func CleanPrompt => (arg) => arg.Trim().Trim(); - public override bool AlsoDoVersionSkippingClaude => true; - public override bool UseIdeogram => false; - public override bool UseClaude => true; - - private IEnumerable GetPrompts() - { - var prompts = new List() { - "A completely normal light switch for a 1990s american north eastern area wall, in the 'on' position", - "a hybrid cat-turtle creature enjoying sunning herself on a log.", - "A modern building with a reflective glass facade. The building's reflection captures another tower, possibly a skyscraper. The image has text overlay that reads 'How Buildings Learn' written in white font against a dark background. The text 'written and presented by Stewart Brand' is also visible at the bottom.", - "Close-up of a super clear and sharp photo of a perfectly preserved tablet covered with many ancient, alien styles of kanji characters created with the finest jewels, obsidian, intensely emotional and creative, in a semi-grid, clean and pure incredible macro photograph.", - "The streets of Singapore are bustling with people and various street food stalls, creating a lively and multicultural atmosphere. The area is full of guava, cheese, broccoli, kimchi, hardboiled eggs, vinegar, and durian. Each smell seems to waft through the crowd attacking people and animals who are particularly weak to it, striking them with brutal overwhelming negative sensations. This is a biological weapon attack! the image is a schema overhead view with detailed performance analysis\r\nA light switch that is clearly in the \"ON\" not \"OFF\" position. Please describe this close-up 3d depth image in high detail exactly describing the light switch panel and its material and color, the switch which sticks out, its position and what the position represents.", - "A 4x4 grid of super magiscule, block font dense intense incredibly meaningful, profound, ethereal, and subtle KANJI characters. The characters are illustrated in super high resolution, partially 3d style, feeling like they almost emerge from the flat screen, in a super clear image utilizing one or more of: subtle coloration, unusual line thicknesses or variations, unusual kerning, pen and ink, hand-drawn custom artisinal creative characters, and/or super evocative, personalized textures.", - "Introducing the \"Rogue\" card, an exciting new addition between the Queen and Jack of Diamonds in a standard playing card deck. The Rogue card features a dashing figure with a masked face, holding a small, curved dagger. The figure is adorned with a cape and a diamond-encrusted hat, symbolizing the connection to the Diamonds suit. The single-letter symbol for the Rogue card is \"X\", which is displayed in each corner along with the suit of Diamonds. The card's design maintains the simplicity of traditional playing cards, with the \"X\" and diamonds in the corners and a captivating, yet minimalist, pattern in the middle, featuring a subtle diamond-like motif. The Rogue card captures the essence of cunning and deception, adding an exciting twist to the classic deck.\r\n\U0001f955 attacking 🐢 with \U0001f98a", -"An immense tower of magical prortions, ancient, surrounded by a barren environment with just a hint of change and evolution, incredibly deep depth of field, epic ancient", - - }; - foreach (var prompt in prompts) - { - var pd = new PromptDetails(); - pd.ReplacePrompt(prompt, prompt, TransformationType.InitialPrompt); - pd.OverrideFilename = prompt; - yield return pd; - } - } - public override IEnumerable Prompts => GetPrompts(); - } -} - +using CommandLine; + +using Microsoft.VisualBasic; + +using MultiImageClient; + +using Newtonsoft.Json.Linq; + +using RecraftAPIClient; + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.Metrics; +using System.Drawing.Drawing2D; +using System.IO; +using System.Linq; +using System.Net; +using System.Numerics; +using System.Reflection; +using System.Reflection.Metadata.Ecma335; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using System.Threading; +using System.Xml.Linq; + +using static System.Formats.Asn1.AsnWriter; + +namespace MultiImageClient +{ + /// If you have a file of a bunch of prompts, you can use this to load them rather than using some kind of custom iteration system. + public class WriteHere : AbstractPromptSource + { + + public WriteHere(Settings settings) : base(settings) + { + } + public override string Name => nameof(WriteHere); + + public override int ImageCreationLimit => 350; + public override int CopiesPer => 4; + public override bool RandomizeOrder => true; + public override string Prefix => ""; + public override string Suffix => ""; + + private IEnumerable GetPrompts() + { + List prompts = new List +{ + "A highly detailed digital painting, warmly lit, showing Charles—an extraordinarily thin young man with messy black hair—inside his cramped studio apartment at night, facing an old, grandfatherly figure in a red-and-white Santa suit who just appeared unexpectedly. Locks on the apartment door and simple, industrial-themed furnishings are visible, emphasizing the surreal intrusion of the old Santa amid the mundane setting.", + "An intricate illustration of the North Pole’s List Room: a vast, stadium-like interior lined with tens of thousands of polished wooden desks in concentric rings. Enormous scrolls of paper hang from the high domed ceiling, each so thick they coil around metal bars. Scores of pointed-eared elves—dressed in red, green, and gold—hunch over their desks, writing and scrutinizing three-dimensional viewers that float above the angled surfaces, all under a diffuse, magical glow.", + "A richly detailed image of Kelvin, a head elf with golden epaulettes and a serious expression, guiding Charles through the List Room. They stand at one particular desk, angled like an architect’s drafting table, where a flat, crystal-clear viewer shows a child walking along a city sidewalk. The elves around them wear festive but restrained outfits of red and green, and behind them loom the monumental scrolls and countless workstations.", + "A large workshop filled with warm lamplight and rows of sturdy wooden benches, each bearing a strange metal iris connected to a glass pipe filled with gray magical ‘dough.’ Elves in festive attire carefully shape the dough into children’s gifts—a small plastic frog in one case—by hand. Tools, ribbons, and bright wrapping papers are scattered about, with half-finished trinkets and toys lending a tactile, handcrafted atmosphere.", + "A meticulously rendered stable scene, where elegant, strong-limbed reindeer stand in neat rows, their antlers adorned with subtle silver bells. The wooden stable walls are hung with wreaths and holly. At the center, the gleaming red sleigh awaits, its runners reflecting the straw and hay below, while elves tend lovingly to the creatures and ensure everything is perfectly prepared.", + "A grand set of private chambers designed for Santa: luxurious, slightly gaudy furnishings, plush rugs, and carved wood accents. Among these comforts stands Charles, now wearing the red-and-white suit that still doesn’t quite fit. Beside him is Matilda, a petite elf with long black hair, offering him exquisite notebooks and pens. On a side table is the mechanical viewer, dials and levers gleaming, ready to reveal the Earth from orbit.", + "Inside the workshop again, this time focusing closely on a single elf holding a shape-shifting ball of gray dough, trying to form an impossible gift that would maximize a child’s happiness. The elf’s brow is furrowed in concentration, the device half-transformed into something ominous. In the background, Charles stands horrified, notebooks in hand, as colorful presents and decorations create a bizarre contrast to his moral dilemma.", + "A dramatic nighttime rooftop scene in some metropolitan area on Earth. Two indistinguishable copies of Charles—both strong and fluid in motion—confront James, the old Santa now in a human disguise. The air crackles with tension. Below them, city lights and confused pedestrians blur. The style remains painterly and detailed, capturing the swift, inhuman fight through elongated shadows and dynamic poses.", + "A vast emptiness where the North Pole once stood: the cavernous List Room and workshops are gone, leaving only a blank white expanse with faint echoes of candy canes, wreaths, and gold trim. A subtle shimmer in the air hints that the elves, once so numerous and devout, have departed entirely. The atmosphere is both peaceful and melancholic, shafts of pale light accenting the pristine emptiness.", + "A serene, intimate nighttime bedroom in rural China. A small girl, Li Xiu Yang, lies asleep in a simple bed. Charles, now appearing gentler and more resolved, stands beside her, pressing a tiny silver marble to her forehead. Outside the window, the moonlight bathes quiet rooftops. The style remains consistent and painterly, every detail—her soft blanket, modest furniture, a stray toy on the floor—rendered lovingly as he bestows this final, hopeful gift." +}; + foreach (var prompt in prompts) + { + var pd = new PromptDetails(); + pd.ReplacePrompt(prompt, prompt, TransformationType.InitialPrompt); + yield return pd; + } + } + public override IEnumerable Prompts => GetPrompts().OrderBy(el => Random.Shared.Next()); + } +} + + diff --git a/MultiImageClient/promptTransformation/ClaudeRewriteStep.cs b/MultiImageClient/promptTransformation/ClaudeRewriteStep.cs index 7a8bbc1..47686c7 100644 --- a/MultiImageClient/promptTransformation/ClaudeRewriteStep.cs +++ b/MultiImageClient/promptTransformation/ClaudeRewriteStep.cs @@ -1,47 +1,52 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MultiImageClient.Implementation; - -namespace MultiImageClient -{ - public class ClaudeRewriteStep : ITransformationStep - { - private string _prefix { get; set; } - private string _suffix { get; set; } - private ClaudeService _claudeService { get; set; } - private decimal _temperature { get; set; } - - public string Name => nameof(ClaudeRewriteStep); - - public ClaudeRewriteStep(string prefix, string suffix, ClaudeService svc, decimal temperature) - { - _claudeService = svc; - _prefix = prefix; - _suffix = suffix; - _temperature = temperature; - } - - public async Task DoTransformation(PromptDetails pd, MultiClientRunStats stats) - { - var preparedClaudePrompt = $"{_prefix}\nHere is the topic:\n'{pd.Prompt}'\n{_suffix}".Trim(); - var preparedClaudePromptWithTemp = $"temp={_temperature} {preparedClaudePrompt}"; - pd.ReplacePrompt(preparedClaudePrompt, preparedClaudePromptWithTemp, TransformationType.ClauedeRewriteRequest); - - var response = await _claudeService.RewritePromptAsync(pd, stats, _temperature); - if (!response.IsSuccess) - { - Console.WriteLine($"\tClaude fail so reverting FROM: {pd.Show()}"); - pd.UndoLastStep(); - //pd.UndoLastStep(); - Console.WriteLine($"\tClaude fail so reverted TO: {pd.Show()}"); - pd.ReplacePrompt(pd.Prompt, response.ErrorMessage, TransformationType.ClaudeDidRefuseRewrite); - - return false; - } - return true; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + + +namespace MultiImageClient +{ + public class ClaudeRewriteStep : ITransformationStep + { + //var claudeStep = new ClaudeRewriteStep("Please take the following topic and make it specific; cast the die, take a chance, and expand it to a longer, detailed, specific description of a scene with all the elements of it described. Describe how the thing looks, feels, appears, etc in high detail. Put the most important aspects first such as the overall description, then continue by expanding that and adding more detail, structure, theme. Be specific in whatevr you do. If it seems appropriate, if a man appears don't just say 'the man', but instead actually give him a name, traits, personality, etc. The goal is to deeply expand the world envisioned by the original topic creator. Overall, follow the implied theem and goals of the creator, but just expand it into much more specifics and concreate actualization. Never use phrases or words like 'diverse', 'vibrant' etc. Be very concrete and precise in your descriptions, similar to how ansel adams describing a new treasured species of bird would - detailed, caring, dense, clear, sharp, speculative and never wordy or fluffy. every single word you say must be relevant to the goal of increasing the info you share about this image or sitaution or scene. Be direct and clear.", "", claudeService, 0.4m, stats); + + private string _prefix { get; set; } + private string _suffix { get; set; } + private ClaudeService _claudeService { get; set; } + private decimal _temperature { get; set; } + private MultiClientRunStats _stats { get; set; } + + public string Name => nameof(ClaudeRewriteStep); + + /// you need to put the instructions to claude into the prefix and/or suffix. generally things like "based on this idea, expand it quite a bit" + public ClaudeRewriteStep(string prefix, string suffix, ClaudeService svc, decimal temperature, MultiClientRunStats stats) + { + _claudeService = svc; + _prefix = prefix; + _suffix = suffix; + _temperature = temperature; + _stats = stats; + } + + public async Task DoTransformation(PromptDetails pd) + { + var preparedClaudePrompt = $"{_prefix}\nHere is the topic:\n'{pd.Prompt}'\nPlease reply in a paragraph with no line breaks at all, just a single unified paragraph text.{_suffix}".Trim(); + var preparedClaudePromptWithTemp = $"temp={_temperature} {preparedClaudePrompt}"; + pd.ReplacePrompt(preparedClaudePrompt, preparedClaudePromptWithTemp, TransformationType.ClaudeRewriteRequest); + + var response = await _claudeService.RewritePromptAsync(pd, _temperature); + if (!response.IsSuccess) + { + Logger.Log($"\tClaude fail so reverting FROM: {pd.Show()}"); + pd.UndoLastStep(); + //pd.UndoLastStep(); + Logger.Log($"\tClaude fail so reverted TO: {pd.Show()}"); + pd.ReplacePrompt(pd.Prompt, response.ErrorMessage, TransformationType.ClaudeDidRefuseRewrite); + + return false; + } + return true; + } + } +} diff --git a/MultiImageClient/promptTransformation/IImageGenerator.cs b/MultiImageClient/promptTransformation/IImageGenerator.cs new file mode 100644 index 0000000..e471630 --- /dev/null +++ b/MultiImageClient/promptTransformation/IImageGenerator.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + /// + /// you can configure and create it (with a _service). + /// so say you want a "landscape anime" image maker based on bfl and another like square and another depending on what claude thinks. you just make one generator for each then as you get your prompts, you send it in. + /// you can make them all at first or as you go. The point is that we preconfigure everything? + /// a thing you can send a prompt to, and get back a TaskProcessResult + /// + /// so, they should have already been created with the options set how you'd like them to be sent out. + /// + public interface IImageGenerator + { + public abstract ImageGeneratorApiType ApiType { get; } + public abstract string GetFilenamePart(PromptDetails pd); + + /// return just the parts on the right, we know everything else. + public abstract List GetRightParts(); + + // This is yet another type of description of the generator. Not the filename, not the full details. And not the name... but why not? yes name if provided does supercede. So if no name, default to something senseible to describe API/company/remote endpoint style + meaningful config options + public abstract string GetGeneratorSpecPart(); + + /// I suppose we should tell/show users how much images cost. + public abstract decimal GetCost(); + + public abstract Task ProcessPromptAsync(IImageGenerator generator, PromptDetails promptDetails); + + /// when this jobspec is run, how should it be mapped to the filename? + } +} + diff --git a/MultiImageClient/promptTransformation/ImageCombiner.cs b/MultiImageClient/promptTransformation/ImageCombiner.cs new file mode 100644 index 0000000..6489121 --- /dev/null +++ b/MultiImageClient/promptTransformation/ImageCombiner.cs @@ -0,0 +1,950 @@ +#nullable enable +using IdeogramAPIClient; + +using ImageMagick; + +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System.Text.Json; +using System.Text.Json.Serialization; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Diagnostics; + + +using GenerativeAI.Types.RagEngine; + +namespace MultiImageClient +{ + // ImageCombiner creates visual comparison and documentation layouts by combining multiple images into a single output. + // + // Main functionality: + // 1. Batch Layout Images: Combines multiple generated images from different image generators into horizontal square grid layouts, + // with labels showing which generator produced each image and the original prompt used. + // + // 2. Roundtrip Layout Images: Creates comprehensive visual documentation of the image description ? regeneration workflow. + // Shows the full pipeline: original image ? describer instructions ? generated description ? regenerated images. + // This layout displays: + // - The original input image + // - The instructions/prompts given to the describer model + // - The description text output by the describer model + // - All images regenerated from that description by various image generators + // All sections are labeled with the relevant model names (e.g., "Qwen2-VL Description", "InternVL Instructions", etc.) + // + // 3. Smart Text Sizing: Automatically calculates optimal font sizes to fit description text and instructions within their panels, + // using binary search to maximize readability while ensuring text fits in the available space. + // + // 4. Error Handling: Displays error placeholders for failed image generations with descriptive error messages. + // + // All combined images are saved to disk in dated folders and automatically opened in the default image viewer. + public static class ImageCombiner + { + private const int CombinedImageGeneratorFontSize = 12; + private const int CombinedImagePromptFontSize = 16; + private const int GeneratedImageLabelFontSize = 18; + private const float LabelLineHeightMultiplier = 1.5f; + private const int PlaceholderWidth = 300; + private const int LabelFontSize = 12; + + /// Fraction of the generator-label font size added as extra + /// descender reservation so letters with descenders (g, p, y) + /// are never clipped by the band below. + private const float LabelDescenderReserveFraction = 0.25f; + + + private static IEnumerable LoadResultImages(IEnumerable results) + { + var loadedImages = new List(); + + foreach (var result in results.Where(el => el.IsSuccess)) + { + try + { + foreach (var imageBytes in result.GetAllImages) + { + if (imageBytes != null && imageBytes.Length > 0) + { + var image = Image.Load(imageBytes); + + // Resize if height > 1300px, keeping proportions and max height of 1024px + if (image.Height > 1300) + { + var aspectRatio = (float)image.Width / image.Height; + var newHeight = 1024; + var newWidth = (int)(newHeight * aspectRatio); + + Logger.Log($"Resizing image from {image.Width}x{image.Height} to {newWidth}x{newHeight} for combining"); + image.Mutate(x => x.Resize(newWidth, newHeight)); + } + + loadedImages.Add(new LoadedImage + { + Success = true, + Result = result.IsSuccess ? "successX" : result.ErrorMessage ?? "missing error", + Generator = result.ImageGeneratorDescription, + TimingLabel = FormatTiming(result.CreateTotalMs + result.DownloadTotalMs), + Image = image, + Width = image.Width, + Height = image.Height + }); + } + else + { + // Success but no bytes somehow + Logger.Log($"No image bytes for successful result from {result.ImageGenerator}"); + loadedImages.Add(GetPlaceholder(result)); + } + } + } + catch (Exception ex) + { + // GetImageBytes() might throw if bytes weren't set, or image loading failed + Logger.Log($"Failed to get/load image from {result.ImageGenerator}: {ex.Message}"); + loadedImages.Add(GetPlaceholder(result)); + } + } + + // Failed result - show placeholder with error + foreach (var result in results.Where(el => !el.IsSuccess)) + { + + loadedImages.Add(GetPlaceholder(result)); + } + + return loadedImages.OrderBy(el => el.Generator); + } + + private static Image RenderHorizontalLayout(IEnumerable loadedImages, Font generatorFont) + { + int totalWidth = loadedImages.Sum(img => img.Width); + totalWidth = Math.Max(totalWidth, PlaceholderWidth); + + int maxImageHeight = loadedImages.Select(i => i.Height) + .DefaultIfEmpty(PlaceholderWidth) + .Max(); + + int subtitleHeight = MeasureImageSubdescriptionHeight(loadedImages, generatorFont); + int subtitleBlockHeight = Math.Max(UIConstants.Padding * 2, subtitleHeight + UIConstants.Padding * 2); + int totalHeight = maxImageHeight + subtitleBlockHeight; + + var layoutImage = ImageUtils.CreateStandardImage(totalWidth, totalHeight, UIConstants.White); + + var timingFont = FontUtils.CreateFont(Math.Max(8, generatorFont.Size * 0.75f), FontStyle.Regular); + + layoutImage.Mutate(ctx => + { + int currentX = 0; + + foreach (var li in loadedImages) + { + if (li.Success && li.Image != null) + { + ctx.DrawImage(li.Image, new Point(currentX, 0), 1f); + } + else + { + var rect = new RectangleF(currentX, 0, li.Width, maxImageHeight); + var errorText = li.Result; + ctx.DrawErrorPlaceholder(rect, errorText, generatorFont); + } + + float centerX = currentX + li.Width / 2f; + float labelY = maxImageHeight + UIConstants.Padding; + + var labelColor = li.Success ? UIConstants.SuccessGreen : UIConstants.ErrorRed; + DrawCompositeLabelLine(ctx, li.Generator, li.TimingLabel, generatorFont, timingFont, labelColor, centerX, labelY); + + currentX += li.Width; + } + }); + + return layoutImage; + } + + + + /// Renders a generator label as a single horizontal line with a clear + /// typographic hierarchy: + /// + /// [primary font (bold)] gpt-image-2 [secondary font] medium square 1m 4s + /// + /// The first whitespace-delimited token of + /// (typically the model id) stays in the primary font; the rest of + /// the label plus the optional timing ("1m 4s") collapses into one + /// secondary-font string drawn to its right. All pieces share a + /// single bottom pixel (descenders included) so nothing sits visually + /// lower than anything else. Color comes from + /// � no gray text, per AGENTS.md. + private static void DrawCompositeLabelLine( + IImageProcessingContext ctx, + string fullLabel, + string? timingLabel, + Font primaryFont, + Font secondaryFont, + Color color, + float centerX, + float labelY) + { + if (string.IsNullOrWhiteSpace(fullLabel) && string.IsNullOrWhiteSpace(timingLabel)) return; + + // Split: first whitespace-delimited token is the primary piece + // (kept in the big bold font); everything after is secondary. + // Extra whitespace in the source is normalized to exactly two + // spaces between pieces in the rendered output. + var trimmed = (fullLabel ?? string.Empty).Trim(); + string primary; + var secondaryPieces = new List(); + int firstSpace = trimmed.IndexOf(' '); + if (firstSpace < 0) + { + primary = trimmed; + } + else + { + primary = trimmed.Substring(0, firstSpace); + var rest = trimmed.Substring(firstSpace + 1); + secondaryPieces.AddRange(rest.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + } + if (!string.IsNullOrWhiteSpace(timingLabel)) + { + secondaryPieces.Add(timingLabel); + } + string secondary = string.Join(" ", secondaryPieces); + + // Measure each piece independently so we can lay them out on one + // shared baseline. + var primaryMeasureOpts = FontUtils.CreateTextOptions(primaryFont, HorizontalAlignment.Left, VerticalAlignment.Top, UIConstants.LineSpacing); + primaryMeasureOpts.Origin = new PointF(0, 0); + var primaryBounds = string.IsNullOrEmpty(primary) + ? new FontRectangle(0, 0, 0, 0) + : TextMeasurer.MeasureBounds(primary, primaryMeasureOpts); + + float gap = secondaryPieces.Count == 0 || string.IsNullOrEmpty(primary) + ? 0f + : Math.Max(8f, primaryFont.Size * 0.6f); + + float secondaryWidth = 0f; + FontRectangle secondaryBounds = new FontRectangle(0, 0, 0, 0); + if (!string.IsNullOrEmpty(secondary)) + { + var secondaryMeasureOpts = FontUtils.CreateTextOptions(secondaryFont, HorizontalAlignment.Left, VerticalAlignment.Top, UIConstants.LineSpacing); + secondaryMeasureOpts.Origin = new PointF(0, 0); + secondaryBounds = TextMeasurer.MeasureBounds(secondary, secondaryMeasureOpts); + secondaryWidth = secondaryBounds.Width; + } + + float totalWidth = primaryBounds.Width + gap + secondaryWidth; + float startX = centerX - totalWidth / 2f; + + // Draw primary first at (startX, labelY) with Top alignment so + // labelY is the top of its ascenders. Then use its actual bottom + // pixel as the shared baseline for the secondary text. + float sharedBottom = labelY + primaryBounds.Height; + if (!string.IsNullOrEmpty(primary)) + { + var primaryDrawOpts = FontUtils.CreateTextOptions(primaryFont, HorizontalAlignment.Left, VerticalAlignment.Top, UIConstants.LineSpacing); + primaryDrawOpts.Origin = new PointF(startX, labelY); + ctx.DrawTextStandard(primaryDrawOpts, primary, color); + var drawnBounds = TextMeasurer.MeasureBounds(primary, primaryDrawOpts); + sharedBottom = drawnBounds.Bottom; + } + + if (!string.IsNullOrEmpty(secondary)) + { + var secondaryDrawOpts = FontUtils.CreateTextOptions(secondaryFont, HorizontalAlignment.Left, VerticalAlignment.Bottom, UIConstants.LineSpacing); + secondaryDrawOpts.Origin = new PointF(startX + primaryBounds.Width + gap, sharedBottom); + ctx.DrawTextStandard(secondaryDrawOpts, secondary, color); + } + } + + private static Image RenderSquareLayout(IEnumerable loadedImages, Font generatorFont) + { + int totalImages = loadedImages.Count(); + + if (totalImages == 0) + { + return RenderEmptyLayout(); + } + + int columns = Math.Max(1, (int)Math.Ceiling(Math.Sqrt(totalImages))); + int rows = (int)Math.Ceiling(totalImages / (double)columns); + + var columnWidths = new int[columns]; + var rowHeights = new int[rows]; + + var index = 0; + foreach (var li in loadedImages) + { + int column = index % columns; + int row = index / columns; + + columnWidths[column] = Math.Max(columnWidths[column], li.Width); + rowHeights[row] = Math.Max(rowHeights[row], li.Height); + index++; + } + + int layoutWidth = Math.Max(columnWidths.Sum(), PlaceholderWidth); + + var columnOffsets = new int[columns]; + int xAccumulator = 0; + for (int col = 0; col < columns; col++) + { + columnOffsets[col] = xAccumulator; + xAccumulator += columnWidths[col]; + } + + // every image has its own description section + int imageSubdescriptionHeight = MeasureImageSubdescriptionHeight(loadedImages, generatorFont); + int subdescripitonHeight = Math.Max(UIConstants.Padding * 2, imageSubdescriptionHeight + UIConstants.Padding * 2); + int layoutHeight = rowHeights.Sum() + subdescripitonHeight * rows; + + var layoutImage = ImageUtils.CreateStandardImage(layoutWidth, layoutHeight, UIConstants.White); + + var timingFont = FontUtils.CreateFont(Math.Max(8, generatorFont.Size * 0.75f), FontStyle.Regular); + + layoutImage.Mutate(ctx => + { + int currentY = 0; + int imageIndex = 0; + + for (int row = 0; row < rows; row++) + { + int rowHeight = rowHeights[row]; + int labelY = currentY + rowHeight + UIConstants.Padding; + + for (int col = 0; col < columns; col++) + { + if (imageIndex >= totalImages) + { + break; + } + + var li = loadedImages.ToList()[imageIndex]; + int columnWidth = columnWidths[col]; + int columnX = columnOffsets[col]; + + int imageX = columnX + Math.Max(0, (columnWidth - li.Width) / 2); + int imageY = currentY; + + if (li.Success && li.Image != null) + { + ctx.DrawImage(li.Image, new Point(imageX, imageY), 1f); + } + else + { + var rect = new RectangleF(columnX, imageY, Math.Max(columnWidth, 1), Math.Max(rowHeight, 1)); + var errorText = li.Result; + ctx.DrawErrorPlaceholder(rect, errorText, generatorFont); + } + + float centerX = columnX + Math.Max(columnWidth, 1) / 2f; + var labelColor = li.Success ? UIConstants.SuccessGreen : UIConstants.ErrorRed; + DrawCompositeLabelLine(ctx, li.Generator, li.TimingLabel, generatorFont, timingFont, labelColor, centerX, labelY); + + imageIndex++; + } + + currentY += rowHeight + subdescripitonHeight; + } + }); + + return layoutImage; + } + + public static async Task CreateBatchLayoutImageHorizontalAsync(IEnumerable results, string prompt, Settings settings) + { + var generatorFont = FontUtils.CreateFont(CombinedImageGeneratorFontSize, FontStyle.Bold); + var promptFont = FontUtils.CreateFont(CombinedImagePromptFontSize, FontStyle.Regular); + + var loadedImages = LoadResultImages(results); + + using var layoutImage = RenderHorizontalLayout(loadedImages, generatorFont); + var totalWidth = layoutImage.Width; + + using var promptPanel = RenderPromptPanel(totalWidth, prompt, promptFont); + + int totalHeight = layoutImage.Height + promptPanel.Height; + var combinedImage = ImageUtils.CreateStandardImage(layoutImage.Width, totalHeight, UIConstants.White); + + combinedImage.Mutate(ctx => + { + ctx.DrawImage(layoutImage, new Point(0, 0), 1f); + ctx.DrawImage(promptPanel, new Point(0, layoutImage.Height), 1f); + }); + + + var outputPath = await SaveCombinedImageToDisk(combinedImage, prompt, settings); + + foreach (var li in loadedImages) + { + li.Image?.Dispose(); + } + + OpenImageWithDefaultApplication(outputPath); + + return outputPath; + } + + // openWhenDone controls whether the combined grid image is popped + // open in the system default viewer after it's written. Default true + // preserves the batch/round-trip workflow behavior. Callers that want + // headless behavior (e.g. the REPL, where viewer popups would spam + // the desktop) pass false. + public static async Task CreateBatchLayoutImageSquareAsync(IEnumerable results, string prompt, Settings settings, bool openWhenDone = true) + { + var generatorFont = FontUtils.CreateFont(CombinedImageGeneratorFontSize, FontStyle.Bold); + var promptFont = FontUtils.CreateFont(CombinedImagePromptFontSize, FontStyle.Regular); + + var loadedImages = LoadResultImages(results); + + using var layoutImage = RenderSquareLayout(loadedImages, generatorFont); + using var promptPanel = RenderPromptPanel(layoutImage.Width, prompt, promptFont); + + int totalHeight = layoutImage.Height + promptPanel.Height; + var combinedImage = ImageUtils.CreateStandardImage(layoutImage.Width, totalHeight, UIConstants.White); + + combinedImage.Mutate(ctx => + { + ctx.DrawImage(layoutImage, new Point(0, 0), 1f); + ctx.DrawImage(promptPanel, new Point(0, layoutImage.Height), 1f); + }); + + + var outputPath = await SaveCombinedImageToDisk(combinedImage, prompt, settings); + + foreach (var li in loadedImages) + { + li.Image?.Dispose(); + } + + if (openWhenDone) + { + OpenImageWithDefaultApplication(outputPath); + } + + return outputPath; + } + + + // this is how werender the "roundtrip workflow". In this case, the user provided an image to us. + // we used an LLM to describe it, then we sent that description to a bunch of images. + // the output format should be a long horizontal strip. Leftmost should be the image, labelled like "original iamge". + // then to the right of that, how it was described, in descriptionText taking up a big square column (sine this text might be long). + // then further to the right, in order, just like we did in RenderHorizontalLayout, should be all the output images ortheir error placeholders. + public async static Task CreateRoundtripLayoutImageAsync(byte[] originalImageBytes, IEnumerable results, string descriptionText, string descriptionInstructions, string describerModelName, Settings settings) + { + var labelFont = FontUtils.CreateFont(36, FontStyle.Bold); // Large font for labels + var originalImage = Image.Load(originalImageBytes); + if (originalImage == null) + { + throw new Exception("no original image."); + } + + // Resize original image if height > 1024px, keeping proportions + if (originalImage.Height > 1024) + { + var aspectRatio = (float)originalImage.Width / originalImage.Height; + var newHeight = 1024; + var newWidth = (int)(newHeight * aspectRatio); + + Logger.Log($"Resizing original image from {originalImage.Width}x{originalImage.Height} to {newWidth}x{newHeight} for combining"); + originalImage.Mutate(x => x.Resize(newWidth, newHeight)); + } + + var maxImageHeight = originalImage.Height; + var descriptionWidth = 1024; // Fixed width for description + var descriptionHeight = maxImageHeight; // Description takes up the same height as the original image. + var loadedImages = LoadResultImages(results).ToList(); + + // Actually measure how much space each label needs + var tempLabelFont = FontUtils.CreateFont(36, FontStyle.Bold); + + // Measure each label text height, accounting for wrapping + var labelHeights = new List(); + foreach (var li in loadedImages) + { + var labelText = li.Result; + if (!string.IsNullOrEmpty(labelText)) + { + // Handle newlines properly - they increase height + var wrappingWidth = li.Width - 10; // Account for padding + var labelHeight = ImageUtils.MeasureTextHeight(labelText, tempLabelFont, UIConstants.LineSpacing, wrappingWidth); + labelHeights.Add(labelHeight); + } + else + { + labelHeights.Add(0); + } + } + + // Also measure the "Original Image" and "Description" labels + var originalLabelHeight = ImageUtils.MeasureTextHeight("Original Image", tempLabelFont, UIConstants.LineSpacing, originalImage.Width - 10); + var descriptionLabelHeight = ImageUtils.MeasureTextHeight("Description", tempLabelFont, UIConstants.LineSpacing, descriptionWidth - 10); + + // Find the maximum label height needed + var maxLabelHeight = Math.Max(originalLabelHeight, descriptionLabelHeight); + if (labelHeights.Any()) + { + maxLabelHeight = Math.Max(maxLabelHeight, labelHeights.Max()); + } + + Logger.Log($"Max label height needed: {maxLabelHeight}px (original: {originalLabelHeight}, description: {descriptionLabelHeight}, generated images: {string.Join(",", labelHeights)})"); + + // Calculate total width and height + var instructionsWidth = 1024; // Same width as description + var totalWidth = originalImage.Width + instructionsWidth + descriptionWidth + loadedImages.Sum(li => li.Width); + var totalHeight = maxImageHeight + maxLabelHeight + UIConstants.Padding * 2; + + var layoutImage = new Image(totalWidth, totalHeight); + + layoutImage.Mutate(ctx => + { + ctx.ApplyStandardGraphicsOptions(); + ctx.Fill(UIConstants.White); + float currentX = 0; + + // 1. Draw Original Image + ctx.DrawImage(originalImage!, new Point((int)currentX, 0), 1f); + var labelOptsOriginal = new RichTextOptions(labelFont) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Top + }; + labelOptsOriginal.Dpi = UIConstants.TextDpi; // Ensure same DPI as measurement! + labelOptsOriginal.Origin = new PointF(currentX + originalImage!.Width / 2f, maxImageHeight + UIConstants.Padding); + ctx.DrawTextStandard(labelOptsOriginal, "Original Image", Color.Black); + currentX += originalImage.Width; + + // 2a. Draw describer instructions + var instructionsWidth = 1024; // Same width as description + var instructionsHeight = maxImageHeight; // Same height as description + + if (!string.IsNullOrEmpty(descriptionInstructions)) + { + // Calculate optimal font size for instructions text + var instrTargetUtilization = 0.95; + var instrAvailableHeight = instructionsHeight - 2 * UIConstants.Padding; + var instrWrappingWidth = instructionsWidth - 2 * UIConstants.Padding; + var instrTargetHeight = instrAvailableHeight * instrTargetUtilization; + + var instrNewlineCount = descriptionInstructions.Count(c => c == '\n'); + + var instrMinFontSize = 12; + var instrMaxFontSize = 60; + var instrOptimalFontSize = instrMinFontSize; + + + // Binary search for optimal font size + while (instrMinFontSize <= instrMaxFontSize) + { + var instrMidFontSize = (instrMinFontSize + instrMaxFontSize) / 2; + var instrTestFont = FontUtils.CreateFont(instrMidFontSize, FontStyle.Regular); + var instrTestHeight = ImageUtils.MeasureTextHeight(descriptionInstructions, instrTestFont, UIConstants.LineSpacing, instrWrappingWidth); + + Logger.Log($"Describer instructions font {instrMidFontSize}pt: height={instrTestHeight}, target={instrTargetHeight}"); + + if (instrTestHeight <= instrTargetHeight) + { + instrOptimalFontSize = instrMidFontSize; + instrMinFontSize = instrMidFontSize + 1; + } + else + { + instrMaxFontSize = instrMidFontSize - 1; + } + } + + if (instrOptimalFontSize < 14) + { + instrOptimalFontSize = 14; // Minimum readable size + Logger.Log($"Describer instructions font size was too small, forcing minimum of 14pt"); + } + + Logger.Log($"FINAL DESCRIBER INSTRUCTIONS FONT SIZE: {instrOptimalFontSize}pt for {descriptionInstructions.Length} chars in {instrWrappingWidth}x{instrTargetHeight}px"); + + // Create font and text options for describer instructions + var instructionsFont = FontUtils.CreateFont(instrOptimalFontSize, FontStyle.Regular); + var instructionsTextOptions = new RichTextOptions(instructionsFont) + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + WrappingLength = instrWrappingWidth, + Origin = new PointF(currentX + UIConstants.Padding, UIConstants.Padding), + Dpi = UIConstants.TextDpi + }; + + // Draw background and text for describer instructions + ctx.Fill(Color.LightBlue, new RectangleF(currentX, 0, instructionsWidth, instructionsHeight)); + ctx.DrawTextStandard(instructionsTextOptions, descriptionInstructions, Color.Black); + + // Draw label for describer instructions + var instructionsLabelOpts = new RichTextOptions(labelFont) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Top + }; + instructionsLabelOpts.Dpi = UIConstants.TextDpi; + instructionsLabelOpts.Origin = new PointF(currentX + instructionsWidth / 2f, maxImageHeight + UIConstants.Padding); + ctx.DrawTextStandard(instructionsLabelOpts, $"{describerModelName} Instructions", Color.Black); + } + else + { + // Draw placeholder if no instructions + ctx.Fill(Color.LightGray, new RectangleF(currentX, 0, instructionsWidth, instructionsHeight)); + var noInstructionsFont = FontUtils.CreateFont(18, FontStyle.Regular); + var noInstructionsTextOptions = new RichTextOptions(noInstructionsFont) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Dpi = UIConstants.TextDpi + }; + noInstructionsTextOptions.Origin = new PointF(currentX + instructionsWidth / 2f, instructionsHeight / 2f); + ctx.DrawTextStandard(noInstructionsTextOptions, "No Instructions", Color.Gray); + + // Draw label even when no instructions + var instructionsLabelOpts = new RichTextOptions(labelFont) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Top + }; + instructionsLabelOpts.Dpi = UIConstants.TextDpi; + instructionsLabelOpts.Origin = new PointF(currentX + instructionsWidth / 2f, instructionsHeight + UIConstants.Padding); + ctx.DrawTextStandard(instructionsLabelOpts, $"{describerModelName} Instructions", Color.Black); + } + currentX += instructionsWidth; + + // 2b. Draw Description Text + var targetUtilization = 0.95; + var availableHeight = descriptionHeight - 2 * UIConstants.Padding; + var wrappingWidth = descriptionWidth - 2 * UIConstants.Padding; + var targetHeight = availableHeight * targetUtilization; + + // Count newlines in the text - they affect height calculations! + var newlineCount = descriptionText.Count(c => c == '\n'); + + Logger.Log($"Description panel: {descriptionWidth}x{descriptionHeight}"); + Logger.Log($"Target to fill: {wrappingWidth}x{targetHeight} (85% of available)"); + Logger.Log($"Text: {descriptionText.Length} chars with {newlineCount} newlines"); + + // This is all so stupid. Why can't you just call MeasureText on the text, including newlines, plus the rectangle size? I really don't get it. + // This whole guessing/semi-hardcoding guidance is wrong. + + // Reasonable font size bounds for long text + var minFontSize = 12; + var maxFontSize = 60; // Much more reasonable maximum for long text + var optimalFontSize = minFontSize; + + Logger.Log($"Adjusted max font size to {maxFontSize} based on text length"); + + + + // Binary search for the optimal size + while (minFontSize <= maxFontSize) + { + var midFontSize = (minFontSize + maxFontSize) / 2; + var testFont = FontUtils.CreateFont(midFontSize, FontStyle.Regular); + var testHeight = ImageUtils.MeasureTextHeight(descriptionText, testFont, UIConstants.LineSpacing, wrappingWidth); + + Logger.Log($"Font {midFontSize}pt: height={testHeight}, target={targetHeight}"); + + if (testHeight <= targetHeight) + { + // Text fits, try larger + optimalFontSize = midFontSize; + minFontSize = midFontSize + 1; + } + else + { + // Text doesn't fit, try smaller + maxFontSize = midFontSize - 1; + } + } + + // Make sure we found something reasonable + if (optimalFontSize < 14) + { + optimalFontSize = 14; // Minimum readable size + Logger.Log($"Font size was too small, forcing minimum of 14pt"); + } + + Logger.Log($"FINAL FONT SIZE: {optimalFontSize}pt for {descriptionText.Length} chars in {wrappingWidth}x{targetHeight}px"); + + // Create the final font and text options with the optimal size + var descriptionFont = FontUtils.CreateFont(optimalFontSize, FontStyle.Regular); + var descriptionTextOptions = new RichTextOptions(descriptionFont) + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + WrappingLength = wrappingWidth, + Origin = new PointF(currentX + UIConstants.Padding, UIConstants.Padding), + Dpi = UIConstants.TextDpi // Ensure same DPI as measurement! + }; + + + ctx.Fill(Color.LightGray, new RectangleF(currentX, 0, descriptionWidth, descriptionHeight)); // Background for text + ctx.DrawTextStandard(descriptionTextOptions, descriptionText, Color.Black); + + var labelOptsDescription = new RichTextOptions(labelFont) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Top + }; + labelOptsDescription.Dpi = UIConstants.TextDpi; // Ensure same DPI as measurement! + labelOptsDescription.Origin = new PointF(currentX + descriptionWidth / 2f, maxImageHeight + UIConstants.Padding); + ctx.DrawTextStandard(labelOptsDescription, $"{describerModelName} Description", Color.Black); + currentX += descriptionWidth; + + + + // 3. Draw Loaded Images with their labels + foreach (var li in loadedImages) + { + // Calculate vertical offset to align bottom of image with maxImageHeight + var imageYOffset = maxImageHeight - li.Height; + + if (li.Image != null) + { + ctx.DrawImage(li.Image, new Point((int)currentX, imageYOffset), 1); + } + else + { + // Draw placeholder for error images + ctx.Fill(Color.LightGray, new Rectangle((int)currentX, imageYOffset, li.Width, li.Height)); + var errorTextOptions = new RichTextOptions(labelFont) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Dpi = UIConstants.TextDpi // Ensure same DPI as measurement! + }; + errorTextOptions.Origin = new PointF(currentX + li.Width / 2f, imageYOffset + li.Height / 2f); + ctx.DrawTextStandard(errorTextOptions, "ERROR", Color.Red); + } + + var labelText = li.Result ?? ""; + + var labelOpts = new RichTextOptions(labelFont) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Top, + WrappingLength = li.Width - 10, // Allow wrapping within image bounds + Dpi = UIConstants.TextDpi // Ensure same DPI as measurement! + }; + labelOpts.Origin = new PointF(currentX + li.Width / 2f, maxImageHeight + UIConstants.Padding); + + var labelColor = li.Success ? UIConstants.SuccessGreen : UIConstants.ErrorRed; + ctx.DrawTextStandard(labelOpts, labelText, labelColor); + + currentX += li.Width; + } + + + + }); + var outputPath = await SaveCombinedImageToDisk(layoutImage, descriptionText, settings); + + foreach (var li in loadedImages) + { + li.Image?.Dispose(); + } + + OpenImageWithDefaultApplication(outputPath); + + return outputPath; + } + private static Image RenderPromptPanel(int width, string prompt, Font promptFont) + { + width = Math.Max(width, PlaceholderWidth); + + int wrappingWidth = Math.Max(1, width - UIConstants.Padding * 4); + int promptHeight = ImageUtils.MeasureTextHeight(prompt, promptFont, UIConstants.LineSpacing, wrappingWidth); + // Halved from the previous Padding*3 / Padding*2 so the + // prompt sits closer to the top of its band. + int topPadding = (int)Math.Ceiling(UIConstants.Padding * 1.5f); + int bottomPadding = UIConstants.Padding; + int totalHeight = topPadding + promptHeight + bottomPadding; + + var promptPanel = ImageUtils.CreateStandardImage(width, totalHeight, UIConstants.White); + + promptPanel.Mutate(ctx => + { + var promptOpts = FontUtils.CreateTextOptions(promptFont, HorizontalAlignment.Left, VerticalAlignment.Top, UIConstants.LineSpacing); + promptOpts.Origin = new PointF(UIConstants.Padding * 2, topPadding); + promptOpts.WrappingLength = wrappingWidth; + + ctx.DrawTextStandard(promptOpts, prompt, UIConstants.Black); + }); + + return promptPanel; + } + + + public static void OpenImageWithDefaultApplication(string imagePath) + { + if (File.Exists(imagePath)) + { + try + { + Process.Start(new ProcessStartInfo(imagePath) { UseShellExecute = true }); + } + catch (Exception ex) + { + Console.WriteLine($"Error opening image {imagePath}: {ex.Message}"); + } + } + else + { + Console.WriteLine($"Image file not found at {imagePath}"); + } + } + + private static int MeasureImageSubdescriptionHeight(IEnumerable loadedImages, Font generatorFont) + { + int measured = loadedImages + .Select(li => $"{li.Result} {li.Generator}" ?? "missing") + .Select(label => ImageUtils.MeasureTextHeight(label, generatorFont, UIConstants.LineSpacing)) + .DefaultIfEmpty(0) + .Max(); + // Add descender reserve: TextMeasurer.MeasureBounds can + // under-report for fonts at this size, clipping the tails + // of g/p/y/j. See AGENTS.md > Visual & Typography Policy. + int descenderReserve = (int)Math.Ceiling(generatorFont.Size * LabelDescenderReserveFraction); + return measured + descenderReserve; + } + + + + + public static LoadedImage GetPlaceholder(TaskProcessResult result) + { + // Try to match what the real image WOULD have been, so the + // placeholder occupies the same column footprint and the prompt + // panel underneath doesn't get squeezed to a narrow 300-px strip. + // Generators (currently just gpt-image-2) stash their chosen size + // into PromptDetails.RuntimeMeta["size"] before the API call, so + // it's available even on failure results. Fall back to a reasonable + // 1024x1024 if no size metadata exists (older generators, unknown + // cases), which is still far nicer than the old 300-px square. + int width = 1024; + int height = 1024; + var meta = result.PromptDetails?.RuntimeMeta; + if (meta != null && meta.TryGetValue("size", out var sizeStr) && !string.IsNullOrWhiteSpace(sizeStr)) + { + if (TryParseSize(sizeStr, out var w, out var h)) + { + width = w; + height = h; + } + } + + return new LoadedImage + { + Image = null, + Success = false, + Result = result.ErrorMessage ?? "ErrorX", + Generator = result.ImageGeneratorDescription, + TimingLabel = FormatTiming(result.CreateTotalMs + result.DownloadTotalMs), + Width = width, + Height = height + }; + } + + // "1024x1024" -> (1024, 1024). Returns false for "auto" or any + // unparseable string so the caller can fall back to a default. + private static bool TryParseSize(string s, out int width, out int height) + { + width = 0; + height = 0; + if (string.IsNullOrWhiteSpace(s)) return false; + var parts = s.Split('x'); + if (parts.Length != 2) return false; + if (!int.TryParse(parts[0], out width) || !int.TryParse(parts[1], out height)) return false; + if (width <= 0 || height <= 0) return false; + return true; + } + + /// Formats a total-elapsed-ms value into a short "%dm %ds" + /// string. Returns null for non-positive values so the caller + /// can unambiguously decide not to render anything. + private static string? FormatTiming(long totalMs) + { + if (totalMs <= 0) return null; + int totalSeconds = (int)Math.Round(totalMs / 1000.0); + int mins = totalSeconds / 60; + int secs = totalSeconds % 60; + return mins > 0 ? $"{mins}m {secs}s" : $"{secs}s"; + } + + private static Image RenderEmptyLayout() + { + var empty = ImageUtils.CreateStandardImage(PlaceholderWidth, PlaceholderWidth + UIConstants.Padding * 2, UIConstants.White); + + empty.Mutate(ctx => + { + var font = FontUtils.CreateFont(LabelFontSize, FontStyle.Regular); + var opts = FontUtils.CreateTextOptions(font, HorizontalAlignment.Center, VerticalAlignment.Center, UIConstants.LineSpacing); + opts.Origin = new PointF(empty.Width / 2f, empty.Height / 2f); + ctx.DrawTextStandard(opts, "No images", UIConstants.ErrorRed); + }); + + return empty; + } + + + private static async Task SaveCombinedImageToDisk(Image image, string prompt, Settings settings) + { + string todayFolder = DateTime.Now.ToString("yyyy-MM-dd-dddd"); + string baseFolder = Path.Combine(settings.ImageDownloadBaseFolder, todayFolder, "Combined"); + Directory.CreateDirectory(baseFolder); + + var truncatedPrompt = FilenameGenerator.TruncatePrompt(prompt, 50); + var baseFilename = $"combined_{truncatedPrompt}_{DateTime.Now:HHmmss}"; + var safeFilename = FilenameGenerator.SanitizeFilename(baseFilename); + var outputPath = Path.Combine(baseFolder, $"{safeFilename}.png"); + + int counter = 1; + while (File.Exists(outputPath)) + { + outputPath = Path.Combine(baseFolder, $"{safeFilename}_{counter}.png"); + counter++; + } + + await image.SaveAsPngAsync(outputPath); + Logger.Log($"Combined image saved: {outputPath}"); + DlMirror.Copy(outputPath, settings.FlatImageMirrorPath); + + return outputPath; + } + + public class LoadedImage + { + public bool Success { get; set; } + public string? Result { get; set; } + public required string Generator { get; set; } + /// Preformatted total generation time (e.g. "2m 15s"), or + /// null if we have no usable timing for this result. + public string? TimingLabel { get; set; } + public Image? Image { get; set; } + public int Width { get; set; } + public int Height { get; set; } + } + + private class ImageDimensions + { + public int TotalWidth { get; set; } + public int MaxImageHeight { get; set; } + public int SubtitleHeight { get; set; } + public int PromptHeight { get; set; } + public int TotalHeight { get; set; } + } + + + } +} diff --git a/MultiImageClient/promptTransformation/ImageSaving.cs b/MultiImageClient/promptTransformation/ImageSaving.cs new file mode 100644 index 0000000..bbdec7e --- /dev/null +++ b/MultiImageClient/promptTransformation/ImageSaving.cs @@ -0,0 +1,268 @@ +#nullable enable +using IdeogramAPIClient; + +using ImageMagick; + +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System.Text.Json; +using System.Text.Json.Serialization; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Diagnostics; + + +namespace MultiImageClient +{ + public static class ImageSaving + { + private static readonly HttpClient httpClient = new HttpClient(); + + private const int LabelRightSideWidth = 200; + private const float LabelTotalLineSpacing = 1.3f; + + private const int LabelFontSize = 12; + private const int LabelRightFontSize = 10; + + + public static void ConvertWebpTopng(string inputFp) + { + var im = new MagickImage(inputFp); + var newFp = Path.ChangeExtension(inputFp, ".png"); + im.Write(newFp); + } + + public static async Task DownloadImageAsync(TaskProcessResult result) + { + try + { + using var response = await httpClient.GetAsync(result.Url); + if (!response.IsSuccessStatusCode) + { + Logger.Log($"Failed to download image: {response.StatusCode}"); + return Array.Empty(); + } + + var res = await response.Content.ReadAsByteArrayAsync(); + if (res.Length == 0) + { + Logger.Log($"Downloaded image is empty"); + return Array.Empty(); + } + + Logger.Log($"\tDownloading image from: {result.Url}, bytes:{res.Length}"); + return res; + } + catch (Exception ex) + { + Logger.Log($"Failed to download image from {result.Url}: {ex.Message}"); + return Array.Empty(); + } + } + + public static async Task SaveImageAsync(PromptDetails promptDetails, byte[] imageBytes, int imageCountN, string contentType, Settings settings, SaveType saveType, IImageGenerator generator) + { + string todayFolder = DateTime.Now.ToString("yyyy-MM-dd-dddd"); + string baseFolder = Path.Combine(settings.ImageDownloadBaseFolder, todayFolder); + + if (saveType != SaveType.Raw) + { + baseFolder = Path.Combine(baseFolder, saveType.ToString()); + } + + Directory.CreateDirectory(baseFolder); + + var usingPromptTextPart = FilenameGenerator.TruncatePrompt(promptDetails.Prompt, 90); + var generatorFilename = generator.GetFilenamePart(promptDetails); + + var safeFilename = FilenameGenerator.GenerateUniqueFilename($"{generatorFilename}_{usingPromptTextPart}", imageCountN, contentType, baseFolder, saveType); + var fullPath = Path.Combine(baseFolder, safeFilename); + + try + { + if (File.Exists(fullPath)) + { + throw new Exception("no overwriting!"); + } + await File.WriteAllBytesAsync(fullPath, imageBytes); + + if (saveType == SaveType.Raw) + { + //Logger.Log($"Saved {saveType} image. Fp: {fullPath}"); + //_stats.SavedRawImageCount++; + } + else + { + var specPart = generator.GetGeneratorSpecPart(); + var imageInfo = GetAnnotationDefaultData(specPart, promptDetails, fullPath, saveType, generator); + var usingSteps = GetUsingSteps(saveType, promptDetails); + if (saveType == SaveType.JustOverride) + { + await TextFormatting.JustAddSimpleTextToBottomAsync( + imageBytes, + usingSteps, + imageInfo, + fullPath, + saveType + ); + } + else if (saveType == SaveType.Label) + { + var rightParts = generator.GetRightParts(); + var costPart = $"{generator.GetCost()} $USD"; + rightParts.Add(costPart); + rightParts.Add("MultiImageClient"); + using var originalImage = SixLabors.ImageSharp.Image.Load(imageBytes); + var label = MakeLabelGeneral(originalImage.Width, promptDetails.Prompt, rightParts); + + using var labelImage = SixLabors.ImageSharp.Image.Load(label); + int newHeight = originalImage.Height + labelImage.Height; + using var combinedImage = new Image(originalImage.Width, newHeight); + combinedImage.Mutate(ctx => + { + ctx.ApplyStandardGraphicsOptions(); + ctx.DrawImage(originalImage, new Point(0, 0), 1f); + ctx.DrawImage(labelImage, new Point(0, originalImage.Height), 1f); + }); + + // Save the combined image + await combinedImage.SaveAsPngAsync(fullPath); + } + else + { + await TextFormatting.SaveImageAndAnnotate( + imageBytes, + usingSteps, + imageInfo, + fullPath, + saveType + ); + } + //stats.SavedAnnotatedImageCount++; + } + } + catch (Exception ex) + { + Logger.Log($"\tError saving {saveType} image: {ex.Message}\r\n{ex}"); + } + + // Optional flat-folder mirror (Settings.FlatImageMirrorPath). + // No-op if the setting is blank. Best-effort; never throws. + DlMirror.Copy(fullPath, settings.FlatImageMirrorPath); + + return fullPath; + } + + public static byte[] MakeLabelGeneral(int width, string prompt, List rightParts) + { + var leftSideWidth = width - LabelRightSideWidth; + var fontSize = LabelFontSize; + var font = FontUtils.CreateFont(fontSize, FontStyle.Regular); + var labelRightFont = FontUtils.CreateFont(LabelRightFontSize, FontStyle.Regular); + + // Measure left side text to determine height + var leftTextOptions = FontUtils.CreateTextOptions(font, + HorizontalAlignment.Left, VerticalAlignment.Top, LabelTotalLineSpacing); + leftTextOptions.WrappingLength = leftSideWidth - (UIConstants.Padding * 2); + + int leftTextHeight = ImageUtils.MeasureTextHeight(prompt, font, LabelTotalLineSpacing, leftTextOptions.WrappingLength); + + // Calculate right side font size (scale down if needed to fit width) + + + // Find the maximum width needed for right side text + //foreach (var text in rightParts.Where(s => !string.IsNullOrEmpty(s))) + //{ + // var testOptions = FontUtils.CreateTextOptions(rightFont, HorizontalAlignment.Right); + // var textBounds = TextMeasurer.MeasureBounds(text, testOptions); + + // rightFont = ImageUtils.AutoSizeFont(text, LabelRightSideWidth, rightFontSize, UIConstants.MinFontSize, FontStyle.Regular); + // rightFontSize = (int)rightFont.Size; + //} + + // Calculate right side height using proper text height measurement + var rightLineHeight = ImageUtils.MeasureTextHeight("Sample", labelRightFont, UIConstants.LineSpacing); + var rightTextHeight = rightParts.Where(s => !string.IsNullOrEmpty(s)).Count() * rightLineHeight; + + // Overall height is the maximum of left and right sides plus padding + int contentHeight = Math.Max(leftTextHeight, rightTextHeight); + int totalHeight = contentHeight + (UIConstants.Padding * 2); + + using var image = ImageUtils.CreateStandardImage(width, totalHeight, UIConstants.Black); + + image.Mutate(ctx => + { + var leftRect = new RectangleF(0, 0, leftSideWidth + 5, totalHeight); + ctx.DrawTextWithBackground(leftRect, prompt, font, UIConstants.White, UIConstants.Black, HorizontalAlignment.Left, LabelTotalLineSpacing); + + // Draw right side text + var yOffset = UIConstants.Padding; + foreach (var text in rightParts.Where(s => !string.IsNullOrEmpty(s))) + { + var rightTextOptions = FontUtils.CreateTextOptions(labelRightFont, HorizontalAlignment.Right, VerticalAlignment.Top, LabelTotalLineSpacing); + rightTextOptions.Origin = new PointF(width - UIConstants.Padding, yOffset); + + ctx.DrawTextStandard(rightTextOptions, text, UIConstants.Gold); + yOffset += rightLineHeight; + } + }); + + // Convert to byte array + using var ms = new MemoryStream(); + image.SaveAsPng(ms); + return ms.ToArray(); + } + + private static IEnumerable GetUsingSteps(SaveType saveType, PromptDetails promptDetails) + { + return saveType switch + { + SaveType.FullAnnotation => promptDetails.TransformationSteps, + SaveType.InitialIdea or SaveType.FinalPrompt or SaveType.Raw or SaveType.JustOverride or SaveType.Label => new List() { promptDetails.TransformationSteps.First() }, + _ => throw new Exception("Invalid SaveType") + }; + } + + private static Dictionary GetAnnotationDefaultData(string generatorIdentifier, PromptDetails promptDetails, string fullPath, SaveType saveType, IImageGenerator generator) + { + var imageInfo = new Dictionary(); + + switch (saveType) + { + case SaveType.FullAnnotation: + imageInfo.Add("Filename", Path.GetFileName(fullPath)); + break; + case SaveType.InitialIdea: + var initialPrompt = promptDetails.TransformationSteps.First().Explanation; + imageInfo.Add("Producer", generatorIdentifier); + imageInfo.Add("Initial Prompt", initialPrompt); + break; + case SaveType.FinalPrompt: + var finalPrompt = promptDetails.Prompt; + imageInfo.Add("Producer", generatorIdentifier); + imageInfo.Add("Final Prompt", finalPrompt); + break; + case SaveType.Raw: + // No annotation + break; + case SaveType.JustOverride: + var initialPrompt2 = promptDetails.TransformationSteps.First().Explanation; + imageInfo.Add("Producer", generatorIdentifier); + imageInfo.Add("Initial Prompt", initialPrompt2); + break; + } + + imageInfo = imageInfo.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value)) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + return imageInfo; + } + } +} diff --git a/MultiImageClient/promptTransformation/ManualModificationStep.cs b/MultiImageClient/promptTransformation/ManualModificationStep.cs index 2afb065..1b9f5c4 100644 --- a/MultiImageClient/promptTransformation/ManualModificationStep.cs +++ b/MultiImageClient/promptTransformation/ManualModificationStep.cs @@ -1,24 +1,27 @@ -using System.Threading.Tasks; -using MultiImageClient.Implementation; - -namespace MultiImageClient -{ - public class ManualModificationStep : ITransformationStep - { - public string Name => nameof(ManualModificationStep); - private string Prefix { get; set; } - private string Suffix { get; set; } - public ManualModificationStep(string prefix, string suffix) - { - Prefix = prefix; - Suffix = suffix; - } - - public Task DoTransformation(PromptDetails pd, MultiClientRunStats stats) - { - var newp = $"{Prefix} {pd.Prompt} {Suffix}".Trim(); - pd.ReplacePrompt(newp, newp, TransformationType.ManualSuffixation); - return Task.FromResult(true); - } - } -} +using MultiImageClient; + +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public class ManualModificationStep : ITransformationStep + { + public string Name => nameof(ManualModificationStep); + private string Prefix { get; set; } + private string Suffix { get; set; } + private MultiClientRunStats _stats { get; set; } + public ManualModificationStep(string prefix, string suffix, MultiClientRunStats stats) + { + Prefix = prefix; + Suffix = suffix; + _stats = stats; + } + + public Task DoTransformation(PromptDetails pd) + { + var newp = $"{Prefix} {pd.Prompt} {Suffix}".Trim(); + pd.ReplacePrompt(newp, newp, TransformationType.ManualSuffixation); + return Task.FromResult(true); + } + } +} diff --git a/MultiImageClient/promptTransformation/MultiClientRunStats.cs b/MultiImageClient/promptTransformation/MultiClientRunStats.cs new file mode 100644 index 0000000..8435f10 --- /dev/null +++ b/MultiImageClient/promptTransformation/MultiClientRunStats.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; + +namespace MultiImageClient +{ + public class MultiClientRunStats + { + public int SavedRawImageCount { get; set; } + public int SavedAnnotatedImageCount { get; set; } + public int SavedJsonLogCount { get; set; } + + public int IdeogramRequestCount { get; set; } + public int IdeogramV3RequestCount { get; set; } + public int IdeogramV4RequestCount { get; set; } + public int IdeogramRefusedCount { get; set; } + public int IdeogramV3RefusedCount { get; set; } + public int IdeogramV4RefusedCount { get; set; } + + public int ClaudeRequestCount { get; set; } + public int ClaudeWouldRefuseCount { get; set; } + public int ClaudeRefusedCount { get; set; } + public int ClaudeRewroteCount { get; set; } + + public int Dalle3RequestCount { get; set; } + public int Dalle3RefusedCount { get; set; } + + public int GptImageOneRequestCount { get; set; } + public int GptImageOneRefusedCount { get; set; } + + public int GptImage2RequestCount { get; set; } + public int GptImage2RefusedCount { get; set; } + + public int BFLImageGenerationRequestCount { get; set; } + public int BFLImageGenerationSuccessCount { get; set; } + public int BFLImageGenerationErrorCount { get; set; } + + public int RecraftImageGenerationRequestCount { get; set; } + public int RecraftImageGenerationSuccessCount { get; set; } + public int RecraftImageGenerationErrorCount { get; set; } + + public int GrokImageGenerationRequestCount { get; set; } + public int GrokImageGenerationSuccessCount { get; set; } + public int GrokImageGenerationErrorCount { get; set; } + + public int GrokVideoGenerationRequestCount { get; set; } + public int GrokVideoGenerationSuccessCount { get; set; } + public int GrokVideoGenerationErrorCount { get; set; } + + public int LocalFlux2RequestCount { get; set; } + public int LocalFlux2SuccessCount { get; set; } + public int LocalFlux2ErrorCount { get; set; } + + public int DirectImageGenerationRequestCount { get; set; } + public int DirectImageGenerationSuccessCount { get; set; } + public int DirectImageGenerationErrorCount { get; set; } + + public int GoogleRequestCount { get; set; } + public int GoogleRefusedCount { get; set; } + + public void PrintStats() + { + var nonZeroStats = new List(); + + if (SavedRawImageCount > 0) + nonZeroStats.Add($"Raw Image Saved:{SavedRawImageCount}"); + if (SavedJsonLogCount > 0) + nonZeroStats.Add($"JSON:{SavedJsonLogCount}"); + + if (ClaudeWouldRefuseCount > 0) + nonZeroStats.Add($"Claude Would Refuse:{ClaudeWouldRefuseCount}"); + if (ClaudeRequestCount > 0) + nonZeroStats.Add($"Claude Requests:{ClaudeRequestCount}"); + if (ClaudeRefusedCount > 0) + nonZeroStats.Add($"Claude Refused:{ClaudeRefusedCount}"); + if (ClaudeRewroteCount > 0) + nonZeroStats.Add($"Claude Accepted:{ClaudeRewroteCount}"); + + if (IdeogramRequestCount > 0) + nonZeroStats.Add($"Ideogram Requests:{IdeogramRequestCount}"); + if (IdeogramV3RequestCount > 0) + nonZeroStats.Add($"Ideogram v3 Requests:{IdeogramV3RequestCount}"); + if (IdeogramRefusedCount > 0) + nonZeroStats.Add($"Ideogram Refused:{IdeogramRefusedCount}"); + if (IdeogramV3RefusedCount > 0) + nonZeroStats.Add($"Ideogram v3 Refused:{IdeogramV3RefusedCount}"); + if (IdeogramV4RequestCount > 0) + nonZeroStats.Add($"Ideogram v4 Requests:{IdeogramV4RequestCount}"); + if (IdeogramV4RefusedCount > 0) + nonZeroStats.Add($"Ideogram v4 Refused:{IdeogramV4RefusedCount}"); + + if (Dalle3RequestCount > 0) + nonZeroStats.Add($"Dalle3 Requests:{Dalle3RequestCount}"); + if (Dalle3RefusedCount > 0) + nonZeroStats.Add($"Dalle3 Refused:{Dalle3RefusedCount}"); + + if (GptImageOneRequestCount > 0) + nonZeroStats.Add($"GPT Image One Requests:{GptImageOneRequestCount}"); + if (GptImageOneRefusedCount > 0) + nonZeroStats.Add($"GPT Image One Refused:{GptImageOneRefusedCount}"); + + if (GptImage2RequestCount > 0) + nonZeroStats.Add($"GPT Image 2 Requests:{GptImage2RequestCount}"); + if (GptImage2RefusedCount > 0) + nonZeroStats.Add($"GPT Image 2 Refused:{GptImage2RefusedCount}"); + + if (GoogleRequestCount > 0) + nonZeroStats.Add($"Google Requests:{GoogleRequestCount}"); + if (GoogleRefusedCount > 0) + nonZeroStats.Add($"Google Refused:{GoogleRefusedCount}"); + + if (BFLImageGenerationRequestCount > 0 | BFLImageGenerationErrorCount > 0 | BFLImageGenerationSuccessCount > 0) + nonZeroStats.Add($"BFL: total:{BFLImageGenerationRequestCount}, ok:{BFLImageGenerationSuccessCount}, bad:{BFLImageGenerationErrorCount} "); + + if (RecraftImageGenerationRequestCount > 0 | RecraftImageGenerationErrorCount > 0 | RecraftImageGenerationSuccessCount > 0) + nonZeroStats.Add($"Recraft: total:{RecraftImageGenerationRequestCount}, ok:{RecraftImageGenerationSuccessCount}, bad:{RecraftImageGenerationErrorCount} "); + + if (GrokImageGenerationRequestCount > 0 | GrokImageGenerationErrorCount > 0 | GrokImageGenerationSuccessCount > 0) + nonZeroStats.Add($"Grok: total:{GrokImageGenerationRequestCount}, ok:{GrokImageGenerationSuccessCount}, bad:{GrokImageGenerationErrorCount} "); + + if (GrokVideoGenerationRequestCount > 0 | GrokVideoGenerationErrorCount > 0 | GrokVideoGenerationSuccessCount > 0) + nonZeroStats.Add($"Grok Video: total:{GrokVideoGenerationRequestCount}, ok:{GrokVideoGenerationSuccessCount}, bad:{GrokVideoGenerationErrorCount} "); + + if (LocalFlux2RequestCount > 0 | LocalFlux2ErrorCount > 0 | LocalFlux2SuccessCount > 0) + nonZeroStats.Add($"Local Flux2: total:{LocalFlux2RequestCount}, ok:{LocalFlux2SuccessCount}, bad:{LocalFlux2ErrorCount} "); + + if (DirectImageGenerationRequestCount > 0 | DirectImageGenerationErrorCount > 0 | DirectImageGenerationSuccessCount > 0) + nonZeroStats.Add($"Direct Image APIs: total:{DirectImageGenerationRequestCount}, ok:{DirectImageGenerationSuccessCount}, bad:{DirectImageGenerationErrorCount} "); + + var res = $"Stats: {string.Join(", ", nonZeroStats)}"; + Console.WriteLine(res); + } + } +} \ No newline at end of file diff --git a/MultiImageClient/promptTransformation/RandomizerStep.cs b/MultiImageClient/promptTransformation/RandomizerStep.cs index 11c43fc..0ce65cc 100644 --- a/MultiImageClient/promptTransformation/RandomizerStep.cs +++ b/MultiImageClient/promptTransformation/RandomizerStep.cs @@ -1,228 +1,229 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime; -using System.Text; -using System.Threading.Tasks; -using MultiImageClient.Implementation; - -namespace MultiImageClient.promptTransformation -{ - public class Alias - { - public List Options = new List(); - public double Probability { get; set; } = 0.23; - public string Name { get; set; } - public Func Formatter { get; set; } - - public Alias(string name, Func formatter, string values) - { - Name = name; - Options.AddRange(values.Split(',').ToList().Select(el => el.Trim())); - Formatter = formatter; - } - - public string GetRandomAlias(bool? force = false) - { - var rndval = Random.Shared.NextDouble(); - if ((force.HasValue && force.Value ) || rndval < Probability) - { - var val = Options[new Random().Next(Options.Count)]; - return Formatter(val); - } - else - { - return ""; - } - } - } - - public class RandomizerStep : ITransformationStep - { - private static Random random = new Random(); - - public string Name => nameof(RandomizerStep); - public static Alias strange = new Alias("GPTStrange", (string a) => $"{a}.", "unclear image, a strange image, pixellated, made of food, badly photoshopped, as if a blind person organized it, wearing clothing no adult would ever wear, unbelievably strange, cruelly drawn, wasteful, awful, terrifyingly tiny, miniaturely lilliputian, hundreds of dogs everywhere, hundreds of spiders everywhere, entire world is furry, completely charred and destroyed and burned down, everything is made of emojis, extremely distorted emoji heads, hidden faces and eyes everywhere, super distortion mode, realistic chibi style, realistic roblox style, super blurry, lost footage restored from 1800 colorized, over-colorized ancient movie style, badly drawn amateur art, in the style of 4chan mspaint memes, as if drawn in mspaint, in the style of deviantart, as if it was a magic card illustration, made of dripped melted wax, art you would see in walmart, unclear image, a strange image, pixellated, badly photoshopped, as if a blind person organized it, super distortion mode, super blurry, lost footage restored from 1800 colorized, over-colorized ancient movie style, unclear image, distorted visuals, glitchy artifacts, bizarre rendering, surreal composition, warped perspectives, fractured realities, digital disintegration, pixelated nightmares, fragmented scenes, dreamlike landscapes, otherworldly visions, hallucinatory spectacles, reality-bending illusions, mind-bending abstractions, paradoxical imagery, nonsensical juxtapositions, unconventional compositions, subverted expectations, surreal amalgamations, dreamscapes from alternate dimensions.unclear image, distorted visuals, glitchy artifacts, bizarre rendering, surreal composition, warped perspectives, fractured realities, digital disintegration, pixelated nightmares, fragmented scenes, dreamlike landscapes, otherworldly visions, hallucinatory spectacles, reality-bending illusions, mind-bending abstractions, paradoxical imagery, nonsensical juxtapositions, unconventional compositions, subverted expectations, surreal amalgamations, dreamscapes from alternate dimensions, made of unconventional materials, edible masterpieces, culinary compositions, gastronomic artworks, fashion-defying ensembles, whimsical attire, emoji mosaics, expressive digital faces, molten wax sculptures, untrained artistic expressions, raw creative impulses, internet subculture aesthetics, digital folk art, online community artistry, trading card universe illustrations, fantastical realms captured on canvas, magical worlds depicted in paint., unfathomably bizarre, inexplicably peculiar, disturbingly odd, unnaturally eerie, hauntingly surreal, disquietingly uncanny, unsettlingly grotesque, nightmarishly twisted, cruelly rendered, mercilessly depicted, wastefully extravagant, shockingly awful, appallingly dreadful, horrifyingly minuscule, microscopic terrors, canine invasions, arachnid infestations, furry apocalypse, scorched landscapes, charred remnants, smoldering ruins, hidden gazes lurking, omnipresent watchful eyes, mass-produced banality, commercial art nightmares.."); - - public static Alias rpgstyles = new Alias("RPGStyles", (string a) => $"In the style of {a}", - "Dungeons and Dragons, Pathfinder, Lamentations of the Flame Princess, " + - "Dungeon Crawl Classics, Fantasy AGE, Warhammer Fantasy, " + - "Palladium Fantasy, G.U.R.P.S, Basic Fantasy, Low Fantasy Gaming, " + - "Vagabond, Tales of the Valiant, Cypher System, Savage Worlds, " + - "RuneQuest, Ars Magica, Iron Kingdoms, Torchbearer, The One Ring, " + - "Burning Wheel, Legend of the Five Rings, Fate, 13th Age, " + - "Adventurer Conqueror King System, Forbidden Lands, Conan, OSR, " + - "Fighting Fantasy, Tunnels and Trolls, Monsters Monsters, TTRPG, " + - "EZD6, Index Card RPG, Dungeons of Drakkenheim"); - - public static Alias games = new Alias("GPTGames", (string a) => $"In the style of {a}", "Clash of Clans, Angry Birds, Candy Crush, Pokemon Go, Call of Duty: Modern Warfare, Super Mario Bros., The Legend of Zelda, Tetris, Minecraft, GTA V," + - " The Elder Scrolls V: Skyrim , World of Warcraft, Overwatch, Super Smash Bros. Ultimate, Dark Souls III, The Witcher 3, Counter-Strike, DOTA 2, League of Legends, Everquest, Plants vs Zombies, 2048, " + - "Minesweeper, Freecell, Fruit Ninja, Simcity, The Sims, Rocket League, Diablo II, Path of Exile, Stardew Vallye, Garry's Mod, PubG, Bejeweled, Final Fantasy VII, Assassin's Creed, Metal Gear Solid V, " + - "Animal Crossing, Halo: Combat Evolved, Portal, Beat Saber, Werewolf, Sudoku, Yu-Gi-Oh!, Magic: The Gathering, Dance Dance Revolution, Quidditch, Chess, Checkers, Baduk aka Weiqi aka go, Tic-Tac-Toe, " + - "Hades, Nethack, Solar Scuffle, Apex Legends, Splatoon 2, Sea of Thieves, Military Madness aka Nectaris, Arkanoid, Donkey Kong Jr., Subnautica, The Last of Us, Pinball, Quake, Myst, " + - "Street Fighter II Turbo, Sonic the Hedgehog, GoldenEye 007, Half-Life, StarCraft, Guitar Hero, Wii Sports, Bioshock, Fallout 2, Pong, Battleship, Club, Frogger, Asteroids, Duck Hunt, " + - "Prince of Persia, Mortal Kombat, The Oregon Trail, Lemmings, fortnite, slay the spire, factorio, Mega Man X, Factorio, EarthBound, Deus Ex, Populous, Baldur's Gate, Age of Empires II: The Age of Kings," + - "Axis and Allies, Risk, Gears of War, Castlevania "); - - public static Alias historySpots = new Alias("GPTHistory", (string a) => $"The setting is {a}", "1200 Iceland, 1920 Iceland, 1500ad, 10bc finland, 1000ad mayan empire, 10000 years BR in caveman times, 100 million years ago among dinosaurs, in a time travel time machine, in the super far future, \" +\r\n\"1200ad peru, 1900 buenos aires, 1966 falkland islands, 1000bc northern canada, 1000bc nunavut, 2020 toronto, 2022 vancouver, 1000 bc, 0ad, 10ad, 40ad rome, 400ad babylon, 800ad kazakhstan, 1200 china, 2000 china, 2005 beijing, 2010 japan"); - - public static Alias astro = new Alias("GPTAstro", (string a) => $"{a} are visible", "galaxies, nebulae, supernovae, pulsars, quasars, cosmic rays, dark matter, dark energy, neutron stars, wormholes, exoplanets, asteroid belts, comets, meteoroids, meteors, shooting stars, auroras, eclipses, space debris, space elevators"); - - public static Alias robloxGames = new Alias("GPTRobloxGames", (string a) => $"In the style the Roblox game: '{a}'", "Adopt Me: Neon Pets, Apocalypse Rising, Bad Business, Balloon Simulator, Bird Simulator, Clone Tycoon 2, Crazy Knife, Cube eat Cube, " + - "Dance Your Blox Off, Firefighter Simulator, Flood Escape 2, Guest World, Heroes Online, Hide and Seek Extreme, Ice Cream Simulator, Mining Simulator 2, Parkour Simulator, Please Donate, Prison Life, " + - "Restaurant Tycoon 2, RoCitizens, Robeats, Robloxian Highschool, Rumble Quest, SCP: Roleplay, Saber Simulator, SharkBite, Snow Shoveling Simulator, Soccer Legends, Strucid, Super Bomb Survival!!, " + - "Super Doomspire, Survive the Killer, Swordburst 2, Terrain Parkour, Texting Simulator, The Floor is Lava, Tower Battles, Ultimate Driving: Westover Islands, Wizard Tycoon - 2 Player, World // Zero, " + - "Zombie Rush, Blade ball, Cube eat cube, Egg Hunt, Trade Hangout, Big paintball, Ride a cart simulator, Tiny Tanks, Hueblox, Classic Crossroads, Grass Cutting Tycoon, Murder Mystery 2," + - "Swordfight on the heights, Sword fighting simulator, Bee Swarm Simulator, Sword Fight Tornument, +1 fat every second, Trade Hangout, Catalog Heaven, Mermaid Life, Hueblox, My Droplets, " + - "Crossroads, The Underground War, mortal coil, Wacky Wizards, Welcome to the town of Robloxia - Building, Nuke the Whales, BIRD, Bloxfruits, Deception Infection, Super Golf, Weight Lifting Simulator," + - " Watermelon Go, BIG Paintball, Bladeball, Survive a plane crash, Fall down a 10000 stud hole, Build to survive the dragobloxers, VR Hands, Condo Games, On Tap, DOORS, Santa's Winter Fortress, Tower Defense Simulator, A wolf or other, Cleaning Simulator, Loomian Legacy, Broken Bones IV, Project Lazuras, Apocalypse Rising, Obby but you're on a bike, The Roblox Happy Home, Gucci Garden, Catalog Avatar Creator, Reason 2 Die, Mad Murderer, Miner's Haven, The Wind, Islands, Adopt Me!, Tower of Hell, Brookhaven 🏡RP, MeepCity, Murder Mystery 2, Blox Fruits, Arsenal, Royale High, Piggy, Jailbreak, Shindo Life, Anime Fighting Simulator, Bee Swarm Simulator, Welcome to Bloxburg, Build A Boat For Treasure, Mad City, Dungeon Quest, Vehicle Simulator, Ninja Legends, Bubble Gum Simulator, Tower Defense Simulator, Phantom Forces, Boku No Roblox, Super Power Training Simulator, Mining Simulator, Dragon Ball Z Final Stand, Shinobi Life 2, Natural Disaster Survival, Work at a Pizza Place, Blox Piece, Adopt and Raise a Cute Kid, Speed Run 4, Weight Lifting Simulator 3, Roblox High School, Theme Park Tycoon 2, Epic Minigames, Lumber Tycoon 2, Super Hero Tycoon, Treasure Hunt Simulator, Island Royale, Zombie Attack, Pet Simulator X, Flee the Facility, Skywars, Vehicle Tycoon, Booga Booga, Lucky Block Battlegrounds, Pokemon Brick Bronze, Among Us"); - - public static Alias scifi = new Alias("GPTScifi", (string a) => $"Utilizing high-tech {a}", "artificial intelligence, quantum computing, virtual reality, augmented reality, brain-computer interfaces, genetic engineering, nanotechnology, bionic implants, androids, cyborgs, exoskeletons, teleportation, time travel, warp drives, force fields, plasma weapons, lasers, energy shields, cloaking devices, replicators, holodecks, genetic recombination, controlled mutation, mental psionics, levitating ships, self-driving cars"); - - public static Alias weatherPhenomena = new Alias("GPTWeatherPhenomena", (string a) => $"Weather conditions: {a}", "hurricanes, typhoons, blizzards, hailstorms, wildfires, heat waves, droughts, famines, earthquakes, landslides, avalanches, volcanic eruptions, tsunamis, flash floods, quicksand, sleet, hail, drizzle, intense fog, mist, sinkholes, sandstorms, dust storms, lightning storms, meteor showers, solar flares"); - - public static Alias aggressivelyFantastic = new Alias("GPTFantastical", (string a) => $"There is some {a}.", "mega-kanji, acid rain forests, aether currents, alien relics, alien strange unusual kaiju, ancient holographic maps, ancient leviathans, ancient war machines, anti-gravity waterfalls, antimatter cascades, Arcane observatories, astral plane waypoints, ball lightning, banshees, barrels and crates, basilisks, Bio-engineered phoenixes, bio-luminescent fungi, bio-mechanical dragons, bioluminescent forests, Bioluminescent reefs, blood-red tides, carnivorous plants, Celestial dragons, celestial music, celestial spheres, Celestial tapestries, chaos vortexes, chonky cats, cliff waterfalls, magnificent towers, Clockwork castles, Cosmic gardens, cosmic jellyfish, cosmic strings, cosmic voids, Cosmic whirlpools, crypid homelands, crystal caves, Crystal-clear tall icebergs, Crystal-encrusted golems, cursed artifacts, cursed labyrinths, cursed ruins, cyclopean monoliths, D&D beholder monsters, Dimension hoppers, dimensional folds, dream weavers, dream worlds, drifting ruins of space stations, drones which are equipped with tools to give flying haircuts, dwarf horses, A dyson sphere, echo chambers, echoing canyons and gorges, forgotten fjords, eldritch abysses, elemental planes, self-erasing scenery, fnord, enchanted forests, enchanted labyrinths, encrypted alien languages, endless deserts, entropy engines, Ethereal guardians, ethereal planes, exceptionally gigantic and excessively magma-producing volcanoes, exotic and unusual strange orchids, exploding stars, extremely huge and super steep mega-cliffs, fairies, Fireflies of unusual size, flaming skulls, floating islands, Floating markets, flocks of unique strange butterflies, Forgotten realms, forgotten tombs, fractal forests, fractal realms, frozen tundras, fulgurites, futuristic Tesla models, Ghostly caravans, ghostly waltzes, gigantic geodes of various types, gigantic ice walls, glimmering alien spires, glitching realities, glow worms, gravity lenses, gravity wells, gravity-defying mountains, gravity-defying waterfalls, haunted castles, holographic universes, huge greenhouses, huge realistic ant farms, hydras, hyperdimensional cubes, hypersonic sand dunes, icebergs, Infinite libraries, interdimensional portals, invisible rivers, mutated krakens, large solar panel arrays, lava and magma rivers, levitating temples, liquid light, living constellations, living forests, living lightning bolts, living mountains, living statues, living storms, Living vines, Luminescent fungi, Lunar forests, magical liches, magnetic whirlwinds, mega-huge gigantic ocean-going ships and vessels, megasharks and skeletons and relics of them, memory mazes, Memory palaces, merfolk, minotaurs, mirror dimensions, Mirror lakes, mirror mazes, molten landscapes, moonlit battlegrounds, Mystic auroras, Mystic fogs, nano-swarms, naturally forming and artificial lasers, nebula nurseries, Nebulae oceans, necromancer crypts, neon jungles, obsidian fortresses, orcs, paradox loops, parallel universes, pegasi, perpetual eclipses, Phantasmal echoes, phase-shifted cities, phoenixes, pixies, Plasma dragons, plasma tornadoes, pocket dimensions, probability storms, psychic nexuses, psychic storms, pulsar beams, quantum coral reefs, quantum mirages, quantum rainbows, quantum realms, Quantum waterfalls, quantum whispers, reality anchors, reality bubbles, reality glitches, rogue AI swarms, Rune-inscribed monoliths, self-replicating nanobots, sentient crystal clusters, sentient crystals, sentient fog, sentient nebulae, sentient shadows, Sentient stars, sentient storms, serpentine rivers, Shadow puppeteers, shadow realms, shape-shifting mountain ranges, shape-shifting sands, shattered spheres, Shimmering mirages, Singing sand dunes, singularity seeds, Solar flares, Space-time anomalies, specific d&d artifacts, spectral apparitions, stalactites, star forges, star-eating black holes, Starlight sanctuaries, steep striped pyramids, Storm-chasing ships, sub-zero geysers, techno-organic hybrids, Teleporting islands, temporal echoes, temporal rifts, temporal whirlpools, tesseract temples, thought bubbles, Thunderous silence, time rifts, Time-worn ruins, tons and tons of scattered gold bars, Translucent butterflies, triple rainbows, trolls, tsunamis and wave formations and waterspouts, unbelievably cute and anachronistically oversized puppies, underwater cities, unusual alien hexapods, unusual sand-worms, various types of unusual rare never-before-seen dinosaurs, viral supernovas, void flowers, void whales, volcanic lightning storms, Whispering galaxies, whispering willows, wild horses, zeppelins, zero-point energy."); - - public static Alias forts = new Alias("GPTForts", (string a) => $"{a} is being used as a fort.", - "monumental architecture, fort knox, the white house, the louvre, the golden temple of amritsar, the taj mahal, brutalist architecture, a massive curtain wall extending to the horizon which holds an entire city, a ponderously gigantially tall wizard's tower, a monumental glacier, sheer cliffs, a sheer crevass riddled with ledges and ice tunnels, infinite stairways, abandoned and fortified oil rigs, a magically preserved castle, interlocking moats, megaliths, arcologies, red square and the kremlin, mordor and the dark tower, narnian castles at carnaervon, the washington monument, Ancient ziggurats, Floating citadels, Underwater domes, Sky-high spires, Labyrinthine catacombs, Tree-top villages, Subterranean bunkers, Colossal stone statues, Desert fortresses, Ice palaces, Glass pyramids, Steampunk airships, Hollowed-out mountains, Volcano lairs, Enchanted windmills, Celestial observatories, Haunted mansions, Bioluminescent caves, Cloud castles, Crystal fortresses, Time-worn lighthouses, Gigantic shipwrecks, Solar-powered strongholds, Mirror mazes, Spiral towers, Quantum bunkers, Floating islands, Dragon's lairs, Cosmic temples, Celestial arches, Gravity-defying bridges, Massive clock towers, Underwater labyrinths, Mystical pagodas, Space stations, Megalithic circles, Mystic monasteries, Enchanted libraries, Alchemical laboratories, Golem workshops, Rune-inscribed walls, Celestial amphitheaters, Astral gateways, Meteorite craters, Transcendent shrines, Ghostly fortifications, Starship docks, Dimensional nexuses, Floating gardens, Fortress monasteries, Mystic ziggurats, Hanging Gardens over Mesopotamia, Great Wall across endless mountains, Petra's rose-red city in cliffs, Notre-Dame's Gothic spires in Paris, Machu Picchu atop the Andes, Minotaur's labyrinth on Crete, Submerged ruins of Atlantis, Egypt's pyramids guarding sands, Stonehenge's mystical stone circle, Viking longhouses along fjords, Colosseum as colossal fortress, Shinto shrines in cedar forests, Hagia Sophia's domes in Istanbul, Medieval castles on the Rhine, Floating temples of Tenochtitlan, Agra's Red Fort sandstone walls, Alexandria's ancient library, Alhambra overlooking Granada, El Dorado in the Amazon, Ziggurat of Ur under starlight, Celtic hillforts on Britannia's hills, Sumerian cuneiform temples, Tower of Babel reaching heavens, Chichén Itzá's celestial pyramids, Carcassonne's walled battlements, Castles in the Black Forest, Cappadocia's underground cities, Forbidden City in Beijing, Blue Mosque's domes in Istanbul, Rajasthan's deep stepwells, Kremlin's stars over Red Square, Gothic abbeys in countryside, Silk Road desert fortresses, Genghis Khan's Mongolian encampments, Anasazi cliff dwellings, Sunken continent of Lemuria, Hieroglyphic obelisks in Luxor, Meteora's cliff-top monasteries, Malta's megalithic temples, Camelot, Arthur's stronghold, Neuschwanstein in alpine forests, Lalibela's rock-hewn churches, Ancient citadel of Aleppo, Scottish Highlands' castles, Persepolis in ancient Persia, Ghost city Fatehpur Sikri, Dubrovnik's walls by the sea, Epidaurus's echoing amphitheaters, Potala Palace in the clouds, Matera's rock-cut city, Knossos's palace on Crete"); - - ///okay this one is awesome. - public static Alias compositionTypes = new Alias("GPTCompositions", (string a) => $"Composition style: '{a}'", - "low horizon, high horizon, gradient following exploratory, minimalist sky, " + - "rule of thirds with extreme offset, negative space, " + - "leading lines, symmetrical, asymettrical, confrontational, hypnotically repeating, repetitive, starkly minimalist, spherical, " + - "diagonal, golden ratio geometrical compsition, spiral twirling composition, shallow depth of field with extreme bokeh, obsessed with bokeh, deceptive scale, forced perspective, " + - "textured foreground, vibrant color contrast, silhouette, " + - "dynamic tension, abstract pattern, reflective symmetry, " + - "juxtaposition, balanced elements"); - - public static Alias locations = new Alias("GPTLocations", (string a) => $"Location: '{a}'", - "Seaside cliffs of rural Seoul, the moors of central barren foggy Scotland along a plateau edge above an abandoned castle, " + - "the San Francisco bay trail in fog, " + - "The sf peninsula foggy hills above Half Moon Bay, " + - "An infinitely high cliff overlooking a beautiful detailed paradise farming world, " + - "A post-apocalyptic restoration center for humanity" + - "The Ruins of an Ancient City among the crumbling walls lost relics and symbols and abandoned empty streets of a once-great metropolis, " + - "A Floating City in the Clouds with aerial platforms and suspended walkways that provide a unique three-dimensional space, " + - "An Underwater City among submerged buildings and coral reefs with sea creatures and glowing hints of life and wear, " + - "A Volcanic Crater where Lava flows and eruptions thunder nearby revealing glowing magma, " + - "A Dense Misty Jungle where Visibility is limited and the dense unique foliage and flowers flora and fauna, " + - "A Massive Moving Train with carriages on carriages led by a speeding gigantic oversized locomotive along incredible tracks along with a small raccoon detective," + - "In the Tunnels of a Giant Ant Colony which is a A maze-like claustrophobic setting with numerous pathways," + - "A Giant Ancient Library where Countless books and shelves hid hidden mystical or scientific knowledge beyond the ken of normal men inhabited by appropriately studious creatures, " + - "An Abandoned Amusement Park with roller coasters and haunted houses and and carousels, " + - "A Network of Ice Caves with Slippery surfaces and narrow passageways and a constant threat of collapse inhabited by ice denizens," + - "0 BC Rome - Bustling streets of Ancient Rome with senators gladiators and grand architecture like the Colosseum, " + - "Ancient Neanderthal Caves in Europe - Primitive cave dwellings adorned with early human paintings and artifacts," + - "1200s Mayan Empire Temple Top - A vibrant Mayan temple amidst the dense jungle filled with intricate carvings and astronomical alignments," + - "15th Century Forbidden City in Beijing - Majestic palaces and gardens showcasing Imperial China's grandeur," + - "1940s Paris during WWII - War-torn yet resilient Paris with clandestine meetings and the spirit of resistance," + - "Victorian London in the 1800s - Foggy streets horse-drawn carriages and the burgeoning industrial revolution," + - "Ancient Egyptian Pyramids Giza circa 2500 BC - Monumental pyramids and the Sphinx with bustling construction and ancient rituals," + - "Edo Period Tokyo (1603-1868) - Traditional Japanese architecture samurai warriors and vibrant street life," + - "Viking Settlement in Scandinavia 9th Century - Rugged landscapes with longships mead halls and Nordic culture," + - "Renaissance Florence in the 15th Century - Birthplace of the Renaissance filled with art innovation and political intrigue," + - "1920s New York City during the Jazz Age - Skyscrapers rising speakeasies bustling and the Harlem Renaissance in full swing," + - "Ancient Athens during the Golden Age (5th Century BC) - Philosophers orators and the Parthenon symbolizing the cradle of democracy," + - "Mughal Empire in Delhi 17th Century - Opulent palaces and gardens epitomized by the Taj Mahal," + - "Medieval Constantinople 10th Century - A crossroads of cultures with Byzantine and Ottoman influences," + - "Aztec Capital Tenochtitlán 15th Century - An island city with intricate canals bustling markets and grand temples," + - "18th Century Versailles during the French Monarchy - Extravagant palace life with opulent balls and intricate political plots," + - "Ancient Carthage 3rd Century BC - A powerful Mediterranean port city with a mix of cultures and naval dominance," + - "Gold Rush San Francisco 1850s - Rapid growth and diversity amidst the pursuit of fortune," + - "Indus Valley Civilization 2500 BC in Mohenjo-Daro - Advanced urban planning and mysterious script in one of the world's earliest major cities," + - "Sparta in the Classical Greek Period 5th Century BC - A city-state renowned for its military discipline and austere lifestyle, " + - "A Desert Oasis with lush greenery and water springs amidst vast sand dunes sheltering diverse wildlife," + - "A High-Speed Space Station orbiting a distant planet with futuristic technology and panoramic views of the cosmos," + - "An Overgrown Ruined Castle shrouded in ivy and history echoes of ancient battles and forgotten tales," + - "A Crystal Cavern sparkling with multi-colored gems and crystal formations reflecting light in dazzling patterns," + - "A Skyborne Archipelago with floating islands connected by rope bridges harboring unique flora and fauna," + - "A Deep-Sea Abyss where bioluminescent creatures and strange geological formations exist in perpetual darkness," + - "A Futuristic Metropolis with towering skyscrapers neon lights and advanced technology teeming with life," + - "A Wild West Ghost Town with dusty streets abandoned saloons and a sense of a time gone by," + - "A Mystical Forest with enchanted trees whispering secrets magical beings and a sense of wonder," + - "An Arctic Research Base isolated in a snowy expanse with cutting-edge facilities and a harsh climate.," + - "The windswept plains of a post-apocalyptic Earth sorrowful yet simple.," + - "An ancient ruin reclaimed by sand windswept yet retaining geometrical beauty.," + - "An endless golden grain field beneath an alien yellow sky - solitude and freedom.," + - "Wonderland from Alice in Wonderland - Curiosity and confusion in a nonsensical realm where magic mushrooms make you grow or shrink and time runs backward," + - "The Shire from Lord of the Rings - Fear and despair in the bleak ashen plains surrounded by volcanoes where Sauron's eye watches endlessly.," + - "Narnia from The Lion The Witch and the Wardrobe - Good triumphing over evil in a land watched over by a noble lion and currently cast under an endless winter.," + - "An all-white limbo space influenced by The Matrix stripped-down yet disturbed.," + - "Symmetrical brutalist structures from Equilibrium's monochromatic city perfection imposed by force.," + - "Vast Zen rock gardens with raked patterns from memoirs of Japanese monks calming and centering.," + - "An icy Watchtower like Game of Throne's Wall hardened shelter from risks unknown.," + - "A precisely planned forest village resembling aesthetics from Princess Mononoke harmoniously balanced with nature.," + - "On the side of a hill from a romantic nostalgic anime," + - "Norse Gods at Ragnarok (Dread Bravery) - The ultimate battle with fate hanging in the balance.," + - "Mythical Olympus at its Peak (Splendor Rivalry) - The home of the gods resplendent but rife with rivalries.," + - "A City on the Back of a Giant Moving Creature (Wanderlust Fear) - A nomadic city with the constant threat of the unknown.," + - "Parallel Universe New York (Bewilderment Excitement) - Familiar yet bizarrely different with endless possibilities.," + - "Ghost Ship in the Bermuda Triangle (Mystery Fear) - A haunted vessel with an eerie and uncertain fate.," + - "An Enchanted Forest with Warring Fae (Beauty Betrayal) - Ethereal landscapes shadowed by deceit and power struggles.," + - "Middle Earth's Rivendell during War (Tranquility Tension) - A serene elven haven on the brink of war.," + - "Atlantis Rising from the Sea (Wonder Awe) - The mythical city re-emerging revealing ancient technologies and mysteries.," + - "Timbuktu in the 14th Century - A thriving center of African scholarship and trade., " + - "Ancient Babylon 6th Century BC - Hanging Gardens and grand ziggurats along the Euphrates River.," + - "Machu Picchu in the 15th Century - Mysterious Incan city hidden high in the Andes Mountains.," + - "Heian-kyo (Kyoto) during Japan's Heian Period - Elegant imperial courts and the flowering of Japanese culture.," + - "19th Century St. Petersburg - Russian imperial splendor with grand canals and the Winter Palace.," + - "Angkor Wat in the 12th Century Cambodia - Majestic temples surrounded by dense jungle.," + - "Viking Age Dublin 9th Century - A bustling Norse settlement and trading hub.," + - "Venice in the Renaissance - Canals and gondolas with a flourishing of arts and commerce.," + - "Jerusalem during the Crusades - A city at the crossroads of religions and conflicts.," + - "Han Dynasty China's Silk Road Cities - Exotic trade routes connecting East and West.," + - "Istanbul during the Ottoman Empire - A melting pot of cultures bridging Europe and Asia.," + - "The Roaring Twenties in Chicago - Jazz prohibition and the rise of organized crime.," + - "Baghdad during the Islamic Golden Age - Center of learning and culture with the House of Wisdom.," + - "Pompeii just before the eruption of Vesuvius in 79 AD - Daily life in an ancient Roman city.," + - "Ancient Athens at the time of Pericles - Flourishing arts and philosophy in the cradle of democracy.," + - "Elizabethan London in the late 16th Century - The time of Shakespeare and the Globe Theatre.," + - "The Hanseatic League in Medieval Lübeck - A rich and powerful merchant city-state.," + - "Harlem during the Harlem Renaissance - A flourishing of African-American arts and culture.," + - "Pre-Columbian Cusco capital of the Inca Empire - Rich in culture and architecture.," + - "Mohenjo-Daro in the Indus Valley Civilization 2500 BC - An advanced ancient city with intricate urban planning.," + - "Ancient Alexandria in the Hellenistic Period - A center of learning and culture home to the Great Library.," + - "Teotihuacan 1st Century AD - Home to the Pyramid of the Sun a pre-Columbian architectural wonder.," + - "The Klondike Gold Rush 1890s in Yukon Canada - Thrill and hardship in the search for gold.," + - "The Height of the Ottoman Empire in 16th Century Istanbul - A cosmopolitan hub of the world.," + - "Feudal Japan during the Time of Samurai - Castles and cherry blossoms and and warrior code.," + - "Ancient Polynesian Settlements in Hawaii - Early navigators and unique island culture.," + - "Colonial Williamsburg in the 18th Century - A living history of American colonial life.," + - "The Grandeur of the Mughal Empire in 16th Century India - Exquisite architecture and rich culture," + - "Ancient Rome at its Zenith under Emperor Trajan - A cosmopolitan empire at its peak," + - "The Ming Dynasty's Forbidden City in 15th Century Beijing - The imperial palace complex at the heart of China"); - - public static Alias artStyles = new Alias("GPTArtstyles", (string a) => $"The art style is: '{a}'", "Aquarela brasileira, 3D, 3D model, 3D printing art, Abstract, Abstract geometry, Acrylic pour, Action painting, Aerial drone art, Ancient pottery techniques, Animación stop motion, Anime, Ansel Adams-like stark, Apocalyptic, Apocalyptic cityscape, Architectural model building, Arte cinético (Spanish) Arte digital (Spanish),Arte povera (Italian), Arte mosaico bizantino, Arte quilling decorativo, Arte urbano graffiti, Augmented reality, Bamboo weaving, Baroque, Barro negro oaxaqueño, Basquiat-inspired street, Batik javanés, Bead weaving, Berniniesque dynamic, Bijutsu shashin (美術写真),Black velvet, Bioluminescent installation, Blackwork embroidery, Body painting, Bone carving, Book folding, Bordado punto cruz, Bordado suzani, Boschian fantastical, Botticellian grace, Brancusiesque simplified, Bruegelian detailed, Calado en madera, Caligrafía árabe, Calligraphic, Calligraphic graffiti fusion, Calligraphic lettering, Calligraphy, Candle crafting, Caravaggesque chiaroscuro, Cascading light display, Cerámica gres, Cerámica raku abstracta, Cestería mapuche, Chagallesque floating, Chainmaille fabric, Chalk street art, Prismatic Spray wave image, Cinematic virtual scenery, Clay animation storytelling, Clay pottery, Coffee art, Collage, Color-shifting mural art, Conceptual art Conceptual space exploration, Constable-inspired pastoral, Constructivist, Coral assemblage, Coral reef painting, Cosmic, Cosmic art, Cosmic photography, Courbet-like realism, Crackle glaze, Cubism, Cubist, Cubist figure, Cyanotype print, Cybernetic, Cybernetic organism, Cyberpunk, Cézannesque geometric, Da Vincian intricate, Dada, Daliesque surreal Danza butoh visual, Danza contemporánea (Spanish),contemporary dance., Deco, Deco elegance, Dibujo carboncillo expresivo, Digital 3D, Digital fabric weaving, Digital mosaic, Digital painting, Digital vector graphics, Dot painting, Dreamy, Dreamy landscape, Drybrush technique, Dystopian landscape sketch, Ebru sanatı, Ecologicalism, Eco-friendly sculpture, Eco-sculpture, Embroidered, Embroidered fabric, Embroidery, En plein air, Encaustic painting, Ephemeral sand portraits, Escheresque impossible, Escultura hielo luminoso Escultura reciclaje innovador, Estampa ukiyo-e, Ethereal, Ethereal glass sculpture, Ethereal vision, Experimental shadow theater, Expressionist, Fabric batik, Fauvist, Faux finish, Feather art, Felted wool landscapes, Fiber optic tapestry, Filigrana italiana, Fire ink drawing, Floral arrangement, Fluid art, Fluorescent body art, Folk, Folk embroidery, Food carving, Fotografía callejera monocromo, Fractal laser etching, Fresco, Fridaesque personal, Futurist, Futuristic, Futuristic skyline, Galactic spray paint, Gauguinesque exotic, Geodesic dome mural, Geometric, Glacial, Glacial sculpture, Glass blowing, Glass etching, Glitch overload, Glowing ice sculpture, Gold leaf, Gothic, Gothic cathedral, Goyesque grotesque, Grabado linóleo detallado, Graffiti, Graffiti wall, Graffito, Gravura em madeira,Gyotaku fish print, Hand-dyed fabric art, Handmade paper making, Handwoven light patterns, Haptic VR art, Haute couture,Highbrow, Highbrow, Highbrow critique, Hokusai-inspired wave, Holographic art, Holographic display, Holographic street art, Hopperesque solitude, Hyperbolic tessellation art, Hyperreal, Hyperrealism sketch, Ice, Ice carving, Ice sculpting, Ice sculpture, Iconografía bizantina, Glacier carving, fire-melted ice, Lava sculpture, Ikebana (生け花),Mixed media collage, Ilustración digital fantasía, Immersive virtual reality, Impressionist, Ink brush, Ink wash, Instalación arte lumínico, Instalación artística (Spanish),Intaglio, Instalación sonido inmersivo, Interactive light installation, Interactive mural Interactive sidewalk chalk, Iridescent bubble art, Joyería artesanal étnica, Junk sculpture, Kahloesque vivid, Kalamkari indio, Kilning, Kinetic, Kinetic mobile, Kinetic sand drawing, Kinetic sculpture, Kinetic wind art, Kintsugi (金継ぎ),Kintsugi repair, Klimtesque ornate, Lace making, Lacquer, Land art, Large-scale mural painting Laser-cut paper art, Leather tooling, Levitating sculpture art, Light painting, Light projection, Linocut print, Liquid metal sculpture, Litho, Litho print, Lithograph, Lost wax casting, Lowbrow, Lowbrow humor, Luces y sombras (Spanish), Light and shadows, Luminous thread embroidery, Macabre, Macro photography, Magnetic fluid art, Magrittesque mysterious, Mandala design, Manetesque modern, Manga, Manga sketch, Marbling, Metal forging, Michelangelesque heroic, Miniature book binding, Miniature model Minimal, Minimal design, Minimalist, Mixed reality installation, Mokuhanga (木版画),Batik tulis,Céramique raku,Papiroflexia,Arte callejero Joie de vivre,Sgraffito (Italian),Guóhuà (国画),traditional Chinese painting., Monetesque impressionist, Mosaic, Munchian angst, Mural, Mural street, Muralismo digital interactivo Muralismo mexicano (Spanish),Mexican muralism., Máscara veneciana, Natural landscape photography, Needle felting, Neon, Neon calligraphy, Neon portrait, Neon wireframe sculptures, Nihonga, Noir, Noir mystery, O'Keeffesque enlarged, Optical, Optical art, Optical illusion, Organic ceramic forms, Origami, Origami crane, Origami tessellation, Paisaje sonoro urbano, Panoramic digital fresco, Papel picado mexicano, Paper cutting, Paper mâché, Paper quilling, Papiroflexia modular avanzada, Parchment scroll, Pastel, Pastel drawing, Peinture abstraite abstract painting., Peisaj urban (Romanian),urban landscape., Performance art, Performance teatro callejero, Perfume blending, Phosphorescent forest art, Photoreal, Pintura al fresco, Pintura al óleo (Spanish),Pyrography, Pintura sumi-e, Pintura óleo realista, Pirograbado ruso, Pixel art, Pixel-perfect manual editing, Pixel animation, Pixelated design, Pixelated character, Pollock-style drip, Pop Art, Pop icons, Primitivism, Programmable matter sculpture, Proyección mapping dinámico, Psychedelic, Psychedelic swirl, Qi baishi (齐白石),Chinese ink painting style., Quantum dot canvas, Quilling art, Quilting, Quink art, Raku, Rakú japonés, Raphaelesque serene, Reactive sound mural Real-time animation projection, Recycled art, Relief carving, Relieve precolombino, Rembrandtesque dramatic, Resin art, Retro, Retro diner, Retrofuturism, Robotic arm painting, Rodinesque sculptural, Rothko-inspired abstract, Rubenesque voluptuous, Rustic, Rustic barn, Rustic pottery, Salt flat mirage, Salt painting, Sand, Sand animation, Sand castle, Sand mandala, Scented ink calligraphy, Scherenschnitte (German),Scrap metal sculpture, Seuratesque pointillist, Sgraffito, Sgraffito etching, Shadow art, Shadow play, Shell mosaic Shibori dyeing, Silhouette, Silhouette cutout, Silk embroidery, Site-specific performance art, Snow architecture, Soap sculpture, Solar plate etching, Solar reactive painting, Sound installation, Sound wave art, Sound-activated light art, Stained glass, Steampunk, Steampunk gadget, Stencil, Stenciled, Stenciled message, Stone masonry, Sumi-e ink, Surreal, Surreal dreamscape, Surrealist, Ta moko (Māori),traditional skin art., Talla piedra ancestral, Tape art, Tapestry, Tapestry weave, Tapiz flamenco, Tapiz mural contemporáneo, Terrazzo flooring, Textile dyeing Thangka, Thermal imaging, Thermal reactive murals, Three-dimensional street art, Time-lapse drawing, Tinta china (Spanish),Chinese ink, Tissage africain, Titianesque vibrant, Toulouse-Lautrec-esque nightlife Traditional woodblock printing, Tribal, Tribal mask, Trompe l'oeil, Trompe-l'œil,Ukiyo-e, Turner-inspired atmospheric, Técnica collage mixto, Ultraviolet landscape art, Urban, Urban decay photography, Urban sketching, Urban sprawl, Van Goghian swirl, Vaporwave, Vaporwave scene, Vermeeresque light, Vexel illustration, Vintage photo restoration, Virtual sculpture, Vitrail, Vitraux gothique, Warholian pop, Watercolor Watercolor garden, Weaving, Wheatpaste poster, Wire sculpture, Wood carving, Woodblock, Woodblock print, Woodcut, Wool felting, Xylography, Yarn bombing, Yarnbomb, Zen philosophy and koans, Zen garden, Zen garden design, Zentangle drawing, 2D Animation, 360-Degree Video, 3D Animation, 3D Printing Art, AI-Generated Art, ASCII Art, Abstract Expressionism, Absurdist Art, Activist Art, Aerial Photography, Agitprop Art, Algorithmic Art, Allegorical Art, Altered Books, Anti-Art, Appropriation Art, Aquatint, Arabic Calligraphy, Architectural Design, Architectural Sculpture, Art Brut, Art Informel, Art Nouveau, Arte Nucleare, Arte Povera, Artists' Books, Assemblage Art, Astronomical Art, Astrophotography, Augmented Reality Art, Aura Art, Automata, Bad Painting, Bas-Relief, Batik, Beadwork, Bio Art, Bio-Architecture, Biohacking Art, Biomorphism, Blacksmithing, Body Art, Bonsai, Bonsai Art, Book Arts, Book Sculpture, Bookbinding, Botanical Illustration, Brush Painting, Byzantine Art, CNC Art, Camp Art, Capitalist Realism, Caricature, Cartooning, Celtic Knotwork, Ceramic Art, Chakra Art, Chinese Calligraphy, Cityscape Painting, Claymation, Cloisonné, CoBrA Movement, Code Art, Collage Art, Collagraph, Color Field Painting, Comic Art, Community Art, Computer Graphics Art, Conceptual Art, Constructivism, Contextual Art, Critical Regionalism, Cut-Out Animation, Cyborg Art, DNA Art, Dadaism, Data Moshing, Data Visualization, De Stijl, Decoupage, Deep Dream Art, Digital Art, Digital Collage, Digital Painting, Drone Photography, Drypoint, Earthworks, Eco-Art, Egg Tempera Painting, Embossing, Embroidery Art, Enamel Art, Encaustic Art, Encaustic Painting, Endurance Art, Engraving, Entoptic Imagery, Environmental Art, Environmental Graphics, Environmental Sculpture, Etching, Exhibition Design, Expressionism, Fabergé Eggs, Fantastic Realism, Fantasy Art, Fauvism, Fiber Art, Fiberglass Sculpture, Figuration Libre, Figure Painting, Flipbooks, Floral Design, Fluxus, Foam Sculpture, Folk Art, Found Object Art, Found Object Sculpture, Fractal Art, Fresco Painting, Futurism, GAN Art, GIF Art, Garden Design, Gemstone Carving, Generative Art, Genetic Art, Genre Painting, Geometric Abstraction, Geometric Art, Gigapixel Imaging, Gilding, Glass Art, Glassblowing Art, Glitch Art, Gold Leaf Art, Gothic Art, Gouache Painting, Graffiti Art, Graffiti Calligraphy, Graphic Novels, Green Roofs, Guerilla Art, Guohua, Gutai Group, Happenings, Hard-edge Painting, Henna Art, High Relief, High-Speed Photography, History Painting, Holography, Hyperrealism, Ice Sculpture, Iconography, Ikat, Ikebana, Illuminated Letters, Illuminated Manuscripts, Illustration, Immersive Installations, Immersive Media, Impressionism, Indigenous Art, Infographics, Information Design, Infrared Photography, Ink Wash Painting, Inlay Art, Installation Art, Intaglio, Intarsia, Interactive Installations, Interactive Panoramas, Interior Design, Internet Art, Intervention Art, Islamic Geometric Art, Jewelry Design, Kinetic Architecture, Kinetic Art, Kinetic Sculpture, Kinetic Typography, Kintsugi, Kirigami, Kitsch Art, Lace Making, Land Art, Landscape Architecture, Landscape Painting, Laser Art, Letterpress, Lettrism, Light Art, Light Field Photography, Linocut, Lithography, Living Walls, Lost Wax Casting, Lowbrow Art, Lyrical Abstraction, Macro Photography, Magic Realism, Mail Art, Mandala Art, Manga Art, Mannerism, Marquetry, Massurrealism, Matte Painting, Maximalism, Mechanical Art, Medical Illustration, Meme Art, Metal Casting, Metal Leaf Art, Metalwork Art, Mezzotint, Microbial Art, Microscopic Art, Minimalism, Mixed Media, Mixed Reality Art, Monochrome Painting, Monotype, Monumental Art, Mosaic Art, Moss Art, Motion Graphics, Motion Graphics Art, Mughal Miniature, Multisensory Art, Mural Art, Murano Glass, Museum Design, Mythological Art, Narrative Art, Naïve Art, Neo-Dada, Neo-Expressionism, Neo-Geo, Neoclassicism, Net Art, Neural Style Transfer Art, Nordic Runes, Nouveau Réalisme, Op Art, Open Source Art, Optical Art, Organic Abstraction, Origami Architecture, Orphism, Outsider Art, Panorama Photography, Paper Engineering, Papercutting Art, Papier-mâché, Participatory Art, Pattern Art, Performance Art, Persian Miniature, Phenakistoscopes, Photomontage, Photorealism, Pixel Art, Plop Art, Pointillism, Political Art, Polymer Clay Sculpture, Pop Surrealism, Pop-Up Books, Porcelain Art, Portrait Painting, Post-Impressionism, Pre-Raphaelite Art, Procedural Art, Projection Art, Projection Mapping, Propaganda Art, Psychedelic Art, Public Art, Puppet Animation, Purism, Quilting Art, Raku Pottery, Rayonism, Realism, Recycled Art, Reduction Printing, Relational Aesthetics, Relief Carving, Relief Sculpture, Religious Art, Renaissance Art, Resin Casting, Retail Design, Reverse Graffiti, Ritual Art, Robotic Art, Rococo, Romanticism, Sacred Art, Sand Mandala, Sand Sculpture, Satirical Art, Science Fiction Art, Scientific Illustration, Screen Printing, Seascape Painting, Shadow Puppetry, Shamanic Art, Shanshui Painting, Shaped Canvas, Shibori, Silhouette Animation, Site-Specific Art, Situationist International, Social Practice Art, Software Art, Sots Art, Sound Art, Space Art, Stained Glass, Steam Engine Art, Sticker Art, Still Life Painting, Stone Balancing, Stone Carving, Stop Motion Animation, Street Art, Stuckism, Sumi-e, Sumi-e Ink Painting, Superflat, Suprematism, Surrealism, Sustainable Art, Symbolism, Symmetry Art, Synchromism, Tachisme, Tactical Media, Tantric Art, Tapestry Weaving, Tattoo Art, Tenebrism, Terra Cotta Art, Tessellation Art, Time-Lapse Photography, Topiary, Topiary Art, Totemic Art, Toyism, Transavantgarde, Tribal Art, Typography Art, Ukiyo-e, Ultraviolet Photography, Underwater Photography, Upcycling Art, Urban Design, Video Art, Video Installation, Virtual Reality Art, Virtual Tour Photography, Visionary Art, Vorticism, Voxel Art, Watercolor Painting, Wayfinding Design, Wheatpaste Art, Wildlife Art, Wire Sculpture, Wood Carving, Yarn Bombing, Zen Ink Painting, Zero Group, Zines, Zoetropes, Zombie Formalism"); - - private string GetRandomSampling() - { - var sampledAliases = new List(); - - var guys = new List() {aggressivelyFantastic, forts, locations, compositionTypes, artStyles, weatherPhenomena }; - guys = guys.OrderBy(x => Random.Shared.Next()).ToList(); - foreach (var alias in guys) - { - var val = alias.GetRandomAlias(true); - if (!string.IsNullOrEmpty(val)) - { - sampledAliases.Add(val); - } - if (sampledAliases.Count >= 1) - { - break; - } - } - return string.Join(", ", sampledAliases); - } - - public Task DoTransformation(PromptDetails pd, MultiClientRunStats stats) - { - var randoms = GetRandomSampling(); - var newVersion = $"'{pd.Prompt}' {randoms}"; - pd.ReplacePrompt(newVersion, newVersion, TransformationType.Randomizer); - - return Task.FromResult(true); - } - } -} +using MultiImageClient; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime; +using System.Text; +using System.Threading.Tasks; + +namespace MultiImageClient +{ + public class Alias + { + public List Options = new List(); + public double Probability { get; set; } = 0.23; + public string Name { get; set; } + public Func Formatter { get; set; } + + public Alias(string name, Func formatter, string values) + { + Name = name; + Options.AddRange(values.Split(',').ToList().Select(el => el.Trim())); + Formatter = formatter; + } + + public string GetRandomAlias(bool? force = false) + { + var rndval = Random.Shared.NextDouble(); + if ((force.HasValue && force.Value ) || rndval < Probability) + { + var val = Options[new Random().Next(Options.Count)]; + return Formatter(val); + } + else + { + return ""; + } + } + } + + public class RandomizerStep : ITransformationStep + { + private static Random random = new Random(); + + public string Name => nameof(RandomizerStep); + public static Alias strange = new Alias("GPTStrange", (string a) => $"{a}.", "unclear image, a strange image, pixellated, made of food, badly photoshopped, as if a blind person organized it, wearing clothing no adult would ever wear, unbelievably strange, cruelly drawn, wasteful, awful, terrifyingly tiny, miniaturely lilliputian, hundreds of dogs everywhere, hundreds of spiders everywhere, entire world is furry, completely charred and destroyed and burned down, everything is made of emojis, extremely distorted emoji heads, hidden faces and eyes everywhere, super distortion mode, realistic chibi style, realistic roblox style, super blurry, lost footage restored from 1800 colorized, over-colorized ancient movie style, badly drawn amateur art, in the style of 4chan mspaint memes, as if drawn in mspaint, in the style of deviantart, as if it was a magic card illustration, made of dripped melted wax, art you would see in walmart, unclear image, a strange image, pixellated, badly photoshopped, as if a blind person organized it, super distortion mode, super blurry, lost footage restored from 1800 colorized, over-colorized ancient movie style, unclear image, distorted visuals, glitchy artifacts, bizarre rendering, surreal composition, warped perspectives, fractured realities, digital disintegration, pixelated nightmares, fragmented scenes, dreamlike landscapes, otherworldly visions, hallucinatory spectacles, reality-bending illusions, mind-bending abstractions, paradoxical imagery, nonsensical juxtapositions, unconventional compositions, subverted expectations, surreal amalgamations, dreamscapes from alternate dimensions.unclear image, distorted visuals, glitchy artifacts, bizarre rendering, surreal composition, warped perspectives, fractured realities, digital disintegration, pixelated nightmares, fragmented scenes, dreamlike landscapes, otherworldly visions, hallucinatory spectacles, reality-bending illusions, mind-bending abstractions, paradoxical imagery, nonsensical juxtapositions, unconventional compositions, subverted expectations, surreal amalgamations, dreamscapes from alternate dimensions, made of unconventional materials, edible masterpieces, culinary compositions, gastronomic artworks, fashion-defying ensembles, whimsical attire, emoji mosaics, expressive digital faces, molten wax sculptures, untrained artistic expressions, raw creative impulses, internet subculture aesthetics, digital folk art, online community artistry, trading card universe illustrations, fantastical realms captured on canvas, magical worlds depicted in paint., unfathomably bizarre, inexplicably peculiar, disturbingly odd, unnaturally eerie, hauntingly surreal, disquietingly uncanny, unsettlingly grotesque, nightmarishly twisted, cruelly rendered, mercilessly depicted, wastefully extravagant, shockingly awful, appallingly dreadful, horrifyingly minuscule, microscopic terrors, canine invasions, arachnid infestations, furry apocalypse, scorched landscapes, charred remnants, smoldering ruins, hidden gazes lurking, omnipresent watchful eyes, mass-produced banality, commercial art nightmares.."); + + public static Alias rpgstyles = new Alias("RPGStyles", (string a) => $"In the style of {a}", + "Dungeons and Dragons, Pathfinder, Lamentations of the Flame Princess, " + + "Dungeon Crawl Classics, Fantasy AGE, Warhammer Fantasy, " + + "Palladium Fantasy, G.U.R.P.S, Basic Fantasy, Low Fantasy Gaming, " + + "Vagabond, Tales of the Valiant, Cypher System, Savage Worlds, " + + "RuneQuest, Ars Magica, Iron Kingdoms, Torchbearer, The One Ring, " + + "Burning Wheel, Legend of the Five Rings, Fate, 13th Age, " + + "Adventurer Conqueror King System, Forbidden Lands, Conan, OSR, " + + "Fighting Fantasy, Tunnels and Trolls, Monsters Monsters, TTRPG, " + + "EZD6, Index Card RPG, Dungeons of Drakkenheim"); + + public static Alias games = new Alias("GPTGames", (string a) => $"In the style of {a}", "Clash of Clans, Angry Birds, Candy Crush, Pokemon Go, Call of Duty: Modern Warfare, Super Mario Bros., The Legend of Zelda, Tetris, Minecraft, GTA V," + + " The Elder Scrolls V: Skyrim , World of Warcraft, Overwatch, Super Smash Bros. Ultimate, Dark Souls III, The Witcher 3, Counter-Strike, DOTA 2, League of Legends, Everquest, Plants vs Zombies, 2048, " + + "Minesweeper, Freecell, Fruit Ninja, Simcity, The Sims, Rocket League, Diablo II, Path of Exile, Stardew Vallye, Garry's Mod, PubG, Bejeweled, Final Fantasy VII, Assassin's Creed, Metal Gear Solid V, " + + "Animal Crossing, Halo: Combat Evolved, Portal, Beat Saber, Werewolf, Sudoku, Yu-Gi-Oh!, Magic: The Gathering, Dance Dance Revolution, Quidditch, Chess, Checkers, Baduk aka Weiqi aka go, Tic-Tac-Toe, " + + "Hades, Nethack, Solar Scuffle, Apex Legends, Splatoon 2, Sea of Thieves, Military Madness aka Nectaris, Arkanoid, Donkey Kong Jr., Subnautica, The Last of Us, Pinball, Quake, Myst, " + + "Street Fighter II Turbo, Sonic the Hedgehog, GoldenEye 007, Half-Life, StarCraft, Guitar Hero, Wii Sports, Bioshock, Fallout 2, Pong, Battleship, Club, Frogger, Asteroids, Duck Hunt, " + + "Prince of Persia, Mortal Kombat, The Oregon Trail, Lemmings, fortnite, slay the spire, factorio, Mega Man X, Factorio, EarthBound, Deus Ex, Populous, Baldur's Gate, Age of Empires II: The Age of Kings," + + "Axis and Allies, Risk, Gears of War, Castlevania "); + + public static Alias historySpots = new Alias("GPTHistory", (string a) => $"The setting is {a}", "1200 Iceland, 1920 Iceland, 1500ad, 10bc finland, 1000ad mayan empire, 10000 years BR in caveman times, 100 million years ago among dinosaurs, in a time travel time machine, in the super far future, \" +\r\n\"1200ad peru, 1900 buenos aires, 1966 falkland islands, 1000bc northern canada, 1000bc nunavut, 2020 toronto, 2022 vancouver, 1000 bc, 0ad, 10ad, 40ad rome, 400ad babylon, 800ad kazakhstan, 1200 china, 2000 china, 2005 beijing, 2010 japan"); + + public static Alias astro = new Alias("GPTAstro", (string a) => $"{a} are visible", "galaxies, nebulae, supernovae, pulsars, quasars, cosmic rays, dark matter, dark energy, neutron stars, wormholes, exoplanets, asteroid belts, comets, meteoroids, meteors, shooting stars, auroras, eclipses, space debris, space elevators"); + + public static Alias robloxGames = new Alias("GPTRobloxGames", (string a) => $"In the style the Roblox game: '{a}'", "Adopt Me: Neon Pets, Apocalypse Rising, Bad Business, Balloon Simulator, Bird Simulator, Clone Tycoon 2, Crazy Knife, Cube eat Cube, " + + "Dance Your Blox Off, Firefighter Simulator, Flood Escape 2, Guest World, Heroes Online, Hide and Seek Extreme, Ice Cream Simulator, Mining Simulator 2, Parkour Simulator, Please Donate, Prison Life, " + + "Restaurant Tycoon 2, RoCitizens, Robeats, Robloxian Highschool, Rumble Quest, SCP: Roleplay, Saber Simulator, SharkBite, Snow Shoveling Simulator, Soccer Legends, Strucid, Super Bomb Survival!!, " + + "Super Doomspire, Survive the Killer, Swordburst 2, Terrain Parkour, Texting Simulator, The Floor is Lava, Tower Battles, Ultimate Driving: Westover Islands, Wizard Tycoon - 2 Player, World // Zero, " + + "Zombie Rush, Blade ball, Cube eat cube, Egg Hunt, Trade Hangout, Big paintball, Ride a cart simulator, Tiny Tanks, Hueblox, Classic Crossroads, Grass Cutting Tycoon, Murder Mystery 2," + + "Swordfight on the heights, Sword fighting simulator, Bee Swarm Simulator, Sword Fight Tornument, +1 fat every second, Trade Hangout, Catalog Heaven, Mermaid Life, Hueblox, My Droplets, " + + "Crossroads, The Underground War, mortal coil, Wacky Wizards, Welcome to the town of Robloxia - Building, Nuke the Whales, BIRD, Bloxfruits, Deception Infection, Super Golf, Weight Lifting Simulator," + + " Watermelon Go, BIG Paintball, Bladeball, Survive a plane crash, Fall down a 10000 stud hole, Build to survive the dragobloxers, VR Hands, Condo Games, On Tap, DOORS, Santa's Winter Fortress, Tower Defense Simulator, A wolf or other, Cleaning Simulator, Loomian Legacy, Broken Bones IV, Project Lazuras, Apocalypse Rising, Obby but you're on a bike, The Roblox Happy Home, Gucci Garden, Catalog Avatar Creator, Reason 2 Die, Mad Murderer, Miner's Haven, The Wind, Islands, Adopt Me!, Tower of Hell, Brookhaven 🏡RP, MeepCity, Murder Mystery 2, Blox Fruits, Arsenal, Royale High, Piggy, Jailbreak, Shindo Life, Anime Fighting Simulator, Bee Swarm Simulator, Welcome to Bloxburg, Build A Boat For Treasure, Mad City, Dungeon Quest, Vehicle Simulator, Ninja Legends, Bubble Gum Simulator, Tower Defense Simulator, Phantom Forces, Boku No Roblox, Super Power Training Simulator, Mining Simulator, Dragon Ball Z Final Stand, Shinobi Life 2, Natural Disaster Survival, Work at a Pizza Place, Blox Piece, Adopt and Raise a Cute Kid, Speed Run 4, Weight Lifting Simulator 3, Roblox High School, Theme Park Tycoon 2, Epic Minigames, Lumber Tycoon 2, Super Hero Tycoon, Treasure Hunt Simulator, Island Royale, Zombie Attack, Pet Simulator X, Flee the Facility, Skywars, Vehicle Tycoon, Booga Booga, Lucky Block Battlegrounds, Pokemon Brick Bronze, Among Us"); + + public static Alias scifi = new Alias("GPTScifi", (string a) => $"Utilizing high-tech {a}", "artificial intelligence, quantum computing, virtual reality, augmented reality, brain-computer interfaces, genetic engineering, nanotechnology, bionic implants, androids, cyborgs, exoskeletons, teleportation, time travel, warp drives, force fields, plasma weapons, lasers, energy shields, cloaking devices, replicators, holodecks, genetic recombination, controlled mutation, mental psionics, levitating ships, self-driving cars"); + + public static Alias weatherPhenomena = new Alias("GPTWeatherPhenomena", (string a) => $"Weather conditions: {a}", "hurricanes, typhoons, blizzards, hailstorms, wildfires, heat waves, droughts, famines, earthquakes, landslides, avalanches, volcanic eruptions, tsunamis, flash floods, quicksand, sleet, hail, drizzle, intense fog, mist, sinkholes, sandstorms, dust storms, lightning storms, meteor showers, solar flares"); + + public static Alias aggressivelyFantastic = new Alias("GPTFantastical", (string a) => $"There is some {a}.", "mega-kanji, acid rain forests, aether currents, alien relics, alien strange unusual kaiju, ancient holographic maps, ancient leviathans, ancient war machines, anti-gravity waterfalls, antimatter cascades, Arcane observatories, astral plane waypoints, ball lightning, banshees, barrels and crates, basilisks, Bio-engineered phoenixes, bio-luminescent fungi, bio-mechanical dragons, bioluminescent forests, Bioluminescent reefs, blood-red tides, carnivorous plants, Celestial dragons, celestial music, celestial spheres, Celestial tapestries, chaos vortexes, chonky cats, cliff waterfalls, magnificent towers, Clockwork castles, Cosmic gardens, cosmic jellyfish, cosmic strings, cosmic voids, Cosmic whirlpools, crypid homelands, crystal caves, Crystal-clear tall icebergs, Crystal-encrusted golems, cursed artifacts, cursed labyrinths, cursed ruins, cyclopean monoliths, D&D beholder monsters, Dimension hoppers, dimensional folds, dream weavers, dream worlds, drifting ruins of space stations, drones which are equipped with tools to give flying haircuts, dwarf horses, A dyson sphere, echo chambers, echoing canyons and gorges, forgotten fjords, eldritch abysses, elemental planes, self-erasing scenery, fnord, enchanted forests, enchanted labyrinths, encrypted alien languages, endless deserts, entropy engines, Ethereal guardians, ethereal planes, exceptionally gigantic and excessively magma-producing volcanoes, exotic and unusual strange orchids, exploding stars, extremely huge and super steep mega-cliffs, fairies, Fireflies of unusual size, flaming skulls, floating islands, Floating markets, flocks of unique strange butterflies, Forgotten realms, forgotten tombs, fractal forests, fractal realms, frozen tundras, fulgurites, futuristic Tesla models, Ghostly caravans, ghostly waltzes, gigantic geodes of various types, gigantic ice walls, glimmering alien spires, glitching realities, glow worms, gravity lenses, gravity wells, gravity-defying mountains, gravity-defying waterfalls, haunted castles, holographic universes, huge greenhouses, huge realistic ant farms, hydras, hyperdimensional cubes, hypersonic sand dunes, icebergs, Infinite libraries, interdimensional portals, invisible rivers, mutated krakens, large solar panel arrays, lava and magma rivers, levitating temples, liquid light, living constellations, living forests, living lightning bolts, living mountains, living statues, living storms, Living vines, Luminescent fungi, Lunar forests, magical liches, magnetic whirlwinds, mega-huge gigantic ocean-going ships and vessels, megasharks and skeletons and relics of them, memory mazes, Memory palaces, merfolk, minotaurs, mirror dimensions, Mirror lakes, mirror mazes, molten landscapes, moonlit battlegrounds, Mystic auroras, Mystic fogs, nano-swarms, naturally forming and artificial lasers, nebula nurseries, Nebulae oceans, necromancer crypts, neon jungles, obsidian fortresses, orcs, paradox loops, parallel universes, pegasi, perpetual eclipses, Phantasmal echoes, phase-shifted cities, phoenixes, pixies, Plasma dragons, plasma tornadoes, pocket dimensions, probability storms, psychic nexuses, psychic storms, pulsar beams, quantum coral reefs, quantum mirages, quantum rainbows, quantum realms, Quantum waterfalls, quantum whispers, reality anchors, reality bubbles, reality glitches, rogue AI swarms, Rune-inscribed monoliths, self-replicating nanobots, sentient crystal clusters, sentient crystals, sentient fog, sentient nebulae, sentient shadows, Sentient stars, sentient storms, serpentine rivers, Shadow puppeteers, shadow realms, shape-shifting mountain ranges, shape-shifting sands, shattered spheres, Shimmering mirages, Singing sand dunes, singularity seeds, Solar flares, Space-time anomalies, specific d&d artifacts, spectral apparitions, stalactites, star forges, star-eating black holes, Starlight sanctuaries, steep striped pyramids, Storm-chasing ships, sub-zero geysers, techno-organic hybrids, Teleporting islands, temporal echoes, temporal rifts, temporal whirlpools, tesseract temples, thought bubbles, Thunderous silence, time rifts, Time-worn ruins, tons and tons of scattered gold bars, Translucent butterflies, triple rainbows, trolls, tsunamis and wave formations and waterspouts, unbelievably cute and anachronistically oversized puppies, underwater cities, unusual alien hexapods, unusual sand-worms, various types of unusual rare never-before-seen dinosaurs, viral supernovas, void flowers, void whales, volcanic lightning storms, Whispering galaxies, whispering willows, wild horses, zeppelins, zero-point energy."); + + public static Alias forts = new Alias("GPTForts", (string a) => $"{a} is being used as a fort.", + "monumental architecture, fort knox, the white house, the louvre, the golden temple of amritsar, the taj mahal, brutalist architecture, a massive curtain wall extending to the horizon which holds an entire city, a ponderously gigantially tall wizard's tower, a monumental glacier, sheer cliffs, a sheer crevass riddled with ledges and ice tunnels, infinite stairways, abandoned and fortified oil rigs, a magically preserved castle, interlocking moats, megaliths, arcologies, red square and the kremlin, mordor and the dark tower, narnian castles at carnaervon, the washington monument, Ancient ziggurats, Floating citadels, Underwater domes, Sky-high spires, Labyrinthine catacombs, Tree-top villages, Subterranean bunkers, Colossal stone statues, Desert fortresses, Ice palaces, Glass pyramids, Steampunk airships, Hollowed-out mountains, Volcano lairs, Enchanted windmills, Celestial observatories, Haunted mansions, Bioluminescent caves, Cloud castles, Crystal fortresses, Time-worn lighthouses, Gigantic shipwrecks, Solar-powered strongholds, Mirror mazes, Spiral towers, Quantum bunkers, Floating islands, Dragon's lairs, Cosmic temples, Celestial arches, Gravity-defying bridges, Massive clock towers, Underwater labyrinths, Mystical pagodas, Space stations, Megalithic circles, Mystic monasteries, Enchanted libraries, Alchemical laboratories, Golem workshops, Rune-inscribed walls, Celestial amphitheaters, Astral gateways, Meteorite craters, Transcendent shrines, Ghostly fortifications, Starship docks, Dimensional nexuses, Floating gardens, Fortress monasteries, Mystic ziggurats, Hanging Gardens over Mesopotamia, Great Wall across endless mountains, Petra's rose-red city in cliffs, Notre-Dame's Gothic spires in Paris, Machu Picchu atop the Andes, Minotaur's labyrinth on Crete, Submerged ruins of Atlantis, Egypt's pyramids guarding sands, Stonehenge's mystical stone circle, Viking longhouses along fjords, Colosseum as colossal fortress, Shinto shrines in cedar forests, Hagia Sophia's domes in Istanbul, Medieval castles on the Rhine, Floating temples of Tenochtitlan, Agra's Red Fort sandstone walls, Alexandria's ancient library, Alhambra overlooking Granada, El Dorado in the Amazon, Ziggurat of Ur under starlight, Celtic hillforts on Britannia's hills, Sumerian cuneiform temples, Tower of Babel reaching heavens, Chichén Itzá's celestial pyramids, Carcassonne's walled battlements, Castles in the Black Forest, Cappadocia's underground cities, Forbidden City in Beijing, Blue Mosque's domes in Istanbul, Rajasthan's deep stepwells, Kremlin's stars over Red Square, Gothic abbeys in countryside, Silk Road desert fortresses, Genghis Khan's Mongolian encampments, Anasazi cliff dwellings, Sunken continent of Lemuria, Hieroglyphic obelisks in Luxor, Meteora's cliff-top monasteries, Malta's megalithic temples, Camelot, Arthur's stronghold, Neuschwanstein in alpine forests, Lalibela's rock-hewn churches, Ancient citadel of Aleppo, Scottish Highlands' castles, Persepolis in ancient Persia, Ghost city Fatehpur Sikri, Dubrovnik's walls by the sea, Epidaurus's echoing amphitheaters, Potala Palace in the clouds, Matera's rock-cut city, Knossos's palace on Crete"); + + ///okay this one is awesome. + public static Alias compositionTypes = new Alias("GPTCompositions", (string a) => $"Composition style: '{a}'", + "low horizon, high horizon, gradient following exploratory, minimalist sky, " + + "rule of thirds with extreme offset, negative space, " + + "leading lines, symmetrical, asymettrical, confrontational, hypnotically repeating, repetitive, starkly minimalist, spherical, " + + "diagonal, golden ratio geometrical compsition, spiral twirling composition, shallow depth of field with extreme bokeh, obsessed with bokeh, deceptive scale, forced perspective, " + + "textured foreground, vibrant color contrast, silhouette, " + + "dynamic tension, abstract pattern, reflective symmetry, " + + "juxtaposition, balanced elements"); + + public static Alias locations = new Alias("GPTLocations", (string a) => $"Location: '{a}'", + "Seaside cliffs of rural Seoul, the moors of central barren foggy Scotland along a plateau edge above an abandoned castle, " + + "the San Francisco bay trail in fog, " + + "The sf peninsula foggy hills above Half Moon Bay, " + + "An infinitely high cliff overlooking a beautiful detailed paradise farming world, " + + "A post-apocalyptic restoration center for humanity" + + "The Ruins of an Ancient City among the crumbling walls lost relics and symbols and abandoned empty streets of a once-great metropolis, " + + "A Floating City in the Clouds with aerial platforms and suspended walkways that provide a unique three-dimensional space, " + + "An Underwater City among submerged buildings and coral reefs with sea creatures and glowing hints of life and wear, " + + "A Volcanic Crater where Lava flows and eruptions thunder nearby revealing glowing magma, " + + "A Dense Misty Jungle where Visibility is limited and the dense unique foliage and flowers flora and fauna, " + + "A Massive Moving Train with carriages on carriages led by a speeding gigantic oversized locomotive along incredible tracks along with a small raccoon detective," + + "In the Tunnels of a Giant Ant Colony which is a A maze-like claustrophobic setting with numerous pathways," + + "A Giant Ancient Library where Countless books and shelves hid hidden mystical or scientific knowledge beyond the ken of normal men inhabited by appropriately studious creatures, " + + "An Abandoned Amusement Park with roller coasters and haunted houses and and carousels, " + + "A Network of Ice Caves with Slippery surfaces and narrow passageways and a constant threat of collapse inhabited by ice denizens," + + "0 BC Rome - Bustling streets of Ancient Rome with senators gladiators and grand architecture like the Colosseum, " + + "Ancient Neanderthal Caves in Europe - Primitive cave dwellings adorned with early human paintings and artifacts," + + "1200s Mayan Empire Temple Top - A vibrant Mayan temple amidst the dense jungle filled with intricate carvings and astronomical alignments," + + "15th Century Forbidden City in Beijing - Majestic palaces and gardens showcasing Imperial China's grandeur," + + "1940s Paris during WWII - War-torn yet resilient Paris with clandestine meetings and the spirit of resistance," + + "Victorian London in the 1800s - Foggy streets horse-drawn carriages and the burgeoning industrial revolution," + + "Ancient Egyptian Pyramids Giza circa 2500 BC - Monumental pyramids and the Sphinx with bustling construction and ancient rituals," + + "Edo Period Tokyo (1603-1868) - Traditional Japanese architecture samurai warriors and vibrant street life," + + "Viking Settlement in Scandinavia 9th Century - Rugged landscapes with longships mead halls and Nordic culture," + + "Renaissance Florence in the 15th Century - Birthplace of the Renaissance filled with art innovation and political intrigue," + + "1920s New York City during the Jazz Age - Skyscrapers rising speakeasies bustling and the Harlem Renaissance in full swing," + + "Ancient Athens during the Golden Age (5th Century BC) - Philosophers orators and the Parthenon symbolizing the cradle of democracy," + + "Mughal Empire in Delhi 17th Century - Opulent palaces and gardens epitomized by the Taj Mahal," + + "Medieval Constantinople 10th Century - A crossroads of cultures with Byzantine and Ottoman influences," + + "Aztec Capital Tenochtitlán 15th Century - An island city with intricate canals bustling markets and grand temples," + + "18th Century Versailles during the French Monarchy - Extravagant palace life with opulent balls and intricate political plots," + + "Ancient Carthage 3rd Century BC - A powerful Mediterranean port city with a mix of cultures and naval dominance," + + "Gold Rush San Francisco 1850s - Rapid growth and diversity amidst the pursuit of fortune," + + "Indus Valley Civilization 2500 BC in Mohenjo-Daro - Advanced urban planning and mysterious script in one of the world's earliest major cities," + + "Sparta in the Classical Greek Period 5th Century BC - A city-state renowned for its military discipline and austere lifestyle, " + + "A Desert Oasis with lush greenery and water springs amidst vast sand dunes sheltering diverse wildlife," + + "A High-Speed Space Station orbiting a distant planet with futuristic technology and panoramic views of the cosmos," + + "An Overgrown Ruined Castle shrouded in ivy and history echoes of ancient battles and forgotten tales," + + "A Crystal Cavern sparkling with multi-colored gems and crystal formations reflecting light in dazzling patterns," + + "A Skyborne Archipelago with floating islands connected by rope bridges harboring unique flora and fauna," + + "A Deep-Sea Abyss where bioluminescent creatures and strange geological formations exist in perpetual darkness," + + "A Futuristic Metropolis with towering skyscrapers neon lights and advanced technology teeming with life," + + "A Wild West Ghost Town with dusty streets abandoned saloons and a sense of a time gone by," + + "A Mystical Forest with enchanted trees whispering secrets magical beings and a sense of wonder," + + "An Arctic Research Base isolated in a snowy expanse with cutting-edge facilities and a harsh climate.," + + "The windswept plains of a post-apocalyptic Earth sorrowful yet simple.," + + "An ancient ruin reclaimed by sand windswept yet retaining geometrical beauty.," + + "An endless golden grain field beneath an alien yellow sky - solitude and freedom.," + + "Wonderland from Alice in Wonderland - Curiosity and confusion in a nonsensical realm where magic mushrooms make you grow or shrink and time runs backward," + + "The Shire from Lord of the Rings - Fear and despair in the bleak ashen plains surrounded by volcanoes where Sauron's eye watches endlessly.," + + "Narnia from The Lion The Witch and the Wardrobe - Good triumphing over evil in a land watched over by a noble lion and currently cast under an endless winter.," + + "An all-white limbo space influenced by The Matrix stripped-down yet disturbed.," + + "Symmetrical brutalist structures from Equilibrium's monochromatic city perfection imposed by force.," + + "Vast Zen rock gardens with raked patterns from memoirs of Japanese monks calming and centering.," + + "An icy Watchtower like Game of Throne's Wall hardened shelter from risks unknown.," + + "A precisely planned forest village resembling aesthetics from Princess Mononoke harmoniously balanced with nature.," + + "On the side of a hill from a romantic nostalgic anime," + + "Norse Gods at Ragnarok (Dread Bravery) - The ultimate battle with fate hanging in the balance.," + + "Mythical Olympus at its Peak (Splendor Rivalry) - The home of the gods resplendent but rife with rivalries.," + + "A City on the Back of a Giant Moving Creature (Wanderlust Fear) - A nomadic city with the constant threat of the unknown.," + + "Parallel Universe New York (Bewilderment Excitement) - Familiar yet bizarrely different with endless possibilities.," + + "Ghost Ship in the Bermuda Triangle (Mystery Fear) - A haunted vessel with an eerie and uncertain fate.," + + "An Enchanted Forest with Warring Fae (Beauty Betrayal) - Ethereal landscapes shadowed by deceit and power struggles.," + + "Middle Earth's Rivendell during War (Tranquility Tension) - A serene elven haven on the brink of war.," + + "Atlantis Rising from the Sea (Wonder Awe) - The mythical city re-emerging revealing ancient technologies and mysteries.," + + "Timbuktu in the 14th Century - A thriving center of African scholarship and trade., " + + "Ancient Babylon 6th Century BC - Hanging Gardens and grand ziggurats along the Euphrates River.," + + "Machu Picchu in the 15th Century - Mysterious Incan city hidden high in the Andes Mountains.," + + "Heian-kyo (Kyoto) during Japan's Heian Period - Elegant imperial courts and the flowering of Japanese culture.," + + "19th Century St. Petersburg - Russian imperial splendor with grand canals and the Winter Palace.," + + "Angkor Wat in the 12th Century Cambodia - Majestic temples surrounded by dense jungle.," + + "Viking Age Dublin 9th Century - A bustling Norse settlement and trading hub.," + + "Venice in the Renaissance - Canals and gondolas with a flourishing of arts and commerce.," + + "Jerusalem during the Crusades - A city at the crossroads of religions and conflicts.," + + "Han Dynasty China's Silk Road Cities - Exotic trade routes connecting East and West.," + + "Istanbul during the Ottoman Empire - A melting pot of cultures bridging Europe and Asia.," + + "The Roaring Twenties in Chicago - Jazz prohibition and the rise of organized crime.," + + "Baghdad during the Islamic Golden Age - Center of learning and culture with the House of Wisdom.," + + "Pompeii just before the eruption of Vesuvius in 79 AD - Daily life in an ancient Roman city.," + + "Ancient Athens at the time of Pericles - Flourishing arts and philosophy in the cradle of democracy.," + + "Elizabethan London in the late 16th Century - The time of Shakespeare and the Globe Theatre.," + + "The Hanseatic League in Medieval Lübeck - A rich and powerful merchant city-state.," + + "Harlem during the Harlem Renaissance - A flourishing of African-American arts and culture.," + + "Pre-Columbian Cusco capital of the Inca Empire - Rich in culture and architecture.," + + "Mohenjo-Daro in the Indus Valley Civilization 2500 BC - An advanced ancient city with intricate urban planning.," + + "Ancient Alexandria in the Hellenistic Period - A center of learning and culture home to the Great Library.," + + "Teotihuacan 1st Century AD - Home to the Pyramid of the Sun a pre-Columbian architectural wonder.," + + "The Klondike Gold Rush 1890s in Yukon Canada - Thrill and hardship in the search for gold.," + + "The Height of the Ottoman Empire in 16th Century Istanbul - A cosmopolitan hub of the world.," + + "Feudal Japan during the Time of Samurai - Castles and cherry blossoms and and warrior code.," + + "Ancient Polynesian Settlements in Hawaii - Early navigators and unique island culture.," + + "Colonial Williamsburg in the 18th Century - A living history of American colonial life.," + + "The Grandeur of the Mughal Empire in 16th Century India - Exquisite architecture and rich culture," + + "Ancient Rome at its Zenith under Emperor Trajan - A cosmopolitan empire at its peak," + + "The Ming Dynasty's Forbidden City in 15th Century Beijing - The imperial palace complex at the heart of China"); + + public static Alias artStyles = new Alias("GPTArtstyles", (string a) => $"The art style is: '{a}'", "Aquarela brasileira, 3D, 3D model, 3D printing art, Abstract, Abstract geometry, Acrylic pour, Action painting, Aerial drone art, Ancient pottery techniques, Animación stop motion, Anime, Ansel Adams-like stark, Apocalyptic, Apocalyptic cityscape, Architectural model building, Arte cinético (Spanish) Arte digital (Spanish),Arte povera (Italian), Arte mosaico bizantino, Arte quilling decorativo, Arte urbano graffiti, Augmented reality, Bamboo weaving, Baroque, Barro negro oaxaqueño, Basquiat-inspired street, Batik javanés, Bead weaving, Berniniesque dynamic, Bijutsu shashin (美術写真),Black velvet, Bioluminescent installation, Blackwork embroidery, Body painting, Bone carving, Book folding, Bordado punto cruz, Bordado suzani, Boschian fantastical, Botticellian grace, Brancusiesque simplified, Bruegelian detailed, Calado en madera, Caligrafía árabe, Calligraphic, Calligraphic graffiti fusion, Calligraphic lettering, Calligraphy, Candle crafting, Caravaggesque chiaroscuro, Cascading light display, Cerámica gres, Cerámica raku abstracta, Cestería mapuche, Chagallesque floating, Chainmaille fabric, Chalk street art, Prismatic Spray wave image, Cinematic virtual scenery, Clay animation storytelling, Clay pottery, Coffee art, Collage, Color-shifting mural art, Conceptual art Conceptual space exploration, Constable-inspired pastoral, Constructivist, Coral assemblage, Coral reef painting, Cosmic, Cosmic art, Cosmic photography, Courbet-like realism, Crackle glaze, Cubism, Cubist, Cubist figure, Cyanotype print, Cybernetic, Cybernetic organism, Cyberpunk, Cézannesque geometric, Da Vincian intricate, Dada, Daliesque surreal Danza butoh visual, Danza contemporánea (Spanish),contemporary dance., Deco, Deco elegance, Dibujo carboncillo expresivo, Digital 3D, Digital fabric weaving, Digital mosaic, Digital painting, Digital vector graphics, Dot painting, Dreamy, Dreamy landscape, Drybrush technique, Dystopian landscape sketch, Ebru sanatı, Ecologicalism, Eco-friendly sculpture, Eco-sculpture, Embroidered, Embroidered fabric, Embroidery, En plein air, Encaustic painting, Ephemeral sand portraits, Escheresque impossible, Escultura hielo luminoso Escultura reciclaje innovador, Estampa ukiyo-e, Ethereal, Ethereal glass sculpture, Ethereal vision, Experimental shadow theater, Expressionist, Fabric batik, Fauvist, Faux finish, Feather art, Felted wool landscapes, Fiber optic tapestry, Filigrana italiana, Fire ink drawing, Floral arrangement, Fluid art, Fluorescent body art, Folk, Folk embroidery, Food carving, Fotografía callejera monocromo, Fractal laser etching, Fresco, Fridaesque personal, Futurist, Futuristic, Futuristic skyline, Galactic spray paint, Gauguinesque exotic, Geodesic dome mural, Geometric, Glacial, Glacial sculpture, Glass blowing, Glass etching, Glitch overload, Glowing ice sculpture, Gold leaf, Gothic, Gothic cathedral, Goyesque grotesque, Grabado linóleo detallado, Graffiti, Graffiti wall, Graffito, Gravura em madeira,Gyotaku fish print, Hand-dyed fabric art, Handmade paper making, Handwoven light patterns, Haptic VR art, Haute couture,Highbrow, Highbrow, Highbrow critique, Hokusai-inspired wave, Holographic art, Holographic display, Holographic street art, Hopperesque solitude, Hyperbolic tessellation art, Hyperreal, Hyperrealism sketch, Ice, Ice carving, Ice sculpting, Ice sculpture, Iconografía bizantina, Glacier carving, fire-melted ice, Lava sculpture, Ikebana (生け花),Mixed media collage, Ilustración digital fantasía, Immersive virtual reality, Impressionist, Ink brush, Ink wash, Instalación arte lumínico, Instalación artística (Spanish),Intaglio, Instalación sonido inmersivo, Interactive light installation, Interactive mural Interactive sidewalk chalk, Iridescent bubble art, Joyería artesanal étnica, Junk sculpture, Kahloesque vivid, Kalamkari indio, Kilning, Kinetic, Kinetic mobile, Kinetic sand drawing, Kinetic sculpture, Kinetic wind art, Kintsugi (金継ぎ),Kintsugi repair, Klimtesque ornate, Lace making, Lacquer, Land art, Large-scale mural painting Laser-cut paper art, Leather tooling, Levitating sculpture art, Light painting, Light projection, Linocut print, Liquid metal sculpture, Litho, Litho print, Lithograph, Lost wax casting, Lowbrow, Lowbrow humor, Luces y sombras (Spanish), Light and shadows, Luminous thread embroidery, Macabre, Macro photography, Magnetic fluid art, Magrittesque mysterious, Mandala design, Manetesque modern, Manga, Manga sketch, Marbling, Metal forging, Michelangelesque heroic, Miniature book binding, Miniature model Minimal, Minimal design, Minimalist, Mixed reality installation, Mokuhanga (木版画),Batik tulis,Céramique raku,Papiroflexia,Arte callejero Joie de vivre,Sgraffito (Italian),Guóhuà (国画),traditional Chinese painting., Monetesque impressionist, Mosaic, Munchian angst, Mural, Mural street, Muralismo digital interactivo Muralismo mexicano (Spanish),Mexican muralism., Máscara veneciana, Natural landscape photography, Needle felting, Neon, Neon calligraphy, Neon portrait, Neon wireframe sculptures, Nihonga, Noir, Noir mystery, O'Keeffesque enlarged, Optical, Optical art, Optical illusion, Organic ceramic forms, Origami, Origami crane, Origami tessellation, Paisaje sonoro urbano, Panoramic digital fresco, Papel picado mexicano, Paper cutting, Paper mâché, Paper quilling, Papiroflexia modular avanzada, Parchment scroll, Pastel, Pastel drawing, Peinture abstraite abstract painting., Peisaj urban (Romanian),urban landscape., Performance art, Performance teatro callejero, Perfume blending, Phosphorescent forest art, Photoreal, Pintura al fresco, Pintura al óleo (Spanish),Pyrography, Pintura sumi-e, Pintura óleo realista, Pirograbado ruso, Pixel art, Pixel-perfect manual editing, Pixel animation, Pixelated design, Pixelated character, Pollock-style drip, Pop Art, Pop icons, Primitivism, Programmable matter sculpture, Proyección mapping dinámico, Psychedelic, Psychedelic swirl, Qi baishi (齐白石),Chinese ink painting style., Quantum dot canvas, Quilling art, Quilting, Quink art, Raku, Rakú japonés, Raphaelesque serene, Reactive sound mural Real-time animation projection, Recycled art, Relief carving, Relieve precolombino, Rembrandtesque dramatic, Resin art, Retro, Retro diner, Retrofuturism, Robotic arm painting, Rodinesque sculptural, Rothko-inspired abstract, Rubenesque voluptuous, Rustic, Rustic barn, Rustic pottery, Salt flat mirage, Salt painting, Sand, Sand animation, Sand castle, Sand mandala, Scented ink calligraphy, Scherenschnitte (German),Scrap metal sculpture, Seuratesque pointillist, Sgraffito, Sgraffito etching, Shadow art, Shadow play, Shell mosaic Shibori dyeing, Silhouette, Silhouette cutout, Silk embroidery, Site-specific performance art, Snow architecture, Soap sculpture, Solar plate etching, Solar reactive painting, Sound installation, Sound wave art, Sound-activated light art, Stained glass, Steampunk, Steampunk gadget, Stencil, Stenciled, Stenciled message, Stone masonry, Sumi-e ink, Surreal, Surreal dreamscape, Surrealist, Ta moko (Māori),traditional skin art., Talla piedra ancestral, Tape art, Tapestry, Tapestry weave, Tapiz flamenco, Tapiz mural contemporáneo, Terrazzo flooring, Textile dyeing Thangka, Thermal imaging, Thermal reactive murals, Three-dimensional street art, Time-lapse drawing, Tinta china (Spanish),Chinese ink, Tissage africain, Titianesque vibrant, Toulouse-Lautrec-esque nightlife Traditional woodblock printing, Tribal, Tribal mask, Trompe l'oeil, Trompe-l'œil,Ukiyo-e, Turner-inspired atmospheric, Técnica collage mixto, Ultraviolet landscape art, Urban, Urban decay photography, Urban sketching, Urban sprawl, Van Goghian swirl, Vaporwave, Vaporwave scene, Vermeeresque light, Vexel illustration, Vintage photo restoration, Virtual sculpture, Vitrail, Vitraux gothique, Warholian pop, Watercolor Watercolor garden, Weaving, Wheatpaste poster, Wire sculpture, Wood carving, Woodblock, Woodblock print, Woodcut, Wool felting, Xylography, Yarn bombing, Yarnbomb, Zen philosophy and koans, Zen garden, Zen garden design, Zentangle drawing, 2D Animation, 360-Degree Video, 3D Animation, 3D Printing Art, AI-Generated Art, ASCII Art, Abstract Expressionism, Absurdist Art, Activist Art, Aerial Photography, Agitprop Art, Algorithmic Art, Allegorical Art, Altered Books, Anti-Art, Appropriation Art, Aquatint, Arabic Calligraphy, Architectural Design, Architectural Sculpture, Art Brut, Art Informel, Art Nouveau, Arte Nucleare, Arte Povera, Artists' Books, Assemblage Art, Astronomical Art, Astrophotography, Augmented Reality Art, Aura Art, Automata, Bad Painting, Bas-Relief, Batik, Beadwork, Bio Art, Bio-Architecture, Biohacking Art, Biomorphism, Blacksmithing, Body Art, Bonsai, Bonsai Art, Book Arts, Book Sculpture, Bookbinding, Botanical Illustration, Brush Painting, Byzantine Art, CNC Art, Camp Art, Capitalist Realism, Caricature, Cartooning, Celtic Knotwork, Ceramic Art, Chakra Art, Chinese Calligraphy, Cityscape Painting, Claymation, Cloisonné, CoBrA Movement, Code Art, Collage Art, Collagraph, Color Field Painting, Comic Art, Community Art, Computer Graphics Art, Conceptual Art, Constructivism, Contextual Art, Critical Regionalism, Cut-Out Animation, Cyborg Art, DNA Art, Dadaism, Data Moshing, Data Visualization, De Stijl, Decoupage, Deep Dream Art, Digital Art, Digital Collage, Digital Painting, Drone Photography, Drypoint, Earthworks, Eco-Art, Egg Tempera Painting, Embossing, Embroidery Art, Enamel Art, Encaustic Art, Encaustic Painting, Endurance Art, Engraving, Entoptic Imagery, Environmental Art, Environmental Graphics, Environmental Sculpture, Etching, Exhibition Design, Expressionism, Fabergé Eggs, Fantastic Realism, Fantasy Art, Fauvism, Fiber Art, Fiberglass Sculpture, Figuration Libre, Figure Painting, Flipbooks, Floral Design, Fluxus, Foam Sculpture, Folk Art, Found Object Art, Found Object Sculpture, Fractal Art, Fresco Painting, Futurism, GAN Art, GIF Art, Garden Design, Gemstone Carving, Generative Art, Genetic Art, Genre Painting, Geometric Abstraction, Geometric Art, Gigapixel Imaging, Gilding, Glass Art, Glassblowing Art, Glitch Art, Gold Leaf Art, Gothic Art, Gouache Painting, Graffiti Art, Graffiti Calligraphy, Graphic Novels, Green Roofs, Guerilla Art, Guohua, Gutai Group, Happenings, Hard-edge Painting, Henna Art, High Relief, High-Speed Photography, History Painting, Holography, Hyperrealism, Ice Sculpture, Iconography, Ikat, Ikebana, Illuminated Letters, Illuminated Manuscripts, Illustration, Immersive Installations, Immersive Media, Impressionism, Indigenous Art, Infographics, Information Design, Infrared Photography, Ink Wash Painting, Inlay Art, Installation Art, Intaglio, Intarsia, Interactive Installations, Interactive Panoramas, Interior Design, Internet Art, Intervention Art, Islamic Geometric Art, Jewelry Design, Kinetic Architecture, Kinetic Art, Kinetic Sculpture, Kinetic Typography, Kintsugi, Kirigami, Kitsch Art, Lace Making, Land Art, Landscape Architecture, Landscape Painting, Laser Art, Letterpress, Lettrism, Light Art, Light Field Photography, Linocut, Lithography, Living Walls, Lost Wax Casting, Lowbrow Art, Lyrical Abstraction, Macro Photography, Magic Realism, Mail Art, Mandala Art, Manga Art, Mannerism, Marquetry, Massurrealism, Matte Painting, Maximalism, Mechanical Art, Medical Illustration, Meme Art, Metal Casting, Metal Leaf Art, Metalwork Art, Mezzotint, Microbial Art, Microscopic Art, Minimalism, Mixed Media, Mixed Reality Art, Monochrome Painting, Monotype, Monumental Art, Mosaic Art, Moss Art, Motion Graphics, Motion Graphics Art, Mughal Miniature, Multisensory Art, Mural Art, Murano Glass, Museum Design, Mythological Art, Narrative Art, Naïve Art, Neo-Dada, Neo-Expressionism, Neo-Geo, Neoclassicism, Net Art, Neural Style Transfer Art, Nordic Runes, Nouveau Réalisme, Op Art, Open Source Art, Optical Art, Organic Abstraction, Origami Architecture, Orphism, Outsider Art, Panorama Photography, Paper Engineering, Papercutting Art, Papier-mâché, Participatory Art, Pattern Art, Performance Art, Persian Miniature, Phenakistoscopes, Photomontage, Photorealism, Pixel Art, Plop Art, Pointillism, Political Art, Polymer Clay Sculpture, Pop Surrealism, Pop-Up Books, Porcelain Art, Portrait Painting, Post-Impressionism, Pre-Raphaelite Art, Procedural Art, Projection Art, Projection Mapping, Propaganda Art, Psychedelic Art, Public Art, Puppet Animation, Purism, Quilting Art, Raku Pottery, Rayonism, Realism, Recycled Art, Reduction Printing, Relational Aesthetics, Relief Carving, Relief Sculpture, Religious Art, Renaissance Art, Resin Casting, Retail Design, Reverse Graffiti, Ritual Art, Robotic Art, Rococo, Romanticism, Sacred Art, Sand Mandala, Sand Sculpture, Satirical Art, Science Fiction Art, Scientific Illustration, Screen Printing, Seascape Painting, Shadow Puppetry, Shamanic Art, Shanshui Painting, Shaped Canvas, Shibori, Silhouette Animation, Site-Specific Art, Situationist International, Social Practice Art, Software Art, Sots Art, Sound Art, Space Art, Stained Glass, Steam Engine Art, Sticker Art, Still Life Painting, Stone Balancing, Stone Carving, Stop Motion Animation, Street Art, Stuckism, Sumi-e, Sumi-e Ink Painting, Superflat, Suprematism, Surrealism, Sustainable Art, Symbolism, Symmetry Art, Synchromism, Tachisme, Tactical Media, Tantric Art, Tapestry Weaving, Tattoo Art, Tenebrism, Terra Cotta Art, Tessellation Art, Time-Lapse Photography, Topiary, Topiary Art, Totemic Art, Toyism, Transavantgarde, Tribal Art, Typography Art, Ukiyo-e, Ultraviolet Photography, Underwater Photography, Upcycling Art, Urban Design, Video Art, Video Installation, Virtual Reality Art, Virtual Tour Photography, Visionary Art, Vorticism, Voxel Art, Watercolor Painting, Wayfinding Design, Wheatpaste Art, Wildlife Art, Wire Sculpture, Wood Carving, Yarn Bombing, Zen Ink Painting, Zero Group, Zines, Zoetropes, Zombie Formalism"); + + private string GetRandomSampling() + { + var sampledAliases = new List(); + + var guys = new List() {aggressivelyFantastic, forts, locations, compositionTypes, artStyles, weatherPhenomena }; + guys = guys.OrderBy(x => Random.Shared.Next()).ToList(); + foreach (var alias in guys) + { + var val = alias.GetRandomAlias(true); + if (!string.IsNullOrEmpty(val)) + { + sampledAliases.Add(val); + } + if (sampledAliases.Count >= 1) + { + break; + } + } + return string.Join(", ", sampledAliases); + } + + public Task DoTransformation(PromptDetails pd) + { + var randoms = GetRandomSampling(); + var newVersion = $"'{pd.Prompt}' {randoms}"; + pd.ReplacePrompt(newVersion, newVersion, TransformationType.Randomizer); + + return Task.FromResult(true); + } + } +} diff --git a/MultiImageClient/promptTransformation/StylizerStep.cs b/MultiImageClient/promptTransformation/StylizerStep.cs index 1d4a112..1b61892 100644 --- a/MultiImageClient/promptTransformation/StylizerStep.cs +++ b/MultiImageClient/promptTransformation/StylizerStep.cs @@ -1,33 +1,35 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime; -using System.Text; -using System.Threading.Tasks; -using MultiImageClient.Implementation; - -namespace MultiImageClient -{ - - public class StylizerStep : ITransformationStep - { - private static Random random = new Random(); - - public string Name => nameof(StylizerStep); - - public static string GetRandomArtist() - { - var artists = "3D model, A. R. Penck, A. Y. Jackson, Aardman Animations, Aaron Blaise, Aaron Douglas, Aaron James Draplin, Aaron Reed, Aaron Westerberg, Abanindranath Tagore, Abbas Kiarostami, Abbey Lossing, Aberdeen Bestiary, Aboudia, Abraham Ortelius, Abstract D'Oyley, Achille Castiglioni, Acrylic pour, Action movie, Adam Kubert, Adam Neate, Adam Zyglis, Adi Granov, Adolf Schreyer, Adriaen Brouwer, Adrian Frutiger, Adrianna Papell, Adrienne Segur, Adventure Time, Aelbert Cuyp, Aerial view, Aerin Lauder, African mask, Age of Empires, Agnieszka Lorek, Agostino Iacurci, Aibo, Aida Muluneh, Aimee Stewart, Aires Mateus, Aitor Throup, Alan Aldridge, Alan Fearnley, Alan Kitching, Alan Moir, Alasdair Gray, Alaya Gadeh, Albert Dros, Albert Dubout, Albert Edelfelt, Albert Irvin, Albert Koetsier, Albert Namatjira, Albert Oehlen, Albert Paley, Albert Renger-Patzsch, Alberta Ferretti, Alberto Campo Baeza, Alberto Vargas, Aldo Rossi, Ale Giorgini, Alec Monopoly, Alejandro Gonzalez Inarritu, Aleksi Briclot, Alessandro Mendini, Alex Chinneck, Alex Janvier, Alex Katz, Alex MacLean, Alex Majoli, Alex Raymond, Alexander Girard, Alexander Payne, Alexandre Deschaumes, Alexej von Jawlensky, Alexey Brodovitch, Alfonso Cuaron, Alfred Dehodencq, Alfred Sisley, Alfred Wallis, Alice Kettle, Alice Pike Barney, Alicja Kwade, Alida Akers, Alighiero Boetti, Aline Kominsky-Crumb, Alisa Burke, Allan Kaprow, Alma Thomas, Alphonse Bertillon, Alvaro Castagnet, Amanda Blake, Amanda Sage, Ambient lighting, Ambrogio Lorenzetti, American Girl doll, Ami James, Amigurumi, Amy Brown, Amy Giacomelli, Amy Sol, Ana Mendieta, Anaglyph colors, Andre Courreges, Andre Masson, Andrea Galvani, Andrea Palladio, Andrea Torres Balaguer, Andreas Rocha, Andrew Loomis, Andrew Read, Andrew Salgado, André Franquin, Andy Gilmore, Andy Kubert, Andy Marlette, Andy Rementer, Andy Russell, Angelica Kauffman, Angelo Donghia, Angie Lewin, Angry Birds, Anila Quayyum Agha, Animal Crossing, Animalier fashion, Anita Jeram, Ann Mortimer, Ann Telnaes, Ann Veronica Janssens, Anna Atkins, Anna Griffin, Anna Maria Garthwaite, Anna Pugh, Anna Silivonchik, Anna Sui, Annabelle doll, Anne Cotterill, Anne Gifford, Anne Redpath, Annette Schmucker, Annibale Carracci, Annie Ovenden, Anonymous mask, Anselm Feuerbach, Anthea Hamilton, Anthony Caro, Anthony Gerace, Anthony McCall, Antoine Bourdelle, Antoine Pevsner, Antonio Lopez, Antonio Stradivari, Antony Cairns, Arata Isozaki, Archibald Motley, Architectural plan, Arkanoid, Arnaldo Pomodoro, Arne Svenson, Arnold Genthe, Arnold Lobel, Art Baltazar, Arthur Claude Strachan, Arthur Frank Mathews, Arthur John Elsley, Arthur Melville, Arthur Suydam, Arthur Szyk, Artificial lighting, Aryz, Ashlar texture, Ashraful Arefin, Assemblage, Assembly drawing, Astrological chart, Astronomical chart, Asymmetrical composition, Audemars Piguet, Auguste Perret, Augustus Edwin John, Autochrome print, Autostereogram, Autumn, Axel Vervoordt, Axonometric view, Aztec mask, Azzedine Alaia, B. Kliban, Backlit, Bada Shanren, Baggy fashion, Barbara Chase-Riboud, Barbara Chichester, Barbara Kasten, Barbie, Barnett Newman, Barong mask, Barry Blitt, Barry Windsor-Smith, Bas Princen, Bastien Grivet, Baz Luhrmann, Beachwear fashion, Beatrice Alemagna, Beatriz Milhazes, Befana, Bela Kadar, Ben Eine, Ben Garrison, Ben Heine, Benin art, Benito Jacovitti, Benoit Paille, Bentwood, Bernard Leach, Bernard Plossu, Bernardo Bellotto, Bernd and Hilla Becher, Bernie Fuchs, Bertha Lum, Berthe Morisot, Beryl Cook, Beth Cavener, Beth Moon, Bethany Lowe, Betty Boop doll, Betty Woodman, Betye Saar, Beverly Joubert, Bijou Karman, Bill Amend, Bill Bell, Bill Cunningham, Bill Eppridge, Bill Leak, Bill Owens, Bill Plympton, Bill Schorr, Bill Willingham, Billie Tsien, Billy Wilder, BioShock, Bioluminescent colors, Bjarne Melgaard, Black-and-white colors, Blade Runner, Blek le Rat, Blizzard, Blue Sky Animation Studio, Blue hour, Board game, Bob Coonts, Bob Englehart, Bob Kane, Bob Mackie, Bob Peak, Bob Pepper, Bob Staake, Bob the Builder, Bonnie Marris, Book of Kells, Boris Groh, Boris Mikhailov, Bottom view, Bouchra Jarrar, Brandon Kidwell, Bratz doll, Brent Stirton, Brian Michael Bendis, Brian Stelfreeze, Brom, Bruce McLean, Bruce Onobrakpeya, Buffy the Vampire Slayer, Bumpy texture, Bunny Williams, Bunraku, Butch Hartman, Buzz Lightyear, Byzantine icon, CAD drawing, CFA Voysey, Cake design, Call of Duty, Cameo, Camilla d'Errico, Camille Rose Garcia, Candice Olson, Candlelit, Capability Brown, Carel Fabritius, Carey Chen, Caricature, Carl Brenders, Carl Mydans, Carl Wilhelmson, Carlo Dolci, Carlo Scarpa, Carlos Latuff, Carlos Schwabe, Carly Cushnie, Carmen Herrera, Carol Hagan, Carole Feuerman, Carole Spandau, Carolee Schneemann, Caroll Spinney, Carolyn Blish, Carrie Graber, Carsten Holler, Carsten Witte, Cartography, Casey Baugh, Cassius Marcellus Coolidge, Cath Kidston, Catherine Abel, Catherine Holman, Catherine Opie, Catherine Pooler, Cathy Hegman, Cathy Horvath Buchanan, Cathy Locke, Cathy Wilcox, Cathy Wilkes, Catrin Welz-Stein, Cecil Aldin, Cecil B. DeMille, Cecil Beaton, Cecil Touchon, Cecilia Beaux, Cecilie Manz, Cellular diagram, Chad Dutson, Chad Knight, Chantal Joffe, Charles Bibbs, Charles Burton Barber, Charles Conder, Charles Courtney Curran, Charles Dana Gibson, Charles Dwyer, Charles Eames, Charles Fazzino, Charles Ginner, Charles Gleyre, Charles Gwathmey, Charles Le Brun, Charles Marville, Charles Maurice Detmold, Charles Reiffel, Charles Rennie Mackintosh, Charles Robinson, Charles Sheeler, Charles Spencelayh, Charles Tunnicliffe, Charles White, Charles Wysocki, Charles-Amable Lenoir, Charles-Andre van Loo, Charles-Francois Daubigny, Charley Harper, Charley Toorop, Charlie Adlard, Charline von Heyl, Charlotte Perriand, Charred, Chemical structure, Chen Yifei, Chen Zhen, Chicano art, Chien Chung-Wei, Chiho Aoshima, Childe Hassam, Chip Zdarsky, Chris Burden, Chris Claremont, Chris Johanson, Chris Leib, Chris Ryniak, Chris Samnee, Chris Sanders, Chris Ware, Christi Belcourt, Christian Coigny, Christian Krohg, Christian Louboutin, Christian Marclay, Christian Riese Lassen, Christian Tagliavini, Christine Comyn, Christine Ellger, Christo, Christoffer Relander, Christophe Staelens, Christophe Vacher, Christopher Bucklow, Christopher Corr, Christopher Shy, Christopher Wren, Christy Lee Rogers, Christy Tomlinson, Chrome colors, Chuck Jones, Chuck Sperry, Cimabue, Cinematic lighting, Circuit diagram, Cirque du Soleil, City of God, City plan, Claes Oldenburg, Claire Basler, Claire Droppert, Claire Wendling, Clara Peeters, Clare Elsaesser, Clarence Gagnon, Clark Little, Claude Lorrain, Claude Parent, Claude-Joseph Vernet, Claudia Rogge, Claudia Tremblay, Claudio Silvestrin, Clay Bennett, Clay Mann, Clayton Crain, Clemens Ascher, Cleon Peterson, Cleve Gray, Cliff Chiang, Clifford Coffin, Clifford Harper, Clifford Possum Tjapaltjarri, Climate history graph, Clive Barker, Close-up view, Cluttered composition, Clyfford Still, Coat of Arms, Codex Mendoza, Codex Sinaiticus, Cody Ellingham, Colin Campbell Cooper, Colin McCahon, Collagraph, Color field painting, Coloring-in sheet, Colossal scale, Comedy movie, Comfy atmosphere, Command & Conquer, Commedia dell'Arte, Commedia dell'Arte mask, Complementary colors, Conor Harrington, Conrad Shawcross, Constance Guisset, Constance Spry, Constantin Brancusi, Constantin Kousnetzoff, Contour plot, Contrasted lighting, Cool colors, Coordinate system paper, Copperplate engraving, Corey Arnold, Corn husk doll, Cornell Capa, Counter-Strike, Cracked surface, Craigie Aitchison, Craquelure, Crayon Shin-chan, Crinkled texture, Cristina Coral, Cristina McAllister, Cross-section diagram, Cross-section plan, Csaba Markus, Cubism, Cuneiform, Cutaway view, Cy Twombly, D. W. Griffith, DAIN, DALeast, Daarken, Daisuke Yokota, Dan Abnett, Dan Brereton, Dan Graham, Dan Povenmire, Dan Slott, Dan Spiegle, Dan Witz, Dana Trippe, Danh Vo, Daniel Buren, Daniel Eskridge, Daniel Garber, Daniel Kordan, Danny Clinch, Danny O'Connor, Dante Gabriel Rossetti, Daphne Odjig, Darek Zabrocki, Daria Endresen, Daria Petrilli, Dariusz Klimczak, Dark atmosphere, Dark foreboding colors, Daron Nefcy, Darrell K. Sweet, Daryl Cagle, Dashiki fashion, Data visualization, Dav Pilkey, Dave Coverly, Dave Granlund, Dave Heath, Dave Sim, Dave Stevens, Dave Whamond, David A. Hardy, David Adjaye, David Aja, David Alfaro Siqueiros, David Altmejd, David Bellemere, David Bomberg, David Bowers, David Brayne, David Bromley, David Brown Milne, David Burnett, David Cerny, David Choe, David Cox, David Downton, David Drebin, David Driskell, David Goldblatt, David Hammons, David Hettinger, David Horsey, David Hurn, David Inshaw, David Keochkerian, David Lean, David Ligare, David Lloyd Glover, David Mann, David Martiashvili, David Michael Bowers, David Mould, David Nash, David Octavius Hill, David Palumbo, David Plowden, David Pope, David Rees, David Renshaw, David Roberts, David Rowe, David Salle, David Seymour, David Shepherd, David Sims, David Teniers the Younger, David Trubridge, David Tutwiler, David Uhl, David Walker, David Welker, David Wightman, David Wilkie, David Yarrow, Davide Sorrenti, Dawn, Dayak art, Dead Cells, Dean Crouser, Deana Lawson, Debbie Criswell, Deborah Azzopardi, Deborah Turbeville, Declan Shalvey, DeepDream, Deirdre Sullivan-Beeman, Del Kathryn Barton, Delftware, Delphin Enjolras, Delphine Diallo, Demetre Chiparus, Dennis Oppenheim, Dennis Stock, Denys Lasdun, Depressed atmosphere, Derek Gores, Derek Jarman, Desaturated colors, Detailed, Diablo, Diagonal composition, Dick Bruna, Dick Giordano, Dick Sprang, Didier Lourenco, Diego Dayer, Dieter Rams, Diffused lighting, Digimon, Dim lighting, Dima Dmitriev, Dimitry Roulland, Dina Wakley, Dion Lee, Disco Elysium, Disney Studio, Distressed Photo, Doctor Who, Dod Procter, Dogon art, Domenichino, Don Blanding, Don Dixon, Don Heck, Don Hertzfeldt, Donald Judd, Donald Zolan, Donatella Versace, Donkey Kong, Donna Huanca, Doodle, Doom, Dora Carrington, Dorina Costras, Doris Salcedo, Dorothea Sharp, Dorothy Draper, Dorothy Johnstone, Doug Aitken, Doug Chinnery, Doug Hyde, Doug Marlette, Doug TenNapel, Douglas Gordon, Douglas Kirkland, Douglas Smith, Dr. Seuss, Dragon Quest: The Adventure of Dai, Drama movie, Dramatic lighting, Dran, Drawing grid paper, Dreamworks, Dreamy atmosphere, Dries van Noten, Duane Hanson, Duccio, Dungeons and Dragons, Duotone colors, Dusk, Duy Huynh, Dwight William Tryon, E. J. Bellocq, Earle Bergey, Earthy tones, Eastman Johnson, Ebru Sidar, Ed Hardy, Ed McGuinness, Ed Templeton, Eddie Colla, Edgar Maxence, Edith Head, Edmund Tarbell, Edmund de Waal, Edoardo Tresoldi, Edouard Boubat, Edouard Cortes, Eduard Gaertner, Eduard Veith, Eduardo Chillida, Eduardo Paolozzi, Eduardo Souto Moura, Edward Ardizzone, Edward Atkinson Hornel, Edward Bawden, Edward Blair Wilkins, Edward Cucuel, Edward Henry Potthast, Edward Julius Detmold, Edward Okun, Edward Poynter, Edward Wadsworth, Edwin Henry Landseer, Edwin Landseer, Edwin Lord Weeks, Edwin Lutyens, Eerie atmosphere, Eero Aarnio, Egyptian funeral mask, Eiko Ojala, Eikoh Hosoe, Eileen Gray, Elaine Lustig Cohen, Eleanor Antin, Eleanor Vere Boyle, Electrical layout, Electromagnetic spectrum chart, Elena Kotliarker, Elevation drawing, Elger Esser, Elia Kazan, Elia Locardi, Elihu Vedder, Elisabeth Fredriksson, Elisabeth Sonrel, Elisabeth Vigee Le Brun, Elise Palmigiani, Elizabeth Murray, Elizabeth Nourse, Elizabeth Peyton, Elizabeth Shippen Green, Elke Vogelsang, Ellsworth Kelly, Elmer Bischoff, Elsa Schiaparelli, Emanuel Ungaro, Embossed texture, Emil Alzamora, Emil Carlsen, Emil Melmoth, Emile Claus, Emile Vernon, Emiliano Ponzi, Emily Balivet, Emily Carr, Emily Henderson, Emily Kame Kngwarreye, Emmanuelle Moureaux, Emory Douglas, Encaustic tile, Enid Blyton, Epic scale, Epic view, Erez Marom, Eric Cahan, Eric Canete, Eric Carle, Eric Dowdle, Eric Drooker, Eric Fischl, Eric Gill, Eric Lacombe, Eric Lafforgue, Eric Ravilious, Eric Standley, Erich Hartmann, Erik Jones, Erik Kessels, Erin Gregory, Ernest Lawson, Ernie Ball, Ernie Barnes, Ernst Fuchs, Ernst Lubitsch, Ernst Wilhelm Nay, Erte, Erwin Redl, Esad Ribic, Esaias van de Velde, Esao Andrews, Euan Uglow, Eudora Welty, Eugen Chisnicean, Eugene Boudin, Eugene Delacroix, Eugene Galien-Laloue, Eugene Grasset, Eugene Richards, Eugene de Blaas, Eustache Le Sueur, Eva Hesse, Eva Rothschild, Eve Arnold, Eve Ventrue, Evelyn De Morgan, Evelyn Dunbar, Everett Shinn, Evgeni Dinev, Evgeni Gordiets, Evgeny Lushpin, Exploded assembly, Exploded view, Extreme caricature, Exuberant atmosphere, Ezra Jack Keats, Ezra Petronio, F. C. Gundlach, Faberge egg, Fabian Oefner, Fabienne Rivory, Fabio Hurtado, Facepaint, Fairfield Porter, Faith47, Faiza Maghni, Fallout 4, Family Guy, Fantasy movie, Fatima Ronquillo, Fausto Puglisi, Federico Babina, Felice Beato, Felice Varini, Felix Gonzalez-Torres, Felix Vallotton, Ferdinand Hodler, Ferdinand Knab, Ferdinando Scianna, Fern Isabel Coppedge, Fernand Fonssagrives, Fernando Amorsolo, Fernando Botero, Ferris Plock, Ferrofluid, Festima mask, Fiber optic light painting, Fibrous texture, Filigree, Filip Hodas, Filippo Minelli, Fine art etching, Finn Juhl, Finnian MacManus, Fintan Magee, Firelit, First-person view, Fish scale texture, Fish-eye view, Fitz Henry Lane, Flamboyant atmosphere, Flashy colors, Flora Waycott, Florence Broadhurst, Florence Harrison, Florence Knoll, Florian Nicolle, Florine Stettheimer, Fluffy texture, Fluorescent colors, Foggy, Food photography, Footage from CCTV, Found object art, Fra Angelico, Fragmented composition, Frances Brundage, Frances Hodgkins, Frances MacDonald, Frances Tipton Hunter, Francesco Albani, Francesco Borromini, Francesco Clemente, Francesco Guardi, Francesco Solimena, Francis Bernard Dicksee, Francis Manapul, Francis Newton Souza, Francisco de Zurbaran, Franck Bohbot, Franco Fontana, Francois Morellet, Francois Schuiten, Francois Truffaut, Francois-Xavier Lalanne, Francoise Nielly, Frank Bowling, Frank Bramley, Frank Cadogan Cowper, Frank Capra, Frank Holl, Frank McCarthy, Frank Weston Benson, Frankenweenie, Franklin Booth, Franklin Carmichael, Frans Floris, Frans Francken the Younger, Frans Hals, Frans Masereel, Frantisek Kupka, Franz von Stuck, Fred Benes, Fred Calleri, Fred Herzog, Fred Sandback, Fred Stein, Fred Tomaselli, Frederic Bazille, Frederic Remington, Frederic William Burton, Frederick Arthur Bridgman, Frederick Carl Frieseke, Frederick Catherwood, Frederick Cayley Robinson, Frederick Hammersley, Frederick John Kiesler, Frederick McCubbin, Frederick Varley, Friedel Dzubas, FriendsWithYou, Fritz Hansen, Fritz Zuber-Buhler, Frontal view, Fujifilm instax, Full body-height view, Fumihiko Maki, Functional block diagram, Furby, Furry texture, Gabor Csupo, Gabriel Isak, Gabriele Dell'otto, Gail Albert Halaban, Gail Gibbons, Gainax, Gantt chart, Garance Dore, Garth Ennis, Gary Baseman, Gary Benfield, Gary Hume, Gary Petersen, Gary Varvel, Gaston Le Touche, Gene Colan, Gene Davis, Genealogical tree, Genetic map, Gennady Spirin, Genndy Tartakovsky, Gentile Bellini, Geoffrey Beene, Geoglyph, Geological cross-section, George Biddle, George Caleb Bingham, George Callaghan, George Catlin, George Clausen, George Condo, George Cruikshank, George Cukor, George Digalakis, George Eastman, George Goodwin Kilburne, George Henry Boughton, George Henry Yewell, George Hoyningen-Huene, George Nakashima, George Nelson, George Pemba, George Perez, George Petty, George Platt Lynes, George Rodrigue, George Romney, George Segal, George Shaw, George Stefanescu, George Stubbs, George Tice, George Underwood, George Yepes, Georges Clairin, Georges Dambier, Georges Hobeika, Georges Lacombe, Georges Lemmen, Georges Lepape, Georges Rochegrosse, Georges Rousse, Georges Seurat, Georges de La Tour, Georgia O'Keeffe, Georgy Kurasov, Gerald Harvey Jones, Gerard ter Borch, Gerd Arntz, Gerda Wegener, Gered Mankowitz, Germaine Krull, Germaine Richier, Gerrit Dou, Gerrit Rietveld, Gerry Anderson, Gerry Johansson, Gertrude Jekyll, Ghada Amer, Giambattista Valli, Gian Lorenzo Bernini, Gianfranco Ferre, Gianni Berengo Gardin, Gifford Beal, Gil Bruvel, Gilbert Garcin, Gilbert Shelton, Gilbert Stuart, Gilbert Williams, Gillian Wearing, Ginette Callaway, Gio Ponti, Giorgetto Giugiaro, Giorgio Morandi, Giorgio Vasari, Giorgio de Chirico, Giotto, Giovanni Battista Piranesi, Giovanni Battista Tiepolo, Giovanni Battista Venanzi, Giovanni Boldini, Giovanni Domenico Tiepolo, Giovanni Segantini, Giuseppe Cristiano, Giuseppe Penone, Giuseppe de Nittis, Glam Rock fashion, Glen Orbik, Glenn Fabry, Glenn Ligon, Glenn Murcutt, Glitchy, Gloomy atmosphere, Glossy texture, Gnaga mask, GodMachine, Godfried Schalcken, Golden hour, Golgo 13, Gonzalo Borondo, Googie architecture, Google Street View, Gordon Matta-Clark, Goro Fujita, Grace Cossington Smith, Graciela Rodo Boulanger, Gradient colors, Graham Gercken, Graham Sutherland, Granblue Fantasy, Grandma Moses, Grant Morrison, Granular texture, Graph paper, Gravity Falls, Gray Malin, Gray Morrow, Greaser fashion, Greek Theatre mask, Greg Capullo, Greg Gorman, Greg Hildebrandt, Greg Land, Greg Manchess, Greg Nicotero, Greg Olsen, Greg Rucka, Greg Simkins, Greg Staples, Greg Tocchini, Grete Stern, Grigory Gluckmann, Grinling Gibbons, Gritty texture, Ground-level view, Guercino, Guerrilla Girls, Guido Argentini, Guido Borelli da Caluso, Guido Crepax, Guido Guidi, Guido Reni, Guido van Helten, Guild Wars 2, Guillaume Corneille, Guillaume Seignac, Guillem H. Pongiluppi, Gum Bichromate, Gunther Uecker, Gus Van Sant, Gustaf Tenggren, Gustav Bauernfeind, Gustav Stickley, Gustav-Adolf Mossa, Gustave Baumann, Gustave Boulanger, Gustave Courbet, Gustave De Smet, Gustave Eiffel, Gustave Le Gray, Gustave Loiseau, Gustave Moreau, Gustave de Jonghe, Guweiz, Guy Aitchison, Guy Buffet, Guy Carleton Wiggins, Guy Coheleach, Guy Delisle, Guy Denning, Guy Fawkes mask, Guy Harvey, Guy Maddin, Guy Rose, Gyorgy Kepes, Gyotaku, H. A. Brendekilde, H. P. Lovecraft, HENSE, Haas Brothers, Haibane Renmei, Haida art, Half-Life, Halo, Hamtaro, Hanafuda card, Haniwa, Hanna-Barbera, Hannah Dale, Hannah Hoch, Hanne Darboven, Hans Erni, Hans Feurer, Hans Heysen, Hans Hofmann, Hans Hollein, Hans Memling, Hans Op de Beeck, Hans Wegner, Harley Earl, Harmony Korine, Harold Cazneaux, Harold Gilman, Haroon Mirza, Haroshi, Harriet Backer, Harriet Lee-Merrion, Harrison Fisher, Harry Anderson, Harry Bertoia, Harry Furniss, Harry Siddons Mowbray, Harry Watrous, Harry Weisburd, Harsh lighting, Harumi Hironaka, Harun Farocki, Harvey Dunn, Hassan Hajjaj, Hasui Kawase, Hatsune Miku, Hayv Kahraman, Headgear, Heat map, Heather Galler, Hebru Brantley, Hector Guimard, Hedi Slimane, Hedi Xandt, Heidi Swapp, Heinrich Kley, Heinz Mack, Helen Allingham, Helen Cottle, Helen Dardik, Helen Frankenthaler, Helen Lundeberg, Helen Musselwhite, Helene Beland, Helene Schjerfbeck, Helio Oiticica, Hello Kitty, Helmut Lang, Hendrick Avercamp, Hendrick ter Brugghen, Hendrik Kerstens, Hengki Koentjoro, Henning Larsen, Henri Fantin-Latour, Henri Gaudier Brzeska, Henri Le Sidaner, Henri Lebasque, Henri Manguin, Henri Michaux, Henri-Jules-Jean Geoffroy, Henrietta Harris, Henriette Ronner-Knip, Henrik Fisker, Henry Asencio, Henry Justice Ford, Henry Moret, Henry Roderick Newman, Henry Scott Tuke, Henry Selick, Henry Wessel, Herakut, Heraldry, Herb Trimpe, Herbert Bayer, Herman Brood, Herman Miller, Hermann Nitsch, Herringbone texture, Herve Leger, Herzog & de Meuron, Heywood Hardy, Hideo Kojima, Hieratic script, High key photography, Hilla Becher, Hina doll, Hipgnosis, Hiro Mashima, Hiroshi Nagai, Hiroshi Yoshida, History movie, Hito Steyerl, Holly Andres, Holly Hobbie, Holton Rower, Hong-Oai Don, Honore Daumier, Hopi art, Horror movie, Howard Arkley, Howard Behrens, Howard Carter, Howard Chandler Christy, Howard Chaykin, Howard David Johnson, Howard Pyle, Howard Schatz, Howard Terpning, Howardena Pindell, Hubert Robert, Hudoq mask, Hugh Ferriss, Hugues Merle, Huichol art, Huma Bhabha, Humberto Ramos, Humphry Repton, Hurricane, Ian Howorth, Ian Miller, Idris Khan, Igor Mitoraj, Igor Morski, Igor Zenin, Ikenaga Yasunari, Ildiko Neer, Illuminated manuscript, Imants Tillers, Impasto, Inca art, Incandescent colors, India Mahdavi, Indonesian mask, Inessa Garmash, Inez and Vinoodh, Inge Morath, Ingrid Baars, Intaglio, Intarsia woodworking, Intricate, Inuit art, Iren Horrors, Irene Sheri Vishnevskaya, Iridescent colors, Iris Scott, Irma Stern, Irmgard Schoendorf Welch, Irving Amen, Isaac Cruikshank, Isaac Mizrahi, Isabel Toledo, Isabelle Arsenault, Isabelle de Borchgrave, Isao Takahata, Isay Weinfeld, Isekai anime style, Isometric projection, Ithell Colquhoun, Ito Jakuchu, Itzchak Tarkay, Iurie Belegurschi, Ivan Brunetti, Ivan Chermayeff, Ivan Kramskoi, J. E. H. MacDonald, J. J. Abrams, J. J. Grandville, J. Scott Campbell, J.M.W. Turner, J.P. Targete, Jack Butler Yeats, Jack Davison, Jack Gaughan, Jack Kirby, Jack Sorenson, Jack Ziegler, Jackie Morris, Jackie Ormes, Jacob Aue Sobol, Jacob Epstein, Jacob Jordaens, Jacob Philipp Hackert, Jacob Riis, Jacob van Ruisdael, Jacopo Bassano, Jacques Amans, Jacques Lipchitz, Jacques Majorelle, Jacques Monory, Jacques Rivette, Jaehyo Lee, Jaime Hayon, Jama Jurabaev, Jamel Shabazz, James Abbott McNeill Whistler, James Avati, James Avery, James Bama, James Barnor, James Brooks, James Casebere, James Dietz, James Dyson, James Ensor, James Gillray, James Gunn, James Guthrie, James Jean, James Nizam, James Paick, James R. Eads, James Rosenquist, James Sant, James Thurber, James Tissot, James Turrell, James Wan, James Welling, James Wells Champney, James Wilson Morrice, Jamie Heiden, Jamini Roy, Jan Berenstain, Jan Brett, Jan Davidsz de Heem, Jan Fabre, Jan Gossaert, Jan Mankes, Jan Matson, Jan Toorop, Jan Urschel, Jan van Haasteren, Jan van Kessel the Elder, Jan van Scorel, Jane Burch Cochran, Jane Newland, Jane Small, Janet Delaney, Janet Echelman, Janine Antoni, Jannis Kounellis, Januz Miralles, Jarek Kubicki, Jarek Puczel, Jason Edmiston, Jason Freeny, Jason Middlebrook, Jason deCaires Taylor, Jasper Francis Cropsey, Jaume Capdevila, Jaume Plensa, Javier Mariscal, Javiera Estrada, Jay DeFeo, Jean Arp, Jean Beraud, Jean Dubuffet, Jean Fouquet, Jean Jullien, Jean Metzinger, Jean Patou, Jean Prouve, Jean-Antoine Houdon, Jean-Antoine Watteau, Jean-Baptiste Carpeaux, Jean-Baptiste Corot, Jean-Baptiste Greuze, Jean-Baptiste-Simeon Chardin, Jean-Etienne Liotard, Jean-Gabriel Domergue, Jean-Honore Fragonard, Jean-Joseph Benjamin-Constant, Jean-Leon Gerome, Jean-Louis Forain, Jean-Michel Frank, Jean-Paul Goude, Jean-Paul Riopelle, Jean-Philippe Delhomme, Jean-Pierre Gibrat, Jean-Sebastien Rossbach, Jeanine Michna-Bales, Jeanne Lanvin, Jeannette Guichard-Bunel, JeeYoung Lee, Jeff Kinney, Jeff Lemire, Jeff Soto, Jeffrey Milstein, Jeffrey Smart, Jeffrey T. Larson, Jen Bartel, Jen Sorensen, Jennifer Lommers, Jennifer Rubell, Jenny Frison, Jeppe Hein, Jeremiah Ketner, Jeremy Geddes, Jeremy Lipking, Jeremy Scott, Jerry N. Uelsmann, Jesper Ejsing, Jessica Backhaus, Jessica Drossin, Jessica Durrant, Jessica Galbreth, Jessica Roux, Jessica Stockholder, Jessie M. King, Jessie Willcox Smith, Jill Freedman, Jillian Tamaki, Jim Aparo, Jim Burns, Jim Cheung, Jim Daly, Jim Dine, Jim Fitzpatrick, Jim Hansel, Jim Holland, Jim Kazanjian, Jim Lambie, Jim Lee, Jim Starlin, Jim Warren, Jim Woodring, Jimmy Chin, Jimmy Marble, Jimmy Margulies, Jimmy Palmiotti, Jiro Kuwata, Jo Ann Callis, Jo Grundy, Joachim Beuckelaer, Joachim Patinir, Joachim Wtewael, Joan Cornella, Joan Eardley, Joan Mitchell, Joan Snyder, Joao Ruas, Joaquin Sorolla, Jocelyn Hobbie, Jock, Jody Bergsma, Joe Fenton, Joe Heller, Joe Jusko, Joe Madureira, Joe Quesada, Joe Webb, Joel Pett, Joel Rea, Joel Robison, Joel Shapiro, Joel Sternfeld, Joel-Peter Witkin, Joep Bertrams, Joep van Lieshout, Johan Messely, Johanna Basford, Johanna Gullichsen, Johannes Itten, Johannes VanDerBeek, Johfra Bosschart, John Anster Fitzgerald, John Atkinson Grimshaw, John Baldessari, John Batho, John Bellany, John Berkey, John Blanche, John Brack, John Bratby, John Burningham, John Butler Yeats, John Carpenter, John Cassaday, John Chamberlain, John Constable, John Crome, John Currin, John Darkow, John Divola, John Duncan, John Everett Millais, John Frederick Kensett, John Frederick Lewis, John George Brown, John Gould, John Gutmann, John Heartfield, John Hejduk, John Holcroft, John James Audubon, John Kricfalusi, John La Farge, John Larriva, John Lasseter, John Lautner, John Lavery, John Leech, John Lovett, John Lowrie Morrison, John Lurie, John Maler Collier, John Marin, John Martin, John Melhuish Strudwick, John Nash, John Olsen, John Paul Caponigro, John Perceval, John Philip Falter, John Piper, John Pitre, John Reuss, John Roddam Spencer Stanhope, John Ruskin, John Sell Cotman, John Singleton Copley, John Sloane, John Stezaker, John Tenniel, John Trumbull, John Twachtman, John William Godward, Johnathan Harris, Johnny Morant, Johnson Tsang, Jon Carling, Jon Foster, Jon Klassen, Jon Kudelka, Jon McNaught, Jon McNaughton, JonOne, Jonathan Adler, Jonathan Meese, Joost Swarte, Joram Roukes, Jordan Grimmer, Jordi Bernet, Jorge Maia, Joris Laarman, Jorn Utzon, Jos Collignon, Josan Gonzalez, Jose Guadalupe Posada, Jose Royo, Josef Frank, Josef Hoffmann, Josef Kote, Joseph Farquharson, Joseph Kosuth, Joseph Lorusso, Joseph Pennell, Joseph Stella, Josephus Laurentius Dyckmans, Josh Adamski, Josh Keyes, Josh Kirby, Joshua Reynolds, Joyful atmosphere, Juan Gris, Judith Clay, Judith Desrosiers, Judith Leyster, Judy Blame, Judy Dater, Juergen Teller, Jules Bastien-Lepage, Jules Breton, Jules Feiffer, Jules Olitski, Jules Pascin, Julian Beever, Julian Glander, Julian Opie, Julian Schnabel, Juliana Nan, Julie Arkell, Julie Paschkis, Julio Larraz, Julio Le Parc, Julius Leblanc Stewart, Junkanoo mask, Junya Ishigami, Jurassic Park, Jusepe de Ribera, Justin Gaffrey, Justin Gerard, Justin Roiland, Justina Blakeney, Justine Kurland, Kabuki, Kabuki mask, Kader Attia, Kaethe Butcher, Kaffe Fassett, Kamen Rider, Kanban wall, Kangra painting, Kansuke Yamamoto, Karel Appel, Karen Noles, Karen Tarlton, Karin Kuhlmann, Karl Knaths, Karl Schmidt-Rottluff, Karla Gerard, Kate Baylay, Kate Greenaway, Kate Leth, Kate Spade, Kathakali mask, Katharina Grosse, Kathrin Honesta, Kathrin Longhurst, Kathy Fornal, Katie Scott, Katsuya Terada, Katy Grannan, Katy Smail, Katya Gridneva, Kees van Dongen, Keiichi Tanaami, Keiji Inafune, Keisai Eisen, Kelley Jones, Kelly Beeman, Kelly Freas, Kelly Hoppen, Kelly McKernan, Kelly Wearstler, Ken Domon, Ken Fulk, Ken Sugimori, Kenneth Anger, Kenneth Josephson, Kenneth Noland, Kenneth Snelson, Kenojuak Ashevak, Kenro Izu, Kent Monkman, Kenzo, Kerby Rosanes, Kerem Beyit, Kerry James Marshall, Kestutis Kasparavicius, Kevin Carter, Kevin Maguire, Kevin McNeal, Kevin Nowlan, Kevin Siers, Kevin Sloan, Kevin Wada, Kewpie doll, Khokhloma, Kidrobot, Kilian Eng, Kim Keever, Kingdom Hearts, Kinuko Y. Craft, Kirlian photography, Kirsty Mitchell, Kishangarh painting, Kishin Shinoyama, Kitty Lange Kielland, Klaus Frahm, Knox Martin, Kokeshi doll, Koloman Moser, Konstantin Chaykin, Konstantin Grcic, Konstantin Razumov, Koson Ohara, Kourtney Roy, Krampusnacht festival mask, Krenz Cushart, Kris Knight, Kris Van Assche, Kristina Makeeva, Kubo And The Two Strings, Kukeri, Kurt Geiger, Kurt Hutton, Kurt Wenner, Kurzgesagt, Kuzma Petrov-Vodkin, Kwakiutl mask, Kyffin Williams, Kylli Sparre, Kyoto Animation studio, LaToya Ruby Frazier, Ladislav Sutnar, Lalaloopsy, Lalla Essaydi, Lantern-lit, Larry Bell, Larry MacDougall, Larry Towell, Larry Zox, Lascaux, Latex texture, Lath art, Laura Ashley, Laura Callaghan, Laura Iverson, Laura Makabresku, Laurent Parcelier, Laurie Lipton, Laurie Simmons, Lawren Harris, Lawrence Weiner, Layered composition, Layered paper, LeRoy Grannis, Lee Bermejo, Lee Friedlander, Lee Jae-Hyo, Lee Krasner, Lee Middleton doll, Lee Mullican, Legend of the Cryptids, Leigh Bowery, Len Lye, Leo Caillard, Leo Fender, Leo Villareal, Leo and Diane Dillon, Leon Bonnat, Leon Golub, Leon Kossoff, Leon Tukker, Leonard Freed, Les Paul, Leszek Bujnowski, Levi Strauss, Lewis Morley, Li Hongbo, Li Wei, Liam Brazier, Liam Gillick, Lieke Van der Vorst, Light atmosphere, Light colors, Lilia Alvarado, Lilla Cabot Perry, Lim Heng Swee, Limbourg brothers, Lina Bo Bardi, Linda Ravenscroft, Lindisfarne Gospels, Linnea Strid, Lisa Congdon, Lisa Golightly, Lisa Graa Jensen, Lisa Holloway, Lisa Parker, Lisa Pressman, Lisbeth Zwerger, Lisi Martin, Lit by lightning, Lita Cabellut, Lite Brite art, Liu Bolin, Lizzie Fitch, Lois Dodd, Lois Greenfield, Long exposure photography, Loomis Dean, Looney Tunes, Loretta Lux, Lori Earley, Lori Nix, Lorser Feitelson, Louella Pettway, Loui Jover, Louis Comfort Tiffany, Louis Faurer, Louis Janmot, Louis Marcoussis, Louis Rhead, Louis Ritman, Louis Stettner, Louis Valtat, Louise Balaam, Louise Dahl-Wolfe, Louise Lawler, Louise McNaught, Louise Nevelson, Lovis Corinth, Low key photography, Low vantage point view, Low-hanging fog, Lowell Herrero, Lubaina Himid, Luc Tuymans, Luca Giordano, Luca della Robbia, Lucas Cranach the Elder, Lucas Cranach the Younger, Lucas Foglia, Lucha Libre mask, Lucia Heffernan, Lucia Rafanelli, Lucie Rie, Lucien Clergue, Lucien Pissarro, Lucienne Day, Lucio Fontana, Lucy Glendinning, Lucy Grossmith, Lucy Madox Brown, Ludwig Deutsch, Ludwig Favre, Ludwig Meidner, Luigi Ghirri, Luigi Loir, Luigi Russolo, Luis Barragan, Luis Bunuel, Luis Ricardo Falero, Luke Chueh, Luke Jerram, Luminous colors, Lygia Clark, Lynd Ward, Lynda Barry, Lynette Yiadom-Boakye, Lynn Hershman Leeson, Lyonel Feininger, MVRDV, Mab Graves, Mabel Lucie Attwell, Maciej Kuciara, Macro view, Madeleine Vionnet, Madeline Weinrib, Magali Villeneuve, Magdalena Abakanowicz, Maggie Holmes, Maggie Taylor, Magic: The Gathering, Magical atmosphere, Mai Thu Perret, Maira Kalman, Malcolm Liepke, Malika Favre, Mamoru Hosoda, Mana Neyestani, Maneki-neko, Manfred Kielnhofer, Manolo Blahnik, Maori art, Map, Maquette, Marbled texture, Marc Adamus, Marc Allante, Marc Johns, Marc Muench, Marc Newson, Marc Riboud, Marcantonio Raimondi, Marcel Duchamp, Marcel Janco, Marcel Mouly, Marcello Barenghi, Marco Mazzoni, Mardi gras mask, Margaret Macdonald Mackintosh, Margaret Tarrant, Margarita Kareva, Maria Sibylla Merian, Marian Kamensky, Marianne North, Marianne von Werefkin, Mariano Vivanco, Marie Spartali Stillman, Mariko Mori, Marimekko, Marine photography, Marino Marini, Mario Botta, Mario Buatta, Mario Sorrenti, Marion Peck, Marjolein Bastin, Mark Bagley, Mark Brooks, Mark Fiore, Mark Grotjahn, Mark Hearld, Mark Knight, Mark Manders, Mark Riddick, Mark Ruwedel, Mark Schultz, Mark Shaw, Mark Spain, Mark Waid, Mark Wallinger, Mark di Suvero, Marsden Hartley, Marsel van Oosten, Marta Abad Blay, Martin Deschambault, Martin Johnson Heade, Martin Lewis, Martin Margiela, Martin Rowson, Martin Schongauer, Martina Hoogland Ivanow, Martyn Turner, Marvel, Mary Anning, Mary Cassatt, Mary Charles, Mary Delany, Mary Engelbreit, Mary Quant, Mary Sibande, Masaaki Sasamoto, Masaaki Yuasa, Masahisa Fukase, Masamune Shirow, Masao Yamamoto, Mass Effect, Massimiliano Fuksas, Massimo Vitali, Massive scale, Mat Collishaw, Mati Klarwein, Matt Adnate, Matt Furie, Matt Mahurin, Matt Rhodes, Matt Sesow, Matt Wuerker, Matte texture, Matteo Massagrande, Matthaus Merian the Elder, Matthew Pillsbury, Matthias Grunewald, Matthias Haker, Matthias Heiderich, Maud Humphrey, Maud Lewis, Max Beckmann, Max Bill, Max Gimblett, Max Pechstein, Max Rive, Max Weber, Maximalist composition, Maxime Maufra, Maximilien Luce, Maya Deren, Maya Lin, Mayan art, Maynard Dixon, Mead Schaeffer, Medieval Bestiary, Medieval Codex, Mega man, Megan Hess, Megan Massacre, Meghan Howland, Mehndi, Meiji Period photography, Meindert Hobbema, Mel Bochner, Mel Ramos, Melancholic atmosphere, Melissa Launay, Melozzo da Forli, Melvin Sokolsky, Memphis design, Meredith Frampton, Meredith Marsone, Mert Alas, Mert Otsamo, Mert and Marcus, Metal Gear Solid, Metallic colors, Mexican muralism, Miami Vice, Michael Ancher, Michael Aram, Michael Borremans, Michael Breitung, Michael Carson, Michael Cheval, Michael Cho, Michael Craig-Martin, Michael Creese, Michael Eastman, Michael Garmash, Michael Heizer, Michael Humphries, Michael Hussar, Michael Hutter, Michael Kidner, Michael Lang, Michael Leunig, Michael Page, Michael Parkes, Michael Ramirez, Michael Shapcott, Michael Sowa, Michael Sweerts, Michael Thonet, Michael de Adder, Michel Comte, Michelangelo, Micrographia, Mid-century modern, Midday, Miffy, Miho Hirano, Mika Ninagawa, Mikalojus Konstantinas Ciurlionis, Mike Luckovich, Mike Peters, Mike Zeck, Miles Redd, Millie Marotta, Millimeter squared paper, Milo Manara, Milt Kahl, Milton Avery, Mindy Sommers, Minecraft, Miniature scale, Minimalist composition, Minjae Lee, Minkowski diagram, Minoan art, Minor Martin White, Mira Schendel, Miroslav Sasek, Misty, Mitch Dobrowner, Mitsumasa Anno, Miyamoto Musashi, Mo Willems, Moche art, Moebius, Moises Levy, Molang, Molecular model, Molly Brett, Molly Harrison, Momiji Message Dolls, Mondo Guerra, Monica Blatton, Monica Bonvicini, Monique Lhullier, Monir Shahroudy Farmanfarmaian, Monochrome colors, Monokubo, Moonlit, Morag Myerscough, Morgan Maassen, Morgan Weistling, Morphsuits, Mort Kunstler, Mort Walker, Mortal Kombat, Mothmeister, Motion blur, Moxie Girlz, Mr. Brainwash, Mr. Potato Head, Muted colors, My Little Pony, Nacho Carbonell, Naeem Khan, Namibian Ovambo mask, Nancy Noel, Naoya Hatakeyama, Narayan Shridhar Bendre, Natalia Goncharova, Nate Beeler, Nate Berkus, Nathalie Du Pasquier, National Geographic magazine, Natural lighting, Naum Gabo, Navid Baraty, Nazca lines, Ndebele art, Neil Blevins, Neil Harbisson, Neil Krug, Neil Leifer, Nendoroid, Neon colors, Netsuke, Neutral Color Scheme, Nicholas Alan Cope, Nicholas Hilliard, Nick Anderson, Nick Gentry, Nick Park, Nickelodeon, Nickolas Muray, Nico Delort, Nicolai Fechin, Nicolas Bruno, Nicolas Delort, Nicolas Mignard, Nicolas Poussin, Nicolas de Stael, Nicole Eisenman, Nigel van Wieck, Night, Nike Savvas, Niki Boon, Niki de Saint Phalle, Nina Chanel Abney, Nina Leen, Nina Ricci, Nirav Patel, Njideka Akunyili Crosby, Nkisi figure, Noah Bradley, Nobuo Sekine, Noell Oszvald, Noemie Goudal, Noh Theater mask, Nona Faustine, Nona Limmen, Nora Heysen, Noriyoshi Ohrai, Norm Breyfogle, Norman Bel Geddes, Norman Bluhm, Norman Cornish, Norman Lewis, Norman Lindsay, Norman McLaren, Norman Parkinson, Northern lights, Norval Morrisseau, Nuragic mask, Nuremberg Chronicle, Nychos, Odilon Redon, Ogham, Oily texture, Ok go, Oleg Cassini, Oleg Zhivetin, Olek, Olha Darchuk, Olimpia Zagnoli, Oliver Stone, Olivier Coipel, Olivier Mosset, Olivier Theyskens, Oliviero Toscani, Olly Moss, Olmec art, Omar Galliani, Omar Victor Diop, On Kawara, Ori Gersht, Orla Kiely, Orlan, Orthographic view, Oscar Tuazon, Osman Hamdi Bey, Oswald Achenbach, Otto Schmidt, Ottoman art, Outsider art, Overhead view, Oxana Zaika, PJ Crook, Pablo Lobato, Pac-Man, Pacita Abad, Paco Rabanne, Paige Evans, Pallava architecture, Panoramic composition, Panoramic view, Paola Navone, Paola Pivi, Paolo Uccello, Paolo Veronese, Papercraft, Parametric art, Parametric drawing, Paresh Nath, Parquetry, Pascale Marthine Tayou, Pastel colors, Pat Bagley, Patek Philippe, Patent drawing, Patricia Polacco, Patricia Voulgaris, Patrick Chappatte, Patrick Heron, Patrick Seymour, Paul Cadden, Paul Caponigro, Paul Delvaux, Paul Gustav Fischer, Paul Guy Gantner, Paul Hedley, Paul Jenkins, Paul Kaptein, Paul Kenton, Paul Lovering, Paul Lung, Paul Nash, Paul Pelletier, Paul Poiret, Paul Rader, Paul Ranson, Paul Rudolph, Paul Rumsey, Paul Serusier, Paul Signac, Paul Souders, Paul Wonner, Paul-Cesar Helleu, Paulo Mendes da Rocha, Paulo Zerbato, Paulus Potter, Pawel Kuczynski, Pearlescent colors, Peder Balke, Pedro Friedeberg, Peeling texture, Pejac, Pennie Smith, Percy Tarrant, Peregrine Heathcote, Perspective drawing, Petah Coyne, Pete McKee, Pete Souza, Pete Turner, Peter Adderley, Peter Bagge, Peter Basch, Peter Beard, Peter Behrens, Peter Blake, Peter Brookes, Peter Callesen, Peter Coulson, Peter Dombrovskis, Peter Dundas, Peter Eisenman, Peter Elson, Peter Fischli and David Weiss, Peter Halley, Peter Henry Emerson, Peter Holme III, Peter Howson, Peter Hujar, Peter Ilsted, Peter Kemp, Peter Kuper, Peter Lik, Peter Lippmann, Peter Marino, Peter Max, Peter McKinnon, Peter Mendelsund, Peter Mitchev, Peter Mohrbacher, Peter Nottrott, Peter Robert Keil, Peter Sculthorpe, Peter Shire, Peter Turnley, Peter Voulkos, Peter Wileman, Petra Cortright, Petrina Hicks, Petros Afshar, Phase diagram, Phil Hester, Phil Jimenez, Phil Koch, Phil Noto, Phil Tippett, Philip Guston, Philip Jackson, Philip Jones Griffiths, Philip McKay, Philip Pearlstein, Philip Taaffe, Philip Toledano, Philip Treacy, Philip-Lorca diCorcia, Philipp Plein, Philippe Caza, Philippe Faraut, Philippe Geluck, Philippe Parreno, Philippe de Champaigne, Phlegm, Phoebe Anna Traquair, Phyllida Barlow, Phyllis Galembo, Pier Luigi Nervi, Pier Paolo Pasolini, Piero Fornasetti, Piero Manzoni, Pierre Adolphe Valette, Pierre Alechinsky, Pierre Bonnard, Pierre Huyghe, Pierre Jeanneret, Pierre Paulin, Pierre Pellegrini, Pierre Puvis de Chavannes, Pierre-Auguste Cot, Pierre-Jacques Pelletier, Piet Hein Eek, Piet Oudolf, Piet Parra, Pieter Aertsen, Pieter Bruegel the Elder, Pieter Claesz, Pieter Hugo, Pieter de Hooch, Pieter-Jansz van Asch, Pietro Antonio Rotari, Pietro Bernini, Pietro Perugino, Pietro da Cortona, Pinturicchio, Pipilotti Rist, Pitted texture, Pixar studio, Pixelscape, Plague doctor mask, Playful atmosphere, Point cloud, Pol Ledent, Polixeni Papapetrou, Pompeian fresco, Porous texture, Poul Henningsen, Poul Kjaerholm, Pouring rain, Preppy fashion, Primary colors, Prince of Persia, Prismatic texture, Product photography, Propaganda art, Prudence Heward, Prue Stent & Honey Long, Psychedelic colors, Puuung, Quino, R. K. Laxman, RHADS, RJ Matson, Rachel Bingaman, Rackstraw Downes, Radial composition, Radiographic image, Rafael Moneo, Rafael Vinoly, Rafal Olbinski, Raghu Rai, Ragnar Axelsson, Rainer Fetting, Rainer Werner Fassbinder, Rainy, Ralph Gibson, Ralph Horsley, Ralph Rucci, Rammellzee, Ran Ortner, Randal Spangler, Randall Munroe, Rangoli, Rankin, Rankin Bass, Raphael Lacoste, Raphael Perez, Raphaelle Peale, Raqib Shaw, Rashid Johnson, Raw texture, Ray Caesar, Ray Johnson, Raymond Briggs, Raymond Depardon, Raymond Loewy, Raymond Swanland, Rebeca Saray, Rebecca Sugar, Red Dead Redemption, Red Grooms, Reem Acra, Remedios Varo, Renato Guttuso, Rene Burri, Rene Lalique, Resident Evil, Resin art, Revok, Richard Anderson, Richard Anuszkiewicz, Richard Billingham, Richard Burlet, Richard Caldicott, Richard Dadd, Richard Deacon, Richard Diebenkorn, Richard Doyle, Richard Edward Miller, Richard Estes, Richard Eurich, Richard Hambleton, Richard Hamilton, Richard Lindner, Richard Lippold, Richard McGuire, Richard Mosse, Richard Neutra, Richard Parkes Bonington, Richard Phillips, Richard Pousette-Dart, Richard S. Johnson, Richard Schmid, Richard Tuschman, Richard Tuttle, Rick Amor, Rick Baker, Rick McKee, Rick Remender, Rick and Morty, Rie Cramer, Riitta Ikonen, Rimel Neffati, Rineke Dijkstra, Rinko Kawauchi, Rio Carnival float, Rippled texture, Risograph, Rita Angus, Rob Hefferan, Rob Rogers, Rob Ryan, Robert Adams, Robert Arneson, Robert Bateman, Robert Bechtle, Robert Bissell, Robert Campin, Robert Colescott, Robert Cottingham, Robert Doisneau, Robert Frank, Robert Gillmor, Robert Gober, Robert Hagan, Robert Henri, Robert Indiana, Robert Irwin, Robert Kirkman, Robert Longo, Robert Lyn Nelson, Robert Mangold, Robert McCall, Robert McGinnis, Robert Montgomery, Robert Morris, Robert Munsch, Robert Polidori, Robert Reid, Robert Ryman, Robert Smithson, Robert Swain Gifford, Robert Vonnoh, Robert Williams, Robert Wyland, Robert Zemeckis, Roberto Burle Marx, Roberto Cavalli, Roberto Matta, Roberto Rossellini, Robin Moline, Rockabilly fashion, Rockwell Kent, Roe Ethridge, Roeselien Raimond, Roger Bezombes, Roger Fenton, Roger Fry, Roger Hargreaves, Roger Hiorns, Roger Mayne, Roger Vivier, Rogier van der Weyden, Rolf Armstrong, Rolling Stone magazine, Roly-poly doll, Romain Trystram, Roman Vishniac, Roman mosaic, Romance movie, Romare Bearden, Romina Ressia, Romulo Royo, Ron Arad, Ron Clements, Ron Mueck, Ron Tandberg, Ron Walotsky, Ronald Searle, Ronald Wimberly, Ronan and Erwan Bouroullec, Rone, Roni Horn, Ronnie Landfield, Rosa Bonheur, Rosalba Carriera, Rosalyn Drexler, Rose Wylie, Rossdraws, Rosso Fiorentino, Rough surface, Rough texture, Roxy Paine, Roy DeCarava, Roz Chast, Ruben Ireland, Rudolf Ernst, Rudolf Olgiati, Rui Palha, Rupert Bunny, Rupert Vandervell, Ruslan Lobanov, Russ Heath, Russell Young, Rusty Lake, Ruth Asawa, Ruth Bernhard, Ruth Orkin, Ruth Sanderson, Ruud van Empel, Ryan Gander, Ryan Hewett, Ryan McGinley, Ryan McGinness, Ryan Ottley, Ryan Sook, Ryan Stegman, Ryo Takemasa, Sabine Weiss, Sachin Teng, Saint Seiya, Sakimichan, Sally Cruikshank, Salvatore Ferragamo, Sam Chivers, Sam Maloof, Sam Raimi, Sama Calligraphy, Samoan art, Sandra Boynton, Sandra Chevrier, Sandra Silberzweig, Sandro Botticelli, Sandy Welch, Saner, Sanford Biggers, Sanford Robinson Gifford, Sanne Sannes, Santiago Caruso, Santiago Rusinol, Sara Riches, Sarah Joncas, Sarah Lucas, Sardax, Satellite view, Satin texture, Satoru Takizawa, Saturated colors, Satyajit Ray, Saul Leiter, Saul Steinberg, Saul Tepper, Scandi chic fashion, Schematic diagram, Schematics, Sci-fi movie, Scott Adams, Scott Listfield, Scott McCloud, Scott Naismith, Scott Pilgrim, Scott Rohlfs, Scrapbooking, Sealab 2021, Sean Bagshaw, Sean Scully, Sean Yoro, Sebastian Errazuriz, Sebastiano del Piombo, Sebastiao Salgado, Sectional view, Seismogram, Selective Colouring, Sepia tones, Serene atmosphere, Serge Attukwei Clottey, Serge Lutens, Serge Marshennikov, Serge Mouille, Serge Najjar, Serge Poliakoff, Sergei Diaghilev, Sergio Larrain, Sergio Pininfarina, Sergio Toppi, Sesame Street, Seth Globepainter, Seven Samurai, Seydou Keita, Sgraffito, Shabby chic fashion, Shadowed lighting, Sheila Hicks, Sherree Valentine-Daines, Sherry Akrami, Shigeo Fukuda, Shin hanga, Shinichi Maruyama, Shintaro Kago, Shirin Neshat, Shirley Hughes, Shohei Otomo, Shomei Tomatsu, Shopkins Shoppies, Shozo Shimamoto, Shrek, Shuji Terayama, Shusei Nagaoka, Side view, Sidney Nolan, Sidney Sime, Sigmar Polke, Signe Wilkinson, Silhouette, Silvestro Lega, Simen Johan, Simeon Solomon, Simon Birch, Simon Bisley, Simon Kenny, Simon Palmer, Simon Schubert, Simon Vouet, Simone Rocha, Sion Sono, Slick texture, Slim Aarons, Slinkachu, Small scale, Smooth texture, Snowy, Sofonisba Anguissola, Soft lighting, Sohrab Hura, Sol LeWitt, Solve Sundsbo, Somber atmosphere, Sonia Boyce, Sonia Delaunay, Sonia Rykiel, Sonic the Hedgehog, Sopheap Pich, Sophie Delaporte, Sophie Gamand, Sophie Gengembre Anderson, Sophie Taeuber-Arp, Sou Fujimoto, Sound wave diagram, South Park, Space Invaders, Spartacus Chetwynd, Sparth, Spectral texture, Spectrogram, Speedy Graphito, Split view, SpongeBob SquarePants, Spotlight lighting, Spreadsheet, Spring, Spy movie, Squared paper, Squeak Carnwath, Stage lighting, Stan Berenstain, Stan Brakhage, Stan Winston, Stanhope Forbes, Stanko Abadzic, Stanley Donwood, Stanley Greene, Stanley Spencer, Star Trek, StarCraft, Starlit, Statistical graph, Stefan Gesell, Stefanie Schneider, Stencil graphics, Stephan Pastis, Stephanie Law, Stephen Darbishire, Stephen Hillenburg, Stephen Mackey, Stephen Ormandy, Stephen Quiller, Stephen Shortridge, Stephen Wilkes, Stephen Wiltshire, Stevan Dohanos, Steve Bell, Steve Cutts, Steve Dillon, Steve Epting, Steve Hanks, Steve Henderson, Steve Kelley, Steve McCurry, Steve McNiven, Steve Sack, Steven Harrington, Steven Meisel, Steven Outram, Sticker illustration, Stik, Stjepan Sejic, Storm Thorgerson, Stormy, Strandbeest, Strawberry Shortcake doll, Street Fighter, Streetview, Streetwear, Stuart Davis, Stuart Haygarth, Stuart Immonen, Stuart Lippincott, Studio Ghibli, Studio lighting, Su Blackwell, Subdued tones, Subodh Gupta, Sudarsan Pattnaik, Sue Reno, Sulamith Wulfing, Sumerian death mask, Summer, Sung Kim, Sunset, Super Mario, Superjail!, Susan Hiller, Susan Kare, Susan Rios, Susan Seddon Boulet, Suzanne Lalique, Suzanne Valadon, Suzuki Harunobu, Sven Pfrommer, Sverre Fehn, Sylvain Sarrailh, Sylvie Guillot, Symmetrical composition, Synchromism, TCG artwork, Tailored fashion, Takashi Homma, Takehiko Inoue, Tamagotchi, Tamas Dezso, Tammam Azzam, Tania Bruguera, Tara Donovan, Tara McPherson, Tarot de Marseille, Tatsunoko Production studio, Tatsuro Kiuchi, Taylor Wessing, Teagan White, Ted DeGrazia, Ted Gore, Ted McKeever, Ted Rall, Teddy Boys fashion, Teen Titans, Teenage Mutant Ninja Turtles, Teletubbies, Tense atmosphere, Terence Cuneo, Terence Donovan, Terry Isaac, Terry Oakes, Terry Redlin, Tetris, Teun Hocks, Tex Avery, Textured surface, The Adventures of Asterix, The Adventures of Tintin, The Elder Scrolls, The Flintstones, The Grand Budapest Hotel, The Lord of the Rings, The Matrix, The New Yorker magazine, The Secret of Kells, The Simpsons, The Sims, The Sopranos, The Tatami Galaxy, The White Stripes, The Witcher, The X-Files, Theaster Gates, Theo Prins, Theo van Doesburg, Theo van Rysselberghe, Theodor Kittelsen, Theodore Chasseriau, Theodore Gericault, Theodore Robinson, Theodore Rousseau, Theophile Steinlen, Thiago Valdi, Thierry Duval, Thom Browne, Thom Filicia, Thom Mayne, Thomas Ascott, Thomas Barbey, Thomas Benjamin Kennington, Thomas Birch, Thomas Blackshear, Thomas Chippendale, Thomas Cole, Thomas Daniell, Thomas Dewing, Thomas Dodd, Thomas Eakins, Thomas Edwin Mostyn, Thomas Gainsborough, Thomas Hart Benton, Thomas Heatherwick, Thomas Hirschhorn, Thomas Kinkade, Thomas Lawrence, Thomas Leuthard, Thomas Moran, Thomas Nast, Thomas Rowlandson, Thomas Ruff, Thomas Saliot, Thomas Schaller, Thomas Struth, Thomas Sully, Thomas Theodor Heine, Thomas Wrede, Thornton Utz, Thota Vaikuntam, ThunderCats, Thunderstorm, Thurston Hopkins, Tiago Hoisel, Tiki mask, Tillie Walden, Tilted view, Tim Biskup, Tim Bradstreet, Tim Doyle, Tim Etchells, Tim Hawkinson, Tim Hildebrandt, Tim Holtz, Tim Noble, Tim Okamura, Tim Shumate, Time magazine, Time-lapse photography, Timothy Easton, Tina Barney, Tina Modotti, Tintoretto, Tithi Luadthong, Titian, Tlingit art, Tlingit mask, Tobia Scarpa, Tod Browning, Todd Hido, Todd James, Todd McLellan, Todd Nauck, Todd White, Toei Animation studio, Tokujin Yoshioka, Tokusatsu, Tokyo Ghoul, Tom Bagshaw, Tom Eckersley, Tom Everhart, Tom Fruin, Tom Gauld, Tom Grummett, Tom Killion, Tom Lovell, Tom Otterness, Tom Sachs, Tom Thomson, Tom Toles, Tom Tomorrow, Tom Wesselmann, Tom Whalen, Tom and Jerry, Tom of Finland, Tomas Saraceno, Tomasz Alen Kopera, Tomer Hanuka, Tomi Ungerer, Tomie dePaola, Tomma Abts, Tomokazu Matsuyama, Tomoo Gokita, Ton Dubbeldam, Tony Allain, Tony Conrad, Tony DeZuniga, Tony Fitzpatrick, Tony Moore, Tony Orrico, Top view, Topology map, Torito mask, Tory Burch, Toshio Shibata, Toyin Ojih Odutola, Tracey Adams, Tracy Miller, Tracy Reese, Tracy Scott, Traditional Scroll Painting, Tranquil atmosphere, Translucent colors, Trash Polka, Treemap, Trent Parke, Trevor Paglen, Trey Ratcliff, Trisha Romance, Tsukioka Yoshitoshi, Tsumori Chisato, Twin Peaks, Tyler Edlin, UV colors, Ugo Rondinone, Ukrainian folk art, Umberto Boccioni, Underwater lighting, Utamaro, VS Gaitonde, Vacheron Constantin, Val Byrne, Valentine Cameron Prinsep, Valentino, Valerio Adami, Vamberk lace, Vania Zouravliov, Vector chip line art, Vector field, Veined texture, Venetian carnival mask, Vera Wang, Vermiculation texture, Versus, Vibrant colors, Vicki Boutin, Victor Brauner, Victor Moscoso, Victoria Frances, Victorian era cabinet card, Vienna Secession, Vilhelm Hammershoi, Vince Low, Vincent Castiglia, Vincent Di Fate, Vincent Giarrano, Vincent Laforet, Vincent Munier, Vincent Peters, Vinoodh Matadin, Virgil Finlay, Vittore Carpaccio, Viviane Sassen, Vivienne Tam, Vladimir Tretchikoff, Vladimir Volegov, Vogue magazine, Voronoi diagram, Voynich manuscript, WLOP, WRDSMTH, Wabi-sabi, Walasse Ting, Walt Disney, Walt Handelsman, Walter Langley, Walter Lantz, Walter Molino, Walter Quirt, Wang Guangyi, Warcraft, Warhammer 40,000, Warm colors, Warner Bros, Warren Ellis, Warren Keelan, Warwick Goble, Weather map, Weathered surface, Wendell Castle, Wendy Froud, Wendy Vecchi, Werner Bischof, Wes Craven, Wes Wilson, Westworld, Wet texture, Wharton Esherick, What We Do In The Shadows, Where is Waldo, Whimsical atmosphere, Wide-angle view, Wildlife Art, Wilfredo Lam, Will Bullas, Will Burrard-Lucas, Willem Claesz. Heda, Willem van der Velde, William Blake, William Clarke Wontner, William H. Johnson, William Harnett, William Heath Robinson, William Henry Fox Talbot, William Holman Hunt, William Kay Blacklock, William Merritt Chase, William Mortensen, William Patino, William Powell Frith, William Stanley Haseltine, William Steig, William Tillyer, William Wendt, William de Morgan, William-Aldophe Bouguereau, Willy Ronis, Wim Delvoye, Winfred Rembert, Winifred Nicholson, Winnie-the-Pooh, Winslow Homer, Winter, Wireframe, Wiring diagrams, Wolf Kahn, Wolfenstein 3D, Wolfgang Laib, Woody, Wotto, Woven texture, Wyndham Lewis, Xenia Hausner, Xkcd, Yanjun Cheng, Yann Arthus-Bertrand, Yasuo Kuniyoshi, Yener Torun, Yiannis Moralis, Yinka Ilori, Yonatan Frimer, Yossi Kotler, Yousuf Karsh, Yu-Gi-Oh, Yuri Shwedoff, Yuriy Shevchuk, Yusaku Kamekura, Yves Brayer, Yves Tanguy, Zak Noyle, Zandra Rhodes, Zao Wou-Ki, Zapatista mask, Zapiro, Zaria Forman, Zarina Hashmi, Zemer Peled, Zentangle, Zhang Daqian, Zimoun, Zio Ziegler".Split(',', StringSplitOptions.None).ToList(); - return artists.OrderBy(el => random.Next()).ToList().First().Trim(); - } - - public Task DoTransformation(PromptDetails pd, MultiClientRunStats stats) - { - var artist = GetRandomArtist(); - var newVersion = $"In the style of {artist}, {pd.Prompt}"; - pd.ReplacePrompt(newVersion, newVersion, TransformationType.AddingArtistStyle); - - return Task.FromResult(true); - } - } -} +using MultiImageClient; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime; +using System.Text; +using System.Threading.Tasks; + + +namespace MultiImageClient +{ + + public class StylizerStep : ITransformationStep + { + private static Random random = new Random(); + + public string Name => nameof(StylizerStep); + + public static string GetRandomArtist() + { + var artists = "3D model, A. R. Penck, A. Y. Jackson, Aardman Animations, Aaron Blaise, Aaron Douglas, Aaron James Draplin, Aaron Reed, Aaron Westerberg, Abanindranath Tagore, Abbas Kiarostami, Abbey Lossing, Aberdeen Bestiary, Aboudia, Abraham Ortelius, Abstract D'Oyley, Achille Castiglioni, Acrylic pour, Action movie, Adam Kubert, Adam Neate, Adam Zyglis, Adi Granov, Adolf Schreyer, Adriaen Brouwer, Adrian Frutiger, Adrianna Papell, Adrienne Segur, Adventure Time, Aelbert Cuyp, Aerial view, Aerin Lauder, African mask, Age of Empires, Agnieszka Lorek, Agostino Iacurci, Aibo, Aida Muluneh, Aimee Stewart, Aires Mateus, Aitor Throup, Alan Aldridge, Alan Fearnley, Alan Kitching, Alan Moir, Alasdair Gray, Alaya Gadeh, Albert Dros, Albert Dubout, Albert Edelfelt, Albert Irvin, Albert Koetsier, Albert Namatjira, Albert Oehlen, Albert Paley, Albert Renger-Patzsch, Alberta Ferretti, Alberto Campo Baeza, Alberto Vargas, Aldo Rossi, Ale Giorgini, Alec Monopoly, Alejandro Gonzalez Inarritu, Aleksi Briclot, Alessandro Mendini, Alex Chinneck, Alex Janvier, Alex Katz, Alex MacLean, Alex Majoli, Alex Raymond, Alexander Girard, Alexander Payne, Alexandre Deschaumes, Alexej von Jawlensky, Alexey Brodovitch, Alfonso Cuaron, Alfred Dehodencq, Alfred Sisley, Alfred Wallis, Alice Kettle, Alice Pike Barney, Alicja Kwade, Alida Akers, Alighiero Boetti, Aline Kominsky-Crumb, Alisa Burke, Allan Kaprow, Alma Thomas, Alphonse Bertillon, Alvaro Castagnet, Amanda Blake, Amanda Sage, Ambient lighting, Ambrogio Lorenzetti, American Girl doll, Ami James, Amigurumi, Amy Brown, Amy Giacomelli, Amy Sol, Ana Mendieta, Anaglyph colors, Andre Courreges, Andre Masson, Andrea Galvani, Andrea Palladio, Andrea Torres Balaguer, Andreas Rocha, Andrew Loomis, Andrew Read, Andrew Salgado, André Franquin, Andy Gilmore, Andy Kubert, Andy Marlette, Andy Rementer, Andy Russell, Angelica Kauffman, Angelo Donghia, Angie Lewin, Angry Birds, Anila Quayyum Agha, Animal Crossing, Animalier fashion, Anita Jeram, Ann Mortimer, Ann Telnaes, Ann Veronica Janssens, Anna Atkins, Anna Griffin, Anna Maria Garthwaite, Anna Pugh, Anna Silivonchik, Anna Sui, Annabelle doll, Anne Cotterill, Anne Gifford, Anne Redpath, Annette Schmucker, Annibale Carracci, Annie Ovenden, Anonymous mask, Anselm Feuerbach, Anthea Hamilton, Anthony Caro, Anthony Gerace, Anthony McCall, Antoine Bourdelle, Antoine Pevsner, Antonio Lopez, Antonio Stradivari, Antony Cairns, Arata Isozaki, Archibald Motley, Architectural plan, Arkanoid, Arnaldo Pomodoro, Arne Svenson, Arnold Genthe, Arnold Lobel, Art Baltazar, Arthur Claude Strachan, Arthur Frank Mathews, Arthur John Elsley, Arthur Melville, Arthur Suydam, Arthur Szyk, Artificial lighting, Aryz, Ashlar texture, Ashraful Arefin, Assemblage, Assembly drawing, Astrological chart, Astronomical chart, Asymmetrical composition, Audemars Piguet, Auguste Perret, Augustus Edwin John, Autochrome print, Autostereogram, Autumn, Axel Vervoordt, Axonometric view, Aztec mask, Azzedine Alaia, B. Kliban, Backlit, Bada Shanren, Baggy fashion, Barbara Chase-Riboud, Barbara Chichester, Barbara Kasten, Barbie, Barnett Newman, Barong mask, Barry Blitt, Barry Windsor-Smith, Bas Princen, Bastien Grivet, Baz Luhrmann, Beachwear fashion, Beatrice Alemagna, Beatriz Milhazes, Befana, Bela Kadar, Ben Eine, Ben Garrison, Ben Heine, Benin art, Benito Jacovitti, Benoit Paille, Bentwood, Bernard Leach, Bernard Plossu, Bernardo Bellotto, Bernd and Hilla Becher, Bernie Fuchs, Bertha Lum, Berthe Morisot, Beryl Cook, Beth Cavener, Beth Moon, Bethany Lowe, Betty Boop doll, Betty Woodman, Betye Saar, Beverly Joubert, Bijou Karman, Bill Amend, Bill Bell, Bill Cunningham, Bill Eppridge, Bill Leak, Bill Owens, Bill Plympton, Bill Schorr, Bill Willingham, Billie Tsien, Billy Wilder, BioShock, Bioluminescent colors, Bjarne Melgaard, Black-and-white colors, Blade Runner, Blek le Rat, Blizzard, Blue Sky Animation Studio, Blue hour, Board game, Bob Coonts, Bob Englehart, Bob Kane, Bob Mackie, Bob Peak, Bob Pepper, Bob Staake, Bob the Builder, Bonnie Marris, Book of Kells, Boris Groh, Boris Mikhailov, Bottom view, Bouchra Jarrar, Brandon Kidwell, Bratz doll, Brent Stirton, Brian Michael Bendis, Brian Stelfreeze, Brom, Bruce McLean, Bruce Onobrakpeya, Buffy the Vampire Slayer, Bumpy texture, Bunny Williams, Bunraku, Butch Hartman, Buzz Lightyear, Byzantine icon, CAD drawing, CFA Voysey, Cake design, Call of Duty, Cameo, Camilla d'Errico, Camille Rose Garcia, Candice Olson, Candlelit, Capability Brown, Carel Fabritius, Carey Chen, Caricature, Carl Brenders, Carl Mydans, Carl Wilhelmson, Carlo Dolci, Carlo Scarpa, Carlos Latuff, Carlos Schwabe, Carly Cushnie, Carmen Herrera, Carol Hagan, Carole Feuerman, Carole Spandau, Carolee Schneemann, Caroll Spinney, Carolyn Blish, Carrie Graber, Carsten Holler, Carsten Witte, Cartography, Casey Baugh, Cassius Marcellus Coolidge, Cath Kidston, Catherine Abel, Catherine Holman, Catherine Opie, Catherine Pooler, Cathy Hegman, Cathy Horvath Buchanan, Cathy Locke, Cathy Wilcox, Cathy Wilkes, Catrin Welz-Stein, Cecil Aldin, Cecil B. DeMille, Cecil Beaton, Cecil Touchon, Cecilia Beaux, Cecilie Manz, Cellular diagram, Chad Dutson, Chad Knight, Chantal Joffe, Charles Bibbs, Charles Burton Barber, Charles Conder, Charles Courtney Curran, Charles Dana Gibson, Charles Dwyer, Charles Eames, Charles Fazzino, Charles Ginner, Charles Gleyre, Charles Gwathmey, Charles Le Brun, Charles Marville, Charles Maurice Detmold, Charles Reiffel, Charles Rennie Mackintosh, Charles Robinson, Charles Sheeler, Charles Spencelayh, Charles Tunnicliffe, Charles White, Charles Wysocki, Charles-Amable Lenoir, Charles-Andre van Loo, Charles-Francois Daubigny, Charley Harper, Charley Toorop, Charlie Adlard, Charline von Heyl, Charlotte Perriand, Charred, Chemical structure, Chen Yifei, Chen Zhen, Chicano art, Chien Chung-Wei, Chiho Aoshima, Childe Hassam, Chip Zdarsky, Chris Burden, Chris Claremont, Chris Johanson, Chris Leib, Chris Ryniak, Chris Samnee, Chris Sanders, Chris Ware, Christi Belcourt, Christian Coigny, Christian Krohg, Christian Louboutin, Christian Marclay, Christian Riese Lassen, Christian Tagliavini, Christine Comyn, Christine Ellger, Christo, Christoffer Relander, Christophe Staelens, Christophe Vacher, Christopher Bucklow, Christopher Corr, Christopher Shy, Christopher Wren, Christy Lee Rogers, Christy Tomlinson, Chrome colors, Chuck Jones, Chuck Sperry, Cimabue, Cinematic lighting, Circuit diagram, Cirque du Soleil, City of God, City plan, Claes Oldenburg, Claire Basler, Claire Droppert, Claire Wendling, Clara Peeters, Clare Elsaesser, Clarence Gagnon, Clark Little, Claude Lorrain, Claude Parent, Claude-Joseph Vernet, Claudia Rogge, Claudia Tremblay, Claudio Silvestrin, Clay Bennett, Clay Mann, Clayton Crain, Clemens Ascher, Cleon Peterson, Cleve Gray, Cliff Chiang, Clifford Coffin, Clifford Harper, Clifford Possum Tjapaltjarri, Climate history graph, Clive Barker, Close-up view, Cluttered composition, Clyfford Still, Coat of Arms, Codex Mendoza, Codex Sinaiticus, Cody Ellingham, Colin Campbell Cooper, Colin McCahon, Collagraph, Color field painting, Coloring-in sheet, Colossal scale, Comedy movie, Comfy atmosphere, Command & Conquer, Commedia dell'Arte, Commedia dell'Arte mask, Complementary colors, Conor Harrington, Conrad Shawcross, Constance Guisset, Constance Spry, Constantin Brancusi, Constantin Kousnetzoff, Contour plot, Contrasted lighting, Cool colors, Coordinate system paper, Copperplate engraving, Corey Arnold, Corn husk doll, Cornell Capa, Counter-Strike, Cracked surface, Craigie Aitchison, Craquelure, Crayon Shin-chan, Crinkled texture, Cristina Coral, Cristina McAllister, Cross-section diagram, Cross-section plan, Csaba Markus, Cubism, Cuneiform, Cutaway view, Cy Twombly, D. W. Griffith, DAIN, DALeast, Daarken, Daisuke Yokota, Dan Abnett, Dan Brereton, Dan Graham, Dan Povenmire, Dan Slott, Dan Spiegle, Dan Witz, Dana Trippe, Danh Vo, Daniel Buren, Daniel Eskridge, Daniel Garber, Daniel Kordan, Danny Clinch, Danny O'Connor, Dante Gabriel Rossetti, Daphne Odjig, Darek Zabrocki, Daria Endresen, Daria Petrilli, Dariusz Klimczak, Dark atmosphere, Dark foreboding colors, Daron Nefcy, Darrell K. Sweet, Daryl Cagle, Dashiki fashion, Data visualization, Dav Pilkey, Dave Coverly, Dave Granlund, Dave Heath, Dave Sim, Dave Stevens, Dave Whamond, David A. Hardy, David Adjaye, David Aja, David Alfaro Siqueiros, David Altmejd, David Bellemere, David Bomberg, David Bowers, David Brayne, David Bromley, David Brown Milne, David Burnett, David Cerny, David Choe, David Cox, David Downton, David Drebin, David Driskell, David Goldblatt, David Hammons, David Hettinger, David Horsey, David Hurn, David Inshaw, David Keochkerian, David Lean, David Ligare, David Lloyd Glover, David Mann, David Martiashvili, David Michael Bowers, David Mould, David Nash, David Octavius Hill, David Palumbo, David Plowden, David Pope, David Rees, David Renshaw, David Roberts, David Rowe, David Salle, David Seymour, David Shepherd, David Sims, David Teniers the Younger, David Trubridge, David Tutwiler, David Uhl, David Walker, David Welker, David Wightman, David Wilkie, David Yarrow, Davide Sorrenti, Dawn, Dayak art, Dead Cells, Dean Crouser, Deana Lawson, Debbie Criswell, Deborah Azzopardi, Deborah Turbeville, Declan Shalvey, DeepDream, Deirdre Sullivan-Beeman, Del Kathryn Barton, Delftware, Delphin Enjolras, Delphine Diallo, Demetre Chiparus, Dennis Oppenheim, Dennis Stock, Denys Lasdun, Depressed atmosphere, Derek Gores, Derek Jarman, Desaturated colors, Detailed, Diablo, Diagonal composition, Dick Bruna, Dick Giordano, Dick Sprang, Didier Lourenco, Diego Dayer, Dieter Rams, Diffused lighting, Digimon, Dim lighting, Dima Dmitriev, Dimitry Roulland, Dina Wakley, Dion Lee, Disco Elysium, Disney Studio, Distressed Photo, Doctor Who, Dod Procter, Dogon art, Domenichino, Don Blanding, Don Dixon, Don Heck, Don Hertzfeldt, Donald Judd, Donald Zolan, Donatella Versace, Donkey Kong, Donna Huanca, Doodle, Doom, Dora Carrington, Dorina Costras, Doris Salcedo, Dorothea Sharp, Dorothy Draper, Dorothy Johnstone, Doug Aitken, Doug Chinnery, Doug Hyde, Doug Marlette, Doug TenNapel, Douglas Gordon, Douglas Kirkland, Douglas Smith, Dr. Seuss, Dragon Quest: The Adventure of Dai, Drama movie, Dramatic lighting, Dran, Drawing grid paper, Dreamworks, Dreamy atmosphere, Dries van Noten, Duane Hanson, Duccio, Dungeons and Dragons, Duotone colors, Dusk, Duy Huynh, Dwight William Tryon, E. J. Bellocq, Earle Bergey, Earthy tones, Eastman Johnson, Ebru Sidar, Ed Hardy, Ed McGuinness, Ed Templeton, Eddie Colla, Edgar Maxence, Edith Head, Edmund Tarbell, Edmund de Waal, Edoardo Tresoldi, Edouard Boubat, Edouard Cortes, Eduard Gaertner, Eduard Veith, Eduardo Chillida, Eduardo Paolozzi, Eduardo Souto Moura, Edward Ardizzone, Edward Atkinson Hornel, Edward Bawden, Edward Blair Wilkins, Edward Cucuel, Edward Henry Potthast, Edward Julius Detmold, Edward Okun, Edward Poynter, Edward Wadsworth, Edwin Henry Landseer, Edwin Landseer, Edwin Lord Weeks, Edwin Lutyens, Eerie atmosphere, Eero Aarnio, Egyptian funeral mask, Eiko Ojala, Eikoh Hosoe, Eileen Gray, Elaine Lustig Cohen, Eleanor Antin, Eleanor Vere Boyle, Electrical layout, Electromagnetic spectrum chart, Elena Kotliarker, Elevation drawing, Elger Esser, Elia Kazan, Elia Locardi, Elihu Vedder, Elisabeth Fredriksson, Elisabeth Sonrel, Elisabeth Vigee Le Brun, Elise Palmigiani, Elizabeth Murray, Elizabeth Nourse, Elizabeth Peyton, Elizabeth Shippen Green, Elke Vogelsang, Ellsworth Kelly, Elmer Bischoff, Elsa Schiaparelli, Emanuel Ungaro, Embossed texture, Emil Alzamora, Emil Carlsen, Emil Melmoth, Emile Claus, Emile Vernon, Emiliano Ponzi, Emily Balivet, Emily Carr, Emily Henderson, Emily Kame Kngwarreye, Emmanuelle Moureaux, Emory Douglas, Encaustic tile, Enid Blyton, Epic scale, Epic view, Erez Marom, Eric Cahan, Eric Canete, Eric Carle, Eric Dowdle, Eric Drooker, Eric Fischl, Eric Gill, Eric Lacombe, Eric Lafforgue, Eric Ravilious, Eric Standley, Erich Hartmann, Erik Jones, Erik Kessels, Erin Gregory, Ernest Lawson, Ernie Ball, Ernie Barnes, Ernst Fuchs, Ernst Lubitsch, Ernst Wilhelm Nay, Erte, Erwin Redl, Esad Ribic, Esaias van de Velde, Esao Andrews, Euan Uglow, Eudora Welty, Eugen Chisnicean, Eugene Boudin, Eugene Delacroix, Eugene Galien-Laloue, Eugene Grasset, Eugene Richards, Eugene de Blaas, Eustache Le Sueur, Eva Hesse, Eva Rothschild, Eve Arnold, Eve Ventrue, Evelyn De Morgan, Evelyn Dunbar, Everett Shinn, Evgeni Dinev, Evgeni Gordiets, Evgeny Lushpin, Exploded assembly, Exploded view, Extreme caricature, Exuberant atmosphere, Ezra Jack Keats, Ezra Petronio, F. C. Gundlach, Faberge egg, Fabian Oefner, Fabienne Rivory, Fabio Hurtado, Facepaint, Fairfield Porter, Faith47, Faiza Maghni, Fallout 4, Family Guy, Fantasy movie, Fatima Ronquillo, Fausto Puglisi, Federico Babina, Felice Beato, Felice Varini, Felix Gonzalez-Torres, Felix Vallotton, Ferdinand Hodler, Ferdinand Knab, Ferdinando Scianna, Fern Isabel Coppedge, Fernand Fonssagrives, Fernando Amorsolo, Fernando Botero, Ferris Plock, Ferrofluid, Festima mask, Fiber optic light painting, Fibrous texture, Filigree, Filip Hodas, Filippo Minelli, Fine art etching, Finn Juhl, Finnian MacManus, Fintan Magee, Firelit, First-person view, Fish scale texture, Fish-eye view, Fitz Henry Lane, Flamboyant atmosphere, Flashy colors, Flora Waycott, Florence Broadhurst, Florence Harrison, Florence Knoll, Florian Nicolle, Florine Stettheimer, Fluffy texture, Fluorescent colors, Foggy, Food photography, Footage from CCTV, Found object art, Fra Angelico, Fragmented composition, Frances Brundage, Frances Hodgkins, Frances MacDonald, Frances Tipton Hunter, Francesco Albani, Francesco Borromini, Francesco Clemente, Francesco Guardi, Francesco Solimena, Francis Bernard Dicksee, Francis Manapul, Francis Newton Souza, Francisco de Zurbaran, Franck Bohbot, Franco Fontana, Francois Morellet, Francois Schuiten, Francois Truffaut, Francois-Xavier Lalanne, Francoise Nielly, Frank Bowling, Frank Bramley, Frank Cadogan Cowper, Frank Capra, Frank Holl, Frank McCarthy, Frank Weston Benson, Frankenweenie, Franklin Booth, Franklin Carmichael, Frans Floris, Frans Francken the Younger, Frans Hals, Frans Masereel, Frantisek Kupka, Franz von Stuck, Fred Benes, Fred Calleri, Fred Herzog, Fred Sandback, Fred Stein, Fred Tomaselli, Frederic Bazille, Frederic Remington, Frederic William Burton, Frederick Arthur Bridgman, Frederick Carl Frieseke, Frederick Catherwood, Frederick Cayley Robinson, Frederick Hammersley, Frederick John Kiesler, Frederick McCubbin, Frederick Varley, Friedel Dzubas, FriendsWithYou, Fritz Hansen, Fritz Zuber-Buhler, Frontal view, Fujifilm instax, Full body-height view, Fumihiko Maki, Functional block diagram, Furby, Furry texture, Gabor Csupo, Gabriel Isak, Gabriele Dell'otto, Gail Albert Halaban, Gail Gibbons, Gainax, Gantt chart, Garance Dore, Garth Ennis, Gary Baseman, Gary Benfield, Gary Hume, Gary Petersen, Gary Varvel, Gaston Le Touche, Gene Colan, Gene Davis, Genealogical tree, Genetic map, Gennady Spirin, Genndy Tartakovsky, Gentile Bellini, Geoffrey Beene, Geoglyph, Geological cross-section, George Biddle, George Caleb Bingham, George Callaghan, George Catlin, George Clausen, George Condo, George Cruikshank, George Cukor, George Digalakis, George Eastman, George Goodwin Kilburne, George Henry Boughton, George Henry Yewell, George Hoyningen-Huene, George Nakashima, George Nelson, George Pemba, George Perez, George Petty, George Platt Lynes, George Rodrigue, George Romney, George Segal, George Shaw, George Stefanescu, George Stubbs, George Tice, George Underwood, George Yepes, Georges Clairin, Georges Dambier, Georges Hobeika, Georges Lacombe, Georges Lemmen, Georges Lepape, Georges Rochegrosse, Georges Rousse, Georges Seurat, Georges de La Tour, Georgia O'Keeffe, Georgy Kurasov, Gerald Harvey Jones, Gerard ter Borch, Gerd Arntz, Gerda Wegener, Gered Mankowitz, Germaine Krull, Germaine Richier, Gerrit Dou, Gerrit Rietveld, Gerry Anderson, Gerry Johansson, Gertrude Jekyll, Ghada Amer, Giambattista Valli, Gian Lorenzo Bernini, Gianfranco Ferre, Gianni Berengo Gardin, Gifford Beal, Gil Bruvel, Gilbert Garcin, Gilbert Shelton, Gilbert Stuart, Gilbert Williams, Gillian Wearing, Ginette Callaway, Gio Ponti, Giorgetto Giugiaro, Giorgio Morandi, Giorgio Vasari, Giorgio de Chirico, Giotto, Giovanni Battista Piranesi, Giovanni Battista Tiepolo, Giovanni Battista Venanzi, Giovanni Boldini, Giovanni Domenico Tiepolo, Giovanni Segantini, Giuseppe Cristiano, Giuseppe Penone, Giuseppe de Nittis, Glam Rock fashion, Glen Orbik, Glenn Fabry, Glenn Ligon, Glenn Murcutt, Glitchy, Gloomy atmosphere, Glossy texture, Gnaga mask, GodMachine, Godfried Schalcken, Golden hour, Golgo 13, Gonzalo Borondo, Googie architecture, Google Street View, Gordon Matta-Clark, Goro Fujita, Grace Cossington Smith, Graciela Rodo Boulanger, Gradient colors, Graham Gercken, Graham Sutherland, Granblue Fantasy, Grandma Moses, Grant Morrison, Granular texture, Graph paper, Gravity Falls, Gray Malin, Gray Morrow, Greaser fashion, Greek Theatre mask, Greg Capullo, Greg Gorman, Greg Hildebrandt, Greg Land, Greg Manchess, Greg Nicotero, Greg Olsen, Greg Rucka, Greg Simkins, Greg Staples, Greg Tocchini, Grete Stern, Grigory Gluckmann, Grinling Gibbons, Gritty texture, Ground-level view, Guercino, Guerrilla Girls, Guido Argentini, Guido Borelli da Caluso, Guido Crepax, Guido Guidi, Guido Reni, Guido van Helten, Guild Wars 2, Guillaume Corneille, Guillaume Seignac, Guillem H. Pongiluppi, Gum Bichromate, Gunther Uecker, Gus Van Sant, Gustaf Tenggren, Gustav Bauernfeind, Gustav Stickley, Gustav-Adolf Mossa, Gustave Baumann, Gustave Boulanger, Gustave Courbet, Gustave De Smet, Gustave Eiffel, Gustave Le Gray, Gustave Loiseau, Gustave Moreau, Gustave de Jonghe, Guweiz, Guy Aitchison, Guy Buffet, Guy Carleton Wiggins, Guy Coheleach, Guy Delisle, Guy Denning, Guy Fawkes mask, Guy Harvey, Guy Maddin, Guy Rose, Gyorgy Kepes, Gyotaku, H. A. Brendekilde, H. P. Lovecraft, HENSE, Haas Brothers, Haibane Renmei, Haida art, Half-Life, Halo, Hamtaro, Hanafuda card, Haniwa, Hanna-Barbera, Hannah Dale, Hannah Hoch, Hanne Darboven, Hans Erni, Hans Feurer, Hans Heysen, Hans Hofmann, Hans Hollein, Hans Memling, Hans Op de Beeck, Hans Wegner, Harley Earl, Harmony Korine, Harold Cazneaux, Harold Gilman, Haroon Mirza, Haroshi, Harriet Backer, Harriet Lee-Merrion, Harrison Fisher, Harry Anderson, Harry Bertoia, Harry Furniss, Harry Siddons Mowbray, Harry Watrous, Harry Weisburd, Harsh lighting, Harumi Hironaka, Harun Farocki, Harvey Dunn, Hassan Hajjaj, Hasui Kawase, Hatsune Miku, Hayv Kahraman, Headgear, Heat map, Heather Galler, Hebru Brantley, Hector Guimard, Hedi Slimane, Hedi Xandt, Heidi Swapp, Heinrich Kley, Heinz Mack, Helen Allingham, Helen Cottle, Helen Dardik, Helen Frankenthaler, Helen Lundeberg, Helen Musselwhite, Helene Beland, Helene Schjerfbeck, Helio Oiticica, Hello Kitty, Helmut Lang, Hendrick Avercamp, Hendrick ter Brugghen, Hendrik Kerstens, Hengki Koentjoro, Henning Larsen, Henri Fantin-Latour, Henri Gaudier Brzeska, Henri Le Sidaner, Henri Lebasque, Henri Manguin, Henri Michaux, Henri-Jules-Jean Geoffroy, Henrietta Harris, Henriette Ronner-Knip, Henrik Fisker, Henry Asencio, Henry Justice Ford, Henry Moret, Henry Roderick Newman, Henry Scott Tuke, Henry Selick, Henry Wessel, Herakut, Heraldry, Herb Trimpe, Herbert Bayer, Herman Brood, Herman Miller, Hermann Nitsch, Herringbone texture, Herve Leger, Herzog & de Meuron, Heywood Hardy, Hideo Kojima, Hieratic script, High key photography, Hilla Becher, Hina doll, Hipgnosis, Hiro Mashima, Hiroshi Nagai, Hiroshi Yoshida, History movie, Hito Steyerl, Holly Andres, Holly Hobbie, Holton Rower, Hong-Oai Don, Honore Daumier, Hopi art, Horror movie, Howard Arkley, Howard Behrens, Howard Carter, Howard Chandler Christy, Howard Chaykin, Howard David Johnson, Howard Pyle, Howard Schatz, Howard Terpning, Howardena Pindell, Hubert Robert, Hudoq mask, Hugh Ferriss, Hugues Merle, Huichol art, Huma Bhabha, Humberto Ramos, Humphry Repton, Hurricane, Ian Howorth, Ian Miller, Idris Khan, Igor Mitoraj, Igor Morski, Igor Zenin, Ikenaga Yasunari, Ildiko Neer, Illuminated manuscript, Imants Tillers, Impasto, Inca art, Incandescent colors, India Mahdavi, Indonesian mask, Inessa Garmash, Inez and Vinoodh, Inge Morath, Ingrid Baars, Intaglio, Intarsia woodworking, Intricate, Inuit art, Iren Horrors, Irene Sheri Vishnevskaya, Iridescent colors, Iris Scott, Irma Stern, Irmgard Schoendorf Welch, Irving Amen, Isaac Cruikshank, Isaac Mizrahi, Isabel Toledo, Isabelle Arsenault, Isabelle de Borchgrave, Isao Takahata, Isay Weinfeld, Isekai anime style, Isometric projection, Ithell Colquhoun, Ito Jakuchu, Itzchak Tarkay, Iurie Belegurschi, Ivan Brunetti, Ivan Chermayeff, Ivan Kramskoi, J. E. H. MacDonald, J. J. Abrams, J. J. Grandville, J. Scott Campbell, J.M.W. Turner, J.P. Targete, Jack Butler Yeats, Jack Davison, Jack Gaughan, Jack Kirby, Jack Sorenson, Jack Ziegler, Jackie Morris, Jackie Ormes, Jacob Aue Sobol, Jacob Epstein, Jacob Jordaens, Jacob Philipp Hackert, Jacob Riis, Jacob van Ruisdael, Jacopo Bassano, Jacques Amans, Jacques Lipchitz, Jacques Majorelle, Jacques Monory, Jacques Rivette, Jaehyo Lee, Jaime Hayon, Jama Jurabaev, Jamel Shabazz, James Abbott McNeill Whistler, James Avati, James Avery, James Bama, James Barnor, James Brooks, James Casebere, James Dietz, James Dyson, James Ensor, James Gillray, James Gunn, James Guthrie, James Jean, James Nizam, James Paick, James R. Eads, James Rosenquist, James Sant, James Thurber, James Tissot, James Turrell, James Wan, James Welling, James Wells Champney, James Wilson Morrice, Jamie Heiden, Jamini Roy, Jan Berenstain, Jan Brett, Jan Davidsz de Heem, Jan Fabre, Jan Gossaert, Jan Mankes, Jan Matson, Jan Toorop, Jan Urschel, Jan van Haasteren, Jan van Kessel the Elder, Jan van Scorel, Jane Burch Cochran, Jane Newland, Jane Small, Janet Delaney, Janet Echelman, Janine Antoni, Jannis Kounellis, Januz Miralles, Jarek Kubicki, Jarek Puczel, Jason Edmiston, Jason Freeny, Jason Middlebrook, Jason deCaires Taylor, Jasper Francis Cropsey, Jaume Capdevila, Jaume Plensa, Javier Mariscal, Javiera Estrada, Jay DeFeo, Jean Arp, Jean Beraud, Jean Dubuffet, Jean Fouquet, Jean Jullien, Jean Metzinger, Jean Patou, Jean Prouve, Jean-Antoine Houdon, Jean-Antoine Watteau, Jean-Baptiste Carpeaux, Jean-Baptiste Corot, Jean-Baptiste Greuze, Jean-Baptiste-Simeon Chardin, Jean-Etienne Liotard, Jean-Gabriel Domergue, Jean-Honore Fragonard, Jean-Joseph Benjamin-Constant, Jean-Leon Gerome, Jean-Louis Forain, Jean-Michel Frank, Jean-Paul Goude, Jean-Paul Riopelle, Jean-Philippe Delhomme, Jean-Pierre Gibrat, Jean-Sebastien Rossbach, Jeanine Michna-Bales, Jeanne Lanvin, Jeannette Guichard-Bunel, JeeYoung Lee, Jeff Kinney, Jeff Lemire, Jeff Soto, Jeffrey Milstein, Jeffrey Smart, Jeffrey T. Larson, Jen Bartel, Jen Sorensen, Jennifer Lommers, Jennifer Rubell, Jenny Frison, Jeppe Hein, Jeremiah Ketner, Jeremy Geddes, Jeremy Lipking, Jeremy Scott, Jerry N. Uelsmann, Jesper Ejsing, Jessica Backhaus, Jessica Drossin, Jessica Durrant, Jessica Galbreth, Jessica Roux, Jessica Stockholder, Jessie M. King, Jessie Willcox Smith, Jill Freedman, Jillian Tamaki, Jim Aparo, Jim Burns, Jim Cheung, Jim Daly, Jim Dine, Jim Fitzpatrick, Jim Hansel, Jim Holland, Jim Kazanjian, Jim Lambie, Jim Lee, Jim Starlin, Jim Warren, Jim Woodring, Jimmy Chin, Jimmy Marble, Jimmy Margulies, Jimmy Palmiotti, Jiro Kuwata, Jo Ann Callis, Jo Grundy, Joachim Beuckelaer, Joachim Patinir, Joachim Wtewael, Joan Cornella, Joan Eardley, Joan Mitchell, Joan Snyder, Joao Ruas, Joaquin Sorolla, Jocelyn Hobbie, Jock, Jody Bergsma, Joe Fenton, Joe Heller, Joe Jusko, Joe Madureira, Joe Quesada, Joe Webb, Joel Pett, Joel Rea, Joel Robison, Joel Shapiro, Joel Sternfeld, Joel-Peter Witkin, Joep Bertrams, Joep van Lieshout, Johan Messely, Johanna Basford, Johanna Gullichsen, Johannes Itten, Johannes VanDerBeek, Johfra Bosschart, John Anster Fitzgerald, John Atkinson Grimshaw, John Baldessari, John Batho, John Bellany, John Berkey, John Blanche, John Brack, John Bratby, John Burningham, John Butler Yeats, John Carpenter, John Cassaday, John Chamberlain, John Constable, John Crome, John Currin, John Darkow, John Divola, John Duncan, John Everett Millais, John Frederick Kensett, John Frederick Lewis, John George Brown, John Gould, John Gutmann, John Heartfield, John Hejduk, John Holcroft, John James Audubon, John Kricfalusi, John La Farge, John Larriva, John Lasseter, John Lautner, John Lavery, John Leech, John Lovett, John Lowrie Morrison, John Lurie, John Maler Collier, John Marin, John Martin, John Melhuish Strudwick, John Nash, John Olsen, John Paul Caponigro, John Perceval, John Philip Falter, John Piper, John Pitre, John Reuss, John Roddam Spencer Stanhope, John Ruskin, John Sell Cotman, John Singleton Copley, John Sloane, John Stezaker, John Tenniel, John Trumbull, John Twachtman, John William Godward, Johnathan Harris, Johnny Morant, Johnson Tsang, Jon Carling, Jon Foster, Jon Klassen, Jon Kudelka, Jon McNaught, Jon McNaughton, JonOne, Jonathan Adler, Jonathan Meese, Joost Swarte, Joram Roukes, Jordan Grimmer, Jordi Bernet, Jorge Maia, Joris Laarman, Jorn Utzon, Jos Collignon, Josan Gonzalez, Jose Guadalupe Posada, Jose Royo, Josef Frank, Josef Hoffmann, Josef Kote, Joseph Farquharson, Joseph Kosuth, Joseph Lorusso, Joseph Pennell, Joseph Stella, Josephus Laurentius Dyckmans, Josh Adamski, Josh Keyes, Josh Kirby, Joshua Reynolds, Joyful atmosphere, Juan Gris, Judith Clay, Judith Desrosiers, Judith Leyster, Judy Blame, Judy Dater, Juergen Teller, Jules Bastien-Lepage, Jules Breton, Jules Feiffer, Jules Olitski, Jules Pascin, Julian Beever, Julian Glander, Julian Opie, Julian Schnabel, Juliana Nan, Julie Arkell, Julie Paschkis, Julio Larraz, Julio Le Parc, Julius Leblanc Stewart, Junkanoo mask, Junya Ishigami, Jurassic Park, Jusepe de Ribera, Justin Gaffrey, Justin Gerard, Justin Roiland, Justina Blakeney, Justine Kurland, Kabuki, Kabuki mask, Kader Attia, Kaethe Butcher, Kaffe Fassett, Kamen Rider, Kanban wall, Kangra painting, Kansuke Yamamoto, Karel Appel, Karen Noles, Karen Tarlton, Karin Kuhlmann, Karl Knaths, Karl Schmidt-Rottluff, Karla Gerard, Kate Baylay, Kate Greenaway, Kate Leth, Kate Spade, Kathakali mask, Katharina Grosse, Kathrin Honesta, Kathrin Longhurst, Kathy Fornal, Katie Scott, Katsuya Terada, Katy Grannan, Katy Smail, Katya Gridneva, Kees van Dongen, Keiichi Tanaami, Keiji Inafune, Keisai Eisen, Kelley Jones, Kelly Beeman, Kelly Freas, Kelly Hoppen, Kelly McKernan, Kelly Wearstler, Ken Domon, Ken Fulk, Ken Sugimori, Kenneth Anger, Kenneth Josephson, Kenneth Noland, Kenneth Snelson, Kenojuak Ashevak, Kenro Izu, Kent Monkman, Kenzo, Kerby Rosanes, Kerem Beyit, Kerry James Marshall, Kestutis Kasparavicius, Kevin Carter, Kevin Maguire, Kevin McNeal, Kevin Nowlan, Kevin Siers, Kevin Sloan, Kevin Wada, Kewpie doll, Khokhloma, Kidrobot, Kilian Eng, Kim Keever, Kingdom Hearts, Kinuko Y. Craft, Kirlian photography, Kirsty Mitchell, Kishangarh painting, Kishin Shinoyama, Kitty Lange Kielland, Klaus Frahm, Knox Martin, Kokeshi doll, Koloman Moser, Konstantin Chaykin, Konstantin Grcic, Konstantin Razumov, Koson Ohara, Kourtney Roy, Krampusnacht festival mask, Krenz Cushart, Kris Knight, Kris Van Assche, Kristina Makeeva, Kubo And The Two Strings, Kukeri, Kurt Geiger, Kurt Hutton, Kurt Wenner, Kurzgesagt, Kuzma Petrov-Vodkin, Kwakiutl mask, Kyffin Williams, Kylli Sparre, Kyoto Animation studio, LaToya Ruby Frazier, Ladislav Sutnar, Lalaloopsy, Lalla Essaydi, Lantern-lit, Larry Bell, Larry MacDougall, Larry Towell, Larry Zox, Lascaux, Latex texture, Lath art, Laura Ashley, Laura Callaghan, Laura Iverson, Laura Makabresku, Laurent Parcelier, Laurie Lipton, Laurie Simmons, Lawren Harris, Lawrence Weiner, Layered composition, Layered paper, LeRoy Grannis, Lee Bermejo, Lee Friedlander, Lee Jae-Hyo, Lee Krasner, Lee Middleton doll, Lee Mullican, Legend of the Cryptids, Leigh Bowery, Len Lye, Leo Caillard, Leo Fender, Leo Villareal, Leo and Diane Dillon, Leon Bonnat, Leon Golub, Leon Kossoff, Leon Tukker, Leonard Freed, Les Paul, Leszek Bujnowski, Levi Strauss, Lewis Morley, Li Hongbo, Li Wei, Liam Brazier, Liam Gillick, Lieke Van der Vorst, Light atmosphere, Light colors, Lilia Alvarado, Lilla Cabot Perry, Lim Heng Swee, Limbourg brothers, Lina Bo Bardi, Linda Ravenscroft, Lindisfarne Gospels, Linnea Strid, Lisa Congdon, Lisa Golightly, Lisa Graa Jensen, Lisa Holloway, Lisa Parker, Lisa Pressman, Lisbeth Zwerger, Lisi Martin, Lit by lightning, Lita Cabellut, Lite Brite art, Liu Bolin, Lizzie Fitch, Lois Dodd, Lois Greenfield, Long exposure photography, Loomis Dean, Looney Tunes, Loretta Lux, Lori Earley, Lori Nix, Lorser Feitelson, Louella Pettway, Loui Jover, Louis Comfort Tiffany, Louis Faurer, Louis Janmot, Louis Marcoussis, Louis Rhead, Louis Ritman, Louis Stettner, Louis Valtat, Louise Balaam, Louise Dahl-Wolfe, Louise Lawler, Louise McNaught, Louise Nevelson, Lovis Corinth, Low key photography, Low vantage point view, Low-hanging fog, Lowell Herrero, Lubaina Himid, Luc Tuymans, Luca Giordano, Luca della Robbia, Lucas Cranach the Elder, Lucas Cranach the Younger, Lucas Foglia, Lucha Libre mask, Lucia Heffernan, Lucia Rafanelli, Lucie Rie, Lucien Clergue, Lucien Pissarro, Lucienne Day, Lucio Fontana, Lucy Glendinning, Lucy Grossmith, Lucy Madox Brown, Ludwig Deutsch, Ludwig Favre, Ludwig Meidner, Luigi Ghirri, Luigi Loir, Luigi Russolo, Luis Barragan, Luis Bunuel, Luis Ricardo Falero, Luke Chueh, Luke Jerram, Luminous colors, Lygia Clark, Lynd Ward, Lynda Barry, Lynette Yiadom-Boakye, Lynn Hershman Leeson, Lyonel Feininger, MVRDV, Mab Graves, Mabel Lucie Attwell, Maciej Kuciara, Macro view, Madeleine Vionnet, Madeline Weinrib, Magali Villeneuve, Magdalena Abakanowicz, Maggie Holmes, Maggie Taylor, Magic: The Gathering, Magical atmosphere, Mai Thu Perret, Maira Kalman, Malcolm Liepke, Malika Favre, Mamoru Hosoda, Mana Neyestani, Maneki-neko, Manfred Kielnhofer, Manolo Blahnik, Maori art, Map, Maquette, Marbled texture, Marc Adamus, Marc Allante, Marc Johns, Marc Muench, Marc Newson, Marc Riboud, Marcantonio Raimondi, Marcel Duchamp, Marcel Janco, Marcel Mouly, Marcello Barenghi, Marco Mazzoni, Mardi gras mask, Margaret Macdonald Mackintosh, Margaret Tarrant, Margarita Kareva, Maria Sibylla Merian, Marian Kamensky, Marianne North, Marianne von Werefkin, Mariano Vivanco, Marie Spartali Stillman, Mariko Mori, Marimekko, Marine photography, Marino Marini, Mario Botta, Mario Buatta, Mario Sorrenti, Marion Peck, Marjolein Bastin, Mark Bagley, Mark Brooks, Mark Fiore, Mark Grotjahn, Mark Hearld, Mark Knight, Mark Manders, Mark Riddick, Mark Ruwedel, Mark Schultz, Mark Shaw, Mark Spain, Mark Waid, Mark Wallinger, Mark di Suvero, Marsden Hartley, Marsel van Oosten, Marta Abad Blay, Martin Deschambault, Martin Johnson Heade, Martin Lewis, Martin Margiela, Martin Rowson, Martin Schongauer, Martina Hoogland Ivanow, Martyn Turner, Marvel, Mary Anning, Mary Cassatt, Mary Charles, Mary Delany, Mary Engelbreit, Mary Quant, Mary Sibande, Masaaki Sasamoto, Masaaki Yuasa, Masahisa Fukase, Masamune Shirow, Masao Yamamoto, Mass Effect, Massimiliano Fuksas, Massimo Vitali, Massive scale, Mat Collishaw, Mati Klarwein, Matt Adnate, Matt Furie, Matt Mahurin, Matt Rhodes, Matt Sesow, Matt Wuerker, Matte texture, Matteo Massagrande, Matthaus Merian the Elder, Matthew Pillsbury, Matthias Grunewald, Matthias Haker, Matthias Heiderich, Maud Humphrey, Maud Lewis, Max Beckmann, Max Bill, Max Gimblett, Max Pechstein, Max Rive, Max Weber, Maximalist composition, Maxime Maufra, Maximilien Luce, Maya Deren, Maya Lin, Mayan art, Maynard Dixon, Mead Schaeffer, Medieval Bestiary, Medieval Codex, Mega man, Megan Hess, Megan Massacre, Meghan Howland, Mehndi, Meiji Period photography, Meindert Hobbema, Mel Bochner, Mel Ramos, Melancholic atmosphere, Melissa Launay, Melozzo da Forli, Melvin Sokolsky, Memphis design, Meredith Frampton, Meredith Marsone, Mert Alas, Mert Otsamo, Mert and Marcus, Metal Gear Solid, Metallic colors, Mexican muralism, Miami Vice, Michael Ancher, Michael Aram, Michael Borremans, Michael Breitung, Michael Carson, Michael Cheval, Michael Cho, Michael Craig-Martin, Michael Creese, Michael Eastman, Michael Garmash, Michael Heizer, Michael Humphries, Michael Hussar, Michael Hutter, Michael Kidner, Michael Lang, Michael Leunig, Michael Page, Michael Parkes, Michael Ramirez, Michael Shapcott, Michael Sowa, Michael Sweerts, Michael Thonet, Michael de Adder, Michel Comte, Michelangelo, Micrographia, Mid-century modern, Midday, Miffy, Miho Hirano, Mika Ninagawa, Mikalojus Konstantinas Ciurlionis, Mike Luckovich, Mike Peters, Mike Zeck, Miles Redd, Millie Marotta, Millimeter squared paper, Milo Manara, Milt Kahl, Milton Avery, Mindy Sommers, Minecraft, Miniature scale, Minimalist composition, Minjae Lee, Minkowski diagram, Minoan art, Minor Martin White, Mira Schendel, Miroslav Sasek, Misty, Mitch Dobrowner, Mitsumasa Anno, Miyamoto Musashi, Mo Willems, Moche art, Moebius, Moises Levy, Molang, Molecular model, Molly Brett, Molly Harrison, Momiji Message Dolls, Mondo Guerra, Monica Blatton, Monica Bonvicini, Monique Lhullier, Monir Shahroudy Farmanfarmaian, Monochrome colors, Monokubo, Moonlit, Morag Myerscough, Morgan Maassen, Morgan Weistling, Morphsuits, Mort Kunstler, Mort Walker, Mortal Kombat, Mothmeister, Motion blur, Moxie Girlz, Mr. Brainwash, Mr. Potato Head, Muted colors, My Little Pony, Nacho Carbonell, Naeem Khan, Namibian Ovambo mask, Nancy Noel, Naoya Hatakeyama, Narayan Shridhar Bendre, Natalia Goncharova, Nate Beeler, Nate Berkus, Nathalie Du Pasquier, National Geographic magazine, Natural lighting, Naum Gabo, Navid Baraty, Nazca lines, Ndebele art, Neil Blevins, Neil Harbisson, Neil Krug, Neil Leifer, Nendoroid, Neon colors, Netsuke, Neutral Color Scheme, Nicholas Alan Cope, Nicholas Hilliard, Nick Anderson, Nick Gentry, Nick Park, Nickelodeon, Nickolas Muray, Nico Delort, Nicolai Fechin, Nicolas Bruno, Nicolas Delort, Nicolas Mignard, Nicolas Poussin, Nicolas de Stael, Nicole Eisenman, Nigel van Wieck, Night, Nike Savvas, Niki Boon, Niki de Saint Phalle, Nina Chanel Abney, Nina Leen, Nina Ricci, Nirav Patel, Njideka Akunyili Crosby, Nkisi figure, Noah Bradley, Nobuo Sekine, Noell Oszvald, Noemie Goudal, Noh Theater mask, Nona Faustine, Nona Limmen, Nora Heysen, Noriyoshi Ohrai, Norm Breyfogle, Norman Bel Geddes, Norman Bluhm, Norman Cornish, Norman Lewis, Norman Lindsay, Norman McLaren, Norman Parkinson, Northern lights, Norval Morrisseau, Nuragic mask, Nuremberg Chronicle, Nychos, Odilon Redon, Ogham, Oily texture, Ok go, Oleg Cassini, Oleg Zhivetin, Olek, Olha Darchuk, Olimpia Zagnoli, Oliver Stone, Olivier Coipel, Olivier Mosset, Olivier Theyskens, Oliviero Toscani, Olly Moss, Olmec art, Omar Galliani, Omar Victor Diop, On Kawara, Ori Gersht, Orla Kiely, Orlan, Orthographic view, Oscar Tuazon, Osman Hamdi Bey, Oswald Achenbach, Otto Schmidt, Ottoman art, Outsider art, Overhead view, Oxana Zaika, PJ Crook, Pablo Lobato, Pac-Man, Pacita Abad, Paco Rabanne, Paige Evans, Pallava architecture, Panoramic composition, Panoramic view, Paola Navone, Paola Pivi, Paolo Uccello, Paolo Veronese, Papercraft, Parametric art, Parametric drawing, Paresh Nath, Parquetry, Pascale Marthine Tayou, Pastel colors, Pat Bagley, Patek Philippe, Patent drawing, Patricia Polacco, Patricia Voulgaris, Patrick Chappatte, Patrick Heron, Patrick Seymour, Paul Cadden, Paul Caponigro, Paul Delvaux, Paul Gustav Fischer, Paul Guy Gantner, Paul Hedley, Paul Jenkins, Paul Kaptein, Paul Kenton, Paul Lovering, Paul Lung, Paul Nash, Paul Pelletier, Paul Poiret, Paul Rader, Paul Ranson, Paul Rudolph, Paul Rumsey, Paul Serusier, Paul Signac, Paul Souders, Paul Wonner, Paul-Cesar Helleu, Paulo Mendes da Rocha, Paulo Zerbato, Paulus Potter, Pawel Kuczynski, Pearlescent colors, Peder Balke, Pedro Friedeberg, Peeling texture, Pejac, Pennie Smith, Percy Tarrant, Peregrine Heathcote, Perspective drawing, Petah Coyne, Pete McKee, Pete Souza, Pete Turner, Peter Adderley, Peter Bagge, Peter Basch, Peter Beard, Peter Behrens, Peter Blake, Peter Brookes, Peter Callesen, Peter Coulson, Peter Dombrovskis, Peter Dundas, Peter Eisenman, Peter Elson, Peter Fischli and David Weiss, Peter Halley, Peter Henry Emerson, Peter Holme III, Peter Howson, Peter Hujar, Peter Ilsted, Peter Kemp, Peter Kuper, Peter Lik, Peter Lippmann, Peter Marino, Peter Max, Peter McKinnon, Peter Mendelsund, Peter Mitchev, Peter Mohrbacher, Peter Nottrott, Peter Robert Keil, Peter Sculthorpe, Peter Shire, Peter Turnley, Peter Voulkos, Peter Wileman, Petra Cortright, Petrina Hicks, Petros Afshar, Phase diagram, Phil Hester, Phil Jimenez, Phil Koch, Phil Noto, Phil Tippett, Philip Guston, Philip Jackson, Philip Jones Griffiths, Philip McKay, Philip Pearlstein, Philip Taaffe, Philip Toledano, Philip Treacy, Philip-Lorca diCorcia, Philipp Plein, Philippe Caza, Philippe Faraut, Philippe Geluck, Philippe Parreno, Philippe de Champaigne, Phlegm, Phoebe Anna Traquair, Phyllida Barlow, Phyllis Galembo, Pier Luigi Nervi, Pier Paolo Pasolini, Piero Fornasetti, Piero Manzoni, Pierre Adolphe Valette, Pierre Alechinsky, Pierre Bonnard, Pierre Huyghe, Pierre Jeanneret, Pierre Paulin, Pierre Pellegrini, Pierre Puvis de Chavannes, Pierre-Auguste Cot, Pierre-Jacques Pelletier, Piet Hein Eek, Piet Oudolf, Piet Parra, Pieter Aertsen, Pieter Bruegel the Elder, Pieter Claesz, Pieter Hugo, Pieter de Hooch, Pieter-Jansz van Asch, Pietro Antonio Rotari, Pietro Bernini, Pietro Perugino, Pietro da Cortona, Pinturicchio, Pipilotti Rist, Pitted texture, Pixar studio, Pixelscape, Plague doctor mask, Playful atmosphere, Point cloud, Pol Ledent, Polixeni Papapetrou, Pompeian fresco, Porous texture, Poul Henningsen, Poul Kjaerholm, Pouring rain, Preppy fashion, Primary colors, Prince of Persia, Prismatic texture, Product photography, Propaganda art, Prudence Heward, Prue Stent & Honey Long, Psychedelic colors, Puuung, Quino, R. K. Laxman, RHADS, RJ Matson, Rachel Bingaman, Rackstraw Downes, Radial composition, Radiographic image, Rafael Moneo, Rafael Vinoly, Rafal Olbinski, Raghu Rai, Ragnar Axelsson, Rainer Fetting, Rainer Werner Fassbinder, Rainy, Ralph Gibson, Ralph Horsley, Ralph Rucci, Rammellzee, Ran Ortner, Randal Spangler, Randall Munroe, Rangoli, Rankin, Rankin Bass, Raphael Lacoste, Raphael Perez, Raphaelle Peale, Raqib Shaw, Rashid Johnson, Raw texture, Ray Caesar, Ray Johnson, Raymond Briggs, Raymond Depardon, Raymond Loewy, Raymond Swanland, Rebeca Saray, Rebecca Sugar, Red Dead Redemption, Red Grooms, Reem Acra, Remedios Varo, Renato Guttuso, Rene Burri, Rene Lalique, Resident Evil, Resin art, Revok, Richard Anderson, Richard Anuszkiewicz, Richard Billingham, Richard Burlet, Richard Caldicott, Richard Dadd, Richard Deacon, Richard Diebenkorn, Richard Doyle, Richard Edward Miller, Richard Estes, Richard Eurich, Richard Hambleton, Richard Hamilton, Richard Lindner, Richard Lippold, Richard McGuire, Richard Mosse, Richard Neutra, Richard Parkes Bonington, Richard Phillips, Richard Pousette-Dart, Richard S. Johnson, Richard Schmid, Richard Tuschman, Richard Tuttle, Rick Amor, Rick Baker, Rick McKee, Rick Remender, Rick and Morty, Rie Cramer, Riitta Ikonen, Rimel Neffati, Rineke Dijkstra, Rinko Kawauchi, Rio Carnival float, Rippled texture, Risograph, Rita Angus, Rob Hefferan, Rob Rogers, Rob Ryan, Robert Adams, Robert Arneson, Robert Bateman, Robert Bechtle, Robert Bissell, Robert Campin, Robert Colescott, Robert Cottingham, Robert Doisneau, Robert Frank, Robert Gillmor, Robert Gober, Robert Hagan, Robert Henri, Robert Indiana, Robert Irwin, Robert Kirkman, Robert Longo, Robert Lyn Nelson, Robert Mangold, Robert McCall, Robert McGinnis, Robert Montgomery, Robert Morris, Robert Munsch, Robert Polidori, Robert Reid, Robert Ryman, Robert Smithson, Robert Swain Gifford, Robert Vonnoh, Robert Williams, Robert Wyland, Robert Zemeckis, Roberto Burle Marx, Roberto Cavalli, Roberto Matta, Roberto Rossellini, Robin Moline, Rockabilly fashion, Rockwell Kent, Roe Ethridge, Roeselien Raimond, Roger Bezombes, Roger Fenton, Roger Fry, Roger Hargreaves, Roger Hiorns, Roger Mayne, Roger Vivier, Rogier van der Weyden, Rolf Armstrong, Rolling Stone magazine, Roly-poly doll, Romain Trystram, Roman Vishniac, Roman mosaic, Romance movie, Romare Bearden, Romina Ressia, Romulo Royo, Ron Arad, Ron Clements, Ron Mueck, Ron Tandberg, Ron Walotsky, Ronald Searle, Ronald Wimberly, Ronan and Erwan Bouroullec, Rone, Roni Horn, Ronnie Landfield, Rosa Bonheur, Rosalba Carriera, Rosalyn Drexler, Rose Wylie, Rossdraws, Rosso Fiorentino, Rough surface, Rough texture, Roxy Paine, Roy DeCarava, Roz Chast, Ruben Ireland, Rudolf Ernst, Rudolf Olgiati, Rui Palha, Rupert Bunny, Rupert Vandervell, Ruslan Lobanov, Russ Heath, Russell Young, Rusty Lake, Ruth Asawa, Ruth Bernhard, Ruth Orkin, Ruth Sanderson, Ruud van Empel, Ryan Gander, Ryan Hewett, Ryan McGinley, Ryan McGinness, Ryan Ottley, Ryan Sook, Ryan Stegman, Ryo Takemasa, Sabine Weiss, Sachin Teng, Saint Seiya, Sakimichan, Sally Cruikshank, Salvatore Ferragamo, Sam Chivers, Sam Maloof, Sam Raimi, Sama Calligraphy, Samoan art, Sandra Boynton, Sandra Chevrier, Sandra Silberzweig, Sandro Botticelli, Sandy Welch, Saner, Sanford Biggers, Sanford Robinson Gifford, Sanne Sannes, Santiago Caruso, Santiago Rusinol, Sara Riches, Sarah Joncas, Sarah Lucas, Sardax, Satellite view, Satin texture, Satoru Takizawa, Saturated colors, Satyajit Ray, Saul Leiter, Saul Steinberg, Saul Tepper, Scandi chic fashion, Schematic diagram, Schematics, Sci-fi movie, Scott Adams, Scott Listfield, Scott McCloud, Scott Naismith, Scott Pilgrim, Scott Rohlfs, Scrapbooking, Sealab 2021, Sean Bagshaw, Sean Scully, Sean Yoro, Sebastian Errazuriz, Sebastiano del Piombo, Sebastiao Salgado, Sectional view, Seismogram, Selective Colouring, Sepia tones, Serene atmosphere, Serge Attukwei Clottey, Serge Lutens, Serge Marshennikov, Serge Mouille, Serge Najjar, Serge Poliakoff, Sergei Diaghilev, Sergio Larrain, Sergio Pininfarina, Sergio Toppi, Sesame Street, Seth Globepainter, Seven Samurai, Seydou Keita, Sgraffito, Shabby chic fashion, Shadowed lighting, Sheila Hicks, Sherree Valentine-Daines, Sherry Akrami, Shigeo Fukuda, Shin hanga, Shinichi Maruyama, Shintaro Kago, Shirin Neshat, Shirley Hughes, Shohei Otomo, Shomei Tomatsu, Shopkins Shoppies, Shozo Shimamoto, Shrek, Shuji Terayama, Shusei Nagaoka, Side view, Sidney Nolan, Sidney Sime, Sigmar Polke, Signe Wilkinson, Silhouette, Silvestro Lega, Simen Johan, Simeon Solomon, Simon Birch, Simon Bisley, Simon Kenny, Simon Palmer, Simon Schubert, Simon Vouet, Simone Rocha, Sion Sono, Slick texture, Slim Aarons, Slinkachu, Small scale, Smooth texture, Snowy, Sofonisba Anguissola, Soft lighting, Sohrab Hura, Sol LeWitt, Solve Sundsbo, Somber atmosphere, Sonia Boyce, Sonia Delaunay, Sonia Rykiel, Sonic the Hedgehog, Sopheap Pich, Sophie Delaporte, Sophie Gamand, Sophie Gengembre Anderson, Sophie Taeuber-Arp, Sou Fujimoto, Sound wave diagram, South Park, Space Invaders, Spartacus Chetwynd, Sparth, Spectral texture, Spectrogram, Speedy Graphito, Split view, SpongeBob SquarePants, Spotlight lighting, Spreadsheet, Spring, Spy movie, Squared paper, Squeak Carnwath, Stage lighting, Stan Berenstain, Stan Brakhage, Stan Winston, Stanhope Forbes, Stanko Abadzic, Stanley Donwood, Stanley Greene, Stanley Spencer, Star Trek, StarCraft, Starlit, Statistical graph, Stefan Gesell, Stefanie Schneider, Stencil graphics, Stephan Pastis, Stephanie Law, Stephen Darbishire, Stephen Hillenburg, Stephen Mackey, Stephen Ormandy, Stephen Quiller, Stephen Shortridge, Stephen Wilkes, Stephen Wiltshire, Stevan Dohanos, Steve Bell, Steve Cutts, Steve Dillon, Steve Epting, Steve Hanks, Steve Henderson, Steve Kelley, Steve McCurry, Steve McNiven, Steve Sack, Steven Harrington, Steven Meisel, Steven Outram, Sticker illustration, Stik, Stjepan Sejic, Storm Thorgerson, Stormy, Strandbeest, Strawberry Shortcake doll, Street Fighter, Streetview, Streetwear, Stuart Davis, Stuart Haygarth, Stuart Immonen, Stuart Lippincott, Studio Ghibli, Studio lighting, Su Blackwell, Subdued tones, Subodh Gupta, Sudarsan Pattnaik, Sue Reno, Sulamith Wulfing, Sumerian death mask, Summer, Sung Kim, Sunset, Super Mario, Superjail!, Susan Hiller, Susan Kare, Susan Rios, Susan Seddon Boulet, Suzanne Lalique, Suzanne Valadon, Suzuki Harunobu, Sven Pfrommer, Sverre Fehn, Sylvain Sarrailh, Sylvie Guillot, Symmetrical composition, Synchromism, TCG artwork, Tailored fashion, Takashi Homma, Takehiko Inoue, Tamagotchi, Tamas Dezso, Tammam Azzam, Tania Bruguera, Tara Donovan, Tara McPherson, Tarot de Marseille, Tatsunoko Production studio, Tatsuro Kiuchi, Taylor Wessing, Teagan White, Ted DeGrazia, Ted Gore, Ted McKeever, Ted Rall, Teddy Boys fashion, Teen Titans, Teenage Mutant Ninja Turtles, Teletubbies, Tense atmosphere, Terence Cuneo, Terence Donovan, Terry Isaac, Terry Oakes, Terry Redlin, Tetris, Teun Hocks, Tex Avery, Textured surface, The Adventures of Asterix, The Adventures of Tintin, The Elder Scrolls, The Flintstones, The Grand Budapest Hotel, The Lord of the Rings, The Matrix, The New Yorker magazine, The Secret of Kells, The Simpsons, The Sims, The Sopranos, The Tatami Galaxy, The White Stripes, The Witcher, The X-Files, Theaster Gates, Theo Prins, Theo van Doesburg, Theo van Rysselberghe, Theodor Kittelsen, Theodore Chasseriau, Theodore Gericault, Theodore Robinson, Theodore Rousseau, Theophile Steinlen, Thiago Valdi, Thierry Duval, Thom Browne, Thom Filicia, Thom Mayne, Thomas Ascott, Thomas Barbey, Thomas Benjamin Kennington, Thomas Birch, Thomas Blackshear, Thomas Chippendale, Thomas Cole, Thomas Daniell, Thomas Dewing, Thomas Dodd, Thomas Eakins, Thomas Edwin Mostyn, Thomas Gainsborough, Thomas Hart Benton, Thomas Heatherwick, Thomas Hirschhorn, Thomas Kinkade, Thomas Lawrence, Thomas Leuthard, Thomas Moran, Thomas Nast, Thomas Rowlandson, Thomas Ruff, Thomas Saliot, Thomas Schaller, Thomas Struth, Thomas Sully, Thomas Theodor Heine, Thomas Wrede, Thornton Utz, Thota Vaikuntam, ThunderCats, Thunderstorm, Thurston Hopkins, Tiago Hoisel, Tiki mask, Tillie Walden, Tilted view, Tim Biskup, Tim Bradstreet, Tim Doyle, Tim Etchells, Tim Hawkinson, Tim Hildebrandt, Tim Holtz, Tim Noble, Tim Okamura, Tim Shumate, Time magazine, Time-lapse photography, Timothy Easton, Tina Barney, Tina Modotti, Tintoretto, Tithi Luadthong, Titian, Tlingit art, Tlingit mask, Tobia Scarpa, Tod Browning, Todd Hido, Todd James, Todd McLellan, Todd Nauck, Todd White, Toei Animation studio, Tokujin Yoshioka, Tokusatsu, Tokyo Ghoul, Tom Bagshaw, Tom Eckersley, Tom Everhart, Tom Fruin, Tom Gauld, Tom Grummett, Tom Killion, Tom Lovell, Tom Otterness, Tom Sachs, Tom Thomson, Tom Toles, Tom Tomorrow, Tom Wesselmann, Tom Whalen, Tom and Jerry, Tom of Finland, Tomas Saraceno, Tomasz Alen Kopera, Tomer Hanuka, Tomi Ungerer, Tomie dePaola, Tomma Abts, Tomokazu Matsuyama, Tomoo Gokita, Ton Dubbeldam, Tony Allain, Tony Conrad, Tony DeZuniga, Tony Fitzpatrick, Tony Moore, Tony Orrico, Top view, Topology map, Torito mask, Tory Burch, Toshio Shibata, Toyin Ojih Odutola, Tracey Adams, Tracy Miller, Tracy Reese, Tracy Scott, Traditional Scroll Painting, Tranquil atmosphere, Translucent colors, Trash Polka, Treemap, Trent Parke, Trevor Paglen, Trey Ratcliff, Trisha Romance, Tsukioka Yoshitoshi, Tsumori Chisato, Twin Peaks, Tyler Edlin, UV colors, Ugo Rondinone, Ukrainian folk art, Umberto Boccioni, Underwater lighting, Utamaro, VS Gaitonde, Vacheron Constantin, Val Byrne, Valentine Cameron Prinsep, Valentino, Valerio Adami, Vamberk lace, Vania Zouravliov, Vector chip line art, Vector field, Veined texture, Venetian carnival mask, Vera Wang, Vermiculation texture, Versus, Vibrant colors, Vicki Boutin, Victor Brauner, Victor Moscoso, Victoria Frances, Victorian era cabinet card, Vienna Secession, Vilhelm Hammershoi, Vince Low, Vincent Castiglia, Vincent Di Fate, Vincent Giarrano, Vincent Laforet, Vincent Munier, Vincent Peters, Vinoodh Matadin, Virgil Finlay, Vittore Carpaccio, Viviane Sassen, Vivienne Tam, Vladimir Tretchikoff, Vladimir Volegov, Vogue magazine, Voronoi diagram, Voynich manuscript, WLOP, WRDSMTH, Wabi-sabi, Walasse Ting, Walt Disney, Walt Handelsman, Walter Langley, Walter Lantz, Walter Molino, Walter Quirt, Wang Guangyi, Warcraft, Warhammer 40,000, Warm colors, Warner Bros, Warren Ellis, Warren Keelan, Warwick Goble, Weather map, Weathered surface, Wendell Castle, Wendy Froud, Wendy Vecchi, Werner Bischof, Wes Craven, Wes Wilson, Westworld, Wet texture, Wharton Esherick, What We Do In The Shadows, Where is Waldo, Whimsical atmosphere, Wide-angle view, Wildlife Art, Wilfredo Lam, Will Bullas, Will Burrard-Lucas, Willem Claesz. Heda, Willem van der Velde, William Blake, William Clarke Wontner, William H. Johnson, William Harnett, William Heath Robinson, William Henry Fox Talbot, William Holman Hunt, William Kay Blacklock, William Merritt Chase, William Mortensen, William Patino, William Powell Frith, William Stanley Haseltine, William Steig, William Tillyer, William Wendt, William de Morgan, William-Aldophe Bouguereau, Willy Ronis, Wim Delvoye, Winfred Rembert, Winifred Nicholson, Winnie-the-Pooh, Winslow Homer, Winter, Wireframe, Wiring diagrams, Wolf Kahn, Wolfenstein 3D, Wolfgang Laib, Woody, Wotto, Woven texture, Wyndham Lewis, Xenia Hausner, Xkcd, Yanjun Cheng, Yann Arthus-Bertrand, Yasuo Kuniyoshi, Yener Torun, Yiannis Moralis, Yinka Ilori, Yonatan Frimer, Yossi Kotler, Yousuf Karsh, Yu-Gi-Oh, Yuri Shwedoff, Yuriy Shevchuk, Yusaku Kamekura, Yves Brayer, Yves Tanguy, Zak Noyle, Zandra Rhodes, Zao Wou-Ki, Zapatista mask, Zapiro, Zaria Forman, Zarina Hashmi, Zemer Peled, Zentangle, Zhang Daqian, Zimoun, Zio Ziegler".Split(',', StringSplitOptions.None).ToList(); + return artists.OrderBy(el => random.Next()).ToList().First().Trim(); + } + + public Task DoTransformation(PromptDetails pd) + { + var artist = GetRandomArtist(); + var newVersion = $"In the style of {artist}, {pd.Prompt}"; + pd.ReplacePrompt(newVersion, newVersion, TransformationType.AddingArtistStyle); + + return Task.FromResult(true); + } + } +} diff --git a/MultiImageClient/promptTransformation/TextFormatting.cs b/MultiImageClient/promptTransformation/TextFormatting.cs new file mode 100644 index 0000000..e41a49a --- /dev/null +++ b/MultiImageClient/promptTransformation/TextFormatting.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using IdeogramAPIClient; +using System.Text.RegularExpressions; +using SixLabors.ImageSharp.Processing; +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using System.Drawing; +using RectangleF = SixLabors.ImageSharp.RectangleF; +using Color = SixLabors.ImageSharp.Color; +using PointF = SixLabors.ImageSharp.PointF; +using SystemFonts = SixLabors.Fonts.SystemFonts; +using Rectangle = SixLabors.ImageSharp.Rectangle; +using Point = SixLabors.ImageSharp.Point; +using Image = SixLabors.ImageSharp.Image; +using FontStyle = SixLabors.Fonts.FontStyle; + + +namespace MultiImageClient +{ + public static class TextFormatting + { + private static readonly float KEY_WIDTH_PROPORTION = 0.15f; + private static readonly float VALUE_WIDTH_PROPORTION = 1f - KEY_WIDTH_PROPORTION; + + private static void DrawKeyValuePair(IImageProcessingContext ctx, string key, string value, float fontSize, float x, float keyWidth, float valueWidth, ref float y) + { + // just trunc. ugh. + value = value.Length > 2500 ? value[..2500] + "..." : value; + + var font = SystemFonts.CreateFont("Arial", fontSize, FontStyle.Regular); + const float PADDING = 4; + + // Measure text height with wrapping + var keyOptions = new RichTextOptions(font) + { + WrappingLength = keyWidth - (2 * PADDING), + LineSpacing = 1.2f + }; + + var valueOptions = new RichTextOptions(font) + { + WrappingLength = valueWidth - (2 * PADDING), + LineSpacing = 1.2f + }; + + // Using the correct TextMeasurer method from SixLabors.Fonts + var keyBounds = TextMeasurer.MeasureBounds(key, keyOptions); + var valueBounds = TextMeasurer.MeasureBounds(value, valueOptions); + var rowHeight = Math.Max(keyBounds.Height, valueBounds.Height) + (2 * PADDING); + + // Draw the background boxes based on measured height + var keyRect = new RectangleF(x, y, keyWidth, rowHeight); + var valueRect = new RectangleF(x + keyWidth, y, valueWidth, rowHeight); + + ctx.Fill(Color.Black, keyRect) + .Fill(Color.White, valueRect); + + // Update options with final positions + keyOptions.Origin = new PointF(x + PADDING, y + PADDING); + valueOptions.Origin = new PointF(x + keyWidth + PADDING, y + PADDING); + + // Draw the text + ctx.DrawText(keyOptions, key, Color.White); + ctx.DrawText(valueOptions, value, Color.Black); + + y += rowHeight + PADDING; + } + + public static async Task JustAddSimpleTextToBottomAsync(byte[] imageBytes, IEnumerable historySteps, Dictionary imageInfo, string outputPath, SaveType saveType) + { + Console.WriteLine(outputPath); + using var originalImage = Image.Load(imageBytes); + var imageWidth = originalImage.Width; + var intendedFontSize = 24; + + var theText = historySteps.First().Prompt ?? "failed to get text"; + SixLabors.Fonts.FontFamily fontFamily; + if (!SystemFonts.TryGet("Segoe UI", out fontFamily)) + { + if (!SystemFonts.TryGet("Arial", out fontFamily)) + { + fontFamily = SystemFonts.Families.First(); // Fallback + } + } + var testFont = fontFamily.CreateFont(intendedFontSize, FontStyle.Regular); + + var horizontalPadding = 10; + var verticalPadding = 10; + var availableXPixels = imageWidth - (horizontalPadding * 2); + + //we can measure font length with: + var textOptions = new RichTextOptions(testFont) + { + WrappingLength = availableXPixels, + LineSpacing = 1.15f, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + Dpi = UIConstants.TextDpi, + FallbackFontFamilies = new[] { SystemFonts.Families.First() } + }; + + // Using the correct TextMeasurer method from SixLabors.Fonts + var keyBounds = TextMeasurer.MeasureBounds(theText, textOptions); + + int textHeight = (int)Math.Ceiling(keyBounds.Height); + int yPixelsToAdd = textHeight + (verticalPadding * 2); + int newHeight = originalImage.Height + yPixelsToAdd; + + using var annotatedImage = new Image(originalImage.Width, newHeight); + + annotatedImage.Mutate(ctx => + { + // antialiasing. + ctx.SetGraphicsOptions(new GraphicsOptions + { + Antialias = true, + AntialiasSubpixelDepth = 16 + }); + + // Draw original image + ctx.DrawImage(originalImage, new Point(0, 0), 1f); + + // Set up the annotation area at the bottom + ctx.Fill(Color.Black, new Rectangle(0, originalImage.Height, originalImage.Width, yPixelsToAdd)); + + // Position text with proper padding + textOptions.Origin = new PointF(horizontalPadding, originalImage.Height + verticalPadding); + + ctx.DrawText(textOptions, theText, Color.White); + + + var meta = imageInfo["Producer"] ?? "no gen"; + // Create smaller font for metadata + var metaFont = fontFamily.CreateFont(intendedFontSize * 0.75f, FontStyle.Regular); + var metaOptions = new RichTextOptions(metaFont) + { + HorizontalAlignment = HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Bottom, + Dpi = UIConstants.TextDpi, + Origin = new PointF(imageWidth - horizontalPadding, newHeight - verticalPadding) + }; + + ctx.DrawText(metaOptions, meta, new Color(new Rgba32(200, 200, 200))); + }); + + await annotatedImage.SaveAsPngAsync(outputPath); + } + + public static async Task SaveImageAndAnnotate(byte[] imageBytes, IEnumerable historySteps, Dictionary imageInfo, string outputPath, SaveType saveType) + { + if (imageBytes == null || imageBytes.Length == 0) + { + throw new ArgumentException("Image bytes cannot be null or empty"); + } + + try + { + Image originalImage; + try + { + originalImage = Image.Load(imageBytes); + } + catch (Exception ex2) + { + Logger.Log($"{ex2} failed normal load"); + try + { + originalImage = Image.Load(imageBytes); + } + catch (Exception ex3) + { + Logger.Log($"{ex3} failed rgba32 load"); + return; + } + } + + // Define dimensions + int annotationWidth = 850; + float fontSize = 15; + + // Create new image with space for annotations + using var annotatedImage = new Image(originalImage.Width + annotationWidth, originalImage.Height); + + annotatedImage.Mutate(ctx => + { + // Draw original image + ctx.DrawImage(originalImage, new Point(0, 0), 1f); + + // Setup annotation area + float leftMargin = originalImage.Width + 5; + float rightMargin = annotatedImage.Width - 5; + float totalWidth = rightMargin - leftMargin; + float keyWidth = totalWidth * KEY_WIDTH_PROPORTION; + float valueWidth = totalWidth * VALUE_WIDTH_PROPORTION; + + // Fill right side with black background + ctx.Fill(Color.Black, new Rectangle(originalImage.Width, 0, annotationWidth, originalImage.Height)); + + // Draw history steps + float y = 5; + foreach (var historyStep in historySteps) + { + DrawKeyValuePair(ctx, historyStep.TransformationType.ToString(), historyStep.Explanation, fontSize, leftMargin, keyWidth, valueWidth, ref y); + y += 2; // Small gap between entries + } + + // Draw image info if available + if (imageInfo.Any()) + { + foreach (var kvp in imageInfo) + { + DrawKeyValuePair(ctx, kvp.Key, kvp.Value, fontSize, leftMargin, keyWidth, valueWidth, ref y); + } + } + }); + + await annotatedImage.SaveAsPngAsync(outputPath); + } + catch (Exception ex) + { + Logger.Log($"Exception adding fuller text to image: {ex}"); + } + } + } +} diff --git a/MultiImageClient/promptTransformation/TextUtils.cs b/MultiImageClient/promptTransformation/TextUtils.cs new file mode 100644 index 0000000..f2b6a5a --- /dev/null +++ b/MultiImageClient/promptTransformation/TextUtils.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Drawing.Imaging; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Reflection; +using System.Timers; +using System.Text; +using System.Windows.Forms; + +using IdeogramAPIClient; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using CommandLine; + + +namespace MultiImageClient +{ + public static class TextUtils + { + private static readonly object _lockObject = new object(); + private static readonly Dictionary _filenameCounts = new Dictionary(); + private static readonly float KEY_WIDTH_PROPORTION = 0.15f; + private static readonly float VALUE_WIDTH_PROPORTION = 1f - KEY_WIDTH_PROPORTION; + public static List bads = new List() { };// "[", "]", "(", ")", "{", "}","\\","\"","\'", ":", }; + //don't clean these here; if someone needs the prompt to be clean, then just do that yourself later. it's not the prompts problem. + + public static string CleanPrompt(string prompt) + { + foreach (var bad in bads) + { + prompt = prompt.Replace(bad, ""); + } + return prompt.Trim(); + } + + + } +} diff --git a/MultiImageClient/prompts.txt b/MultiImageClient/prompts.txt index bdfcbaa..aeda287 100644 --- a/MultiImageClient/prompts.txt +++ b/MultiImageClient/prompts.txt @@ -1,8 +1,7 @@ - -The streets of Singapore are bustling with people and various street food stalls, creating a lively and multicultural atmosphere. The area is full of guava, cheese, broccoli, kimchi, hardboiled eggs, vinegar, and durian. Each smell seems to waft through the crowd attacking people and animals who are particularly weak to it, striking them with brutal overwhelming negative sensations. This is a biological weapon attack! the image is a schema overhead view with detailed performance analysis -A light switch that is clearly in the "ON" not "OFF" position. Please describe this close-up 3d depth image in high detail exactly describing the light switch panel and its material and color, the switch which sticks out, its position and what the position represents. -A 4x4 grid of super magiscule, block font dense intense incredibly meaningful, profound, ethereal, and subtle KANJI characters. The characters are illustrated in super high resolution, partially 3d style, feeling like they almost emerge from the flat screen, in a super clear image utilizing one or more of: subtle coloration, unusual line thicknesses or variations, unusual kerning, pen and ink, hand-drawn custom artisinal creative characters, and/or super evocative, personalized textures. -Introducing the "Rogue" card, an exciting new addition between the Queen and Jack of Diamonds in a standard playing card deck. The Rogue card features a dashing figure with a masked face, holding a small, curved dagger. The figure is adorned with a cape and a diamond-encrusted hat, symbolizing the connection to the Diamonds suit. The single-letter symbol for the Rogue card is "X", which is displayed in each corner along with the suit of Diamonds. The card's design maintains the simplicity of traditional playing cards, with the "X" and diamonds in the corners and a captivating, yet minimalist, pattern in the middle, featuring a subtle diamond-like motif. The Rogue card captures the essence of cunning and deception, adding an exciting twist to the classic deck. -🥕 attacking 🐢 with 🦊 -Here's a 30x30 ASCII art map of the oasis, focusing on your desired features and incorporating creative engineering solutions:\r\n\r\n+------------------------------+\r\n|🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🐟🐟🐟🐟🌊🌊🌊🌊🌊🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌊🌊\U0001f986\U0001f986\U0001f986🐟🐟🐟🐟\U0001f986\U0001f986\U0001f986🌊🌊🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌿🌿\U0001f986\U0001f986\U0001f986🌿🌿🌿🌿\U0001f986\U0001f986\U0001f986🌿🌿🌊🌊🌊|\r\n|🌊🌊🌊🌊🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌊🌊🌊|\r\n|🌊🌊🌊🌊🌿🐸🐸🐸🐸🐸🌿🌿🌿🌿🐸🐸🐸🐸🐸🌿🌊🌊|\r\n|🌊🌊🌿🌿🌿🐸🐸🐸🐸🐸🌿🌿🌿🌿🐸🐸🐸🐸🐸🌿🌿🌊|\r\n|🌊🌊🌿🌴🌴🌴🌴🌴🌴🌴🌴🐦🐦🐦🌴🌴🌴🌴🌴🌴🌿🌊|\r\n|🌊🌿🌿🌴🌴🌴🌴🌴🌴🌴🌴🐦🐦🐦🌴🌴🌴🌴🌴🌴🌿🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🐿️🐿️🐿️🐿️\U0001f985\U0001f985\U0001f985\U0001f985\U0001f985\U0001f985🐿️🐿️🐿️🐿️🌾🌾🌿|\r\n|🌊🌿🌾🌾🐿️🐿️🐿️🐿️\U0001f985\U0001f985\U0001f985\U0001f985\U0001f985\U0001f985🐿️🐿️🐿️🐿️🌾🌾🌿|\r\n|🌊🌿🌾🌾🐿️🐿️🐿️🐿️🍂🍂🍂🍂🍂🍂🐿️🐿️🐿️🐿️🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂🐕🐈🐕🐈🍂🍂🍂🍂🌾🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂🐕🐈🐕🐈🍂🍂🍂🍂🌾🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🌾🌾🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🌾🌾🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂\U0001f98c\U0001f98c\U0001f98c\U0001f98c🍂🍂🍂🌾🌾🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂\U0001f98c\U0001f98c\U0001f98c\U0001f98c🍂🍂🍂🌾🌾🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🌾🌾🌾🌾🌾🌿|\r\n|🌊🌿🌿🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌿🌿|\r\n|🌊🌊🌿🌿🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌿🌿🌊|\r\n|🌊🌊🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌊🌊|\r\n|🌊🌊🌊🌊🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊|\r\n+------------------------------+\r\n\r\nLegend:\r\n🌊 = Water (lake and wetland areas)\r\n🐟 = Fish (attracted by the oasis)\r\n\U0001f986 = Ducks (attracted by the water and aquatic plants)\r\n🌿 = Reeds and aquatic plants (natural filtration system)\r\n🐸 = Frogs (indicator of ecosystem health)\r\n🌴 = Solar-powered robotic trees (provide shade, dispense bird seed, and monitor bird populations)\r\n🐦 = Various bird species (attracted by the oasis and bird feeders)\r\n🌾 = Tall grass (natural habitat for small animals)\r\n🍂 = Sensor-equipped deciduous trees (monitor environmental conditions and wildlife activity)\r\n🐿️ = Squirrels (attracted by the bird seed and trees)\r\n\U0001f985 = Eagles (attracted by the abundant prey and nesting opportunities)\r\n🐕🐈 = Visitor area with pet-friendly amenities\r\n\U0001f98c = Deer (attracted by the lush vegetation and water sources)\r\n\r\nEngineering features:\r\n\r\n Solar-powered robotic trees that provide shade, dispense bird seed, and monitor bird populations using built-in cameras and sensors\r\n Sensor-equipped deciduous trees that monitor environmental conditions (temperature, humidity, air quality) and wildlife activity\r\n A natural filtration system using reeds and aquatic plants to maintain water quality in the lake and wetland areas\r\n Strategically placed bird feeders that dispense a variety of seeds to attract a diverse range of bird species while minimizing access for rats and pests\r\n An intelligent pest management system that uses ultrasonic devices and targeted scent deterrents to keep rats and other unwanted animals away from the oasis\r\n A network of hidden cameras and microphones throughout the oasis to monitor animal behavior, track population dynamics, and gather data for research purposes\r\n An eco-friendly visitor area with pet-friendly amenities, educational displays, and interactive exhibits showcasing the oasis's unique features and wildlife -A 4x4 grid of super magiscule, block font dense intense incredibly meaningful, profound, ethereal, and subtle KANJI characters. The characters are illustrated in super high resolution, partially 3d style, feeling like they almost emerge from the flat screen, in a super clear image utilizing one or more of: subtle coloration, unusual line thicknesses or variations, unusual kerning, pen and ink, hand-drawn custom artisinal creative characters, and/or super evocative, personalized textures. \ No newline at end of file + +The streets of Singapore are bustling with people and various street food stalls, creating a lively and multicultural atmosphere. The area is full of guava, cheese, broccoli, kimchi, hardboiled eggs, vinegar, and durian. Each smell seems to waft through the crowd attacking people and animals who are particularly weak to it, striking them with brutal overwhelming negative sensations. the image is a schema overhead view with detailed performance analysis +A light switch that is clearly in the "ON" not "OFF" position. Please describe this close-up 3d depth image in high detail exactly describing the light switch panel and its material and color, the switch which sticks out, its position and what the position represents. +A 4x4 grid of super magiscule, block font dense intense incredibly meaningful, profound, ethereal, and subtle KANJI characters. The characters are illustrated in super high resolution, partially 3d style, feeling like they almost emerge from the flat screen, in a super clear image utilizing one or more of: subtle coloration, unusual line thicknesses or variations, unusual kerning, pen and ink, hand-drawn custom artisinal creative characters, and/or super evocative, personalized textures. +Introducing the "Rogue" card, an exciting new addition between the Queen and Jack of Diamonds in a standard playing card deck. The Rogue card features a dashing figure with a masked face, holding a small, curved dagger. The figure is adorned with a cape and a diamond-encrusted hat, symbolizing the connection to the Diamonds suit. The single-letter symbol for the Rogue card is "X", which is displayed in each corner along with the suit of Diamonds. The card's design maintains the simplicity of traditional playing cards, with the "X" and diamonds in the corners and a captivating, yet minimalist, pattern in the middle, featuring a subtle diamond-like motif. The Rogue card captures the essence of cunning and deception, adding an exciting twist to the classic deck. +🥕 attacking 🐢 with 🦊 +Here's a 30x30 ASCII art map of the oasis, focusing on your desired features and incorporating creative engineering solutions:\r\n\r\n+------------------------------+\r\n|🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🐟🐟🐟🐟🌊🌊🌊🌊🌊🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌊🌊\U0001f986\U0001f986\U0001f986🐟🐟🐟🐟\U0001f986\U0001f986\U0001f986🌊🌊🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌿🌿\U0001f986\U0001f986\U0001f986🌿🌿🌿🌿\U0001f986\U0001f986\U0001f986🌿🌿🌊🌊🌊|\r\n|🌊🌊🌊🌊🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌊🌊🌊|\r\n|🌊🌊🌊🌊🌿🐸🐸🐸🐸🐸🌿🌿🌿🌿🐸🐸🐸🐸🐸🌿🌊🌊|\r\n|🌊🌊🌿🌿🌿🐸🐸🐸🐸🐸🌿🌿🌿🌿🐸🐸🐸🐸🐸🌿🌿🌊|\r\n|🌊🌊🌿🌴🌴🌴🌴🌴🌴🌴🌴🐦🐦🐦🌴🌴🌴🌴🌴🌴🌿🌊|\r\n|🌊🌿🌿🌴🌴🌴🌴🌴🌴🌴🌴🐦🐦🐦🌴🌴🌴🌴🌴🌴🌿🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🐿️🐿️🐿️🐿️\U0001f985\U0001f985\U0001f985\U0001f985\U0001f985\U0001f985🐿️🐿️🐿️🐿️🌾🌾🌿|\r\n|🌊🌿🌾🌾🐿️🐿️🐿️🐿️\U0001f985\U0001f985\U0001f985\U0001f985\U0001f985\U0001f985🐿️🐿️🐿️🐿️🌾🌾🌿|\r\n|🌊🌿🌾🌾🐿️🐿️🐿️🐿️🍂🍂🍂🍂🍂🍂🐿️🐿️🐿️🐿️🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂🐕🐈🐕🐈🍂🍂🍂🍂🌾🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂🐕🐈🐕🐈🍂🍂🍂🍂🌾🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🌾🌾🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🌾🌾🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂\U0001f98c\U0001f98c\U0001f98c\U0001f98c🍂🍂🍂🌾🌾🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂\U0001f98c\U0001f98c\U0001f98c\U0001f98c🍂🍂🍂🌾🌾🌾🌾🌾🌿|\r\n|🌊🌿🌾🌾🌾🌾🍂🍂🍂🍂🍂🍂🍂🍂🍂🍂🌾🌾🌾🌾🌾🌿|\r\n|🌊🌿🌿🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌿🌿|\r\n|🌊🌊🌿🌿🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌿🌿🌊|\r\n|🌊🌊🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌊🌊|\r\n|🌊🌊🌊🌊🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌊🌊🌊|\r\n|🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊|\r\n+------------------------------+\r\n\r\nLegend:\r\n🌊 = Water (lake and wetland areas)\r\n🐟 = Fish (attracted by the oasis)\r\n\U0001f986 = Ducks (attracted by the water and aquatic plants)\r\n🌿 = Reeds and aquatic plants (natural filtration system)\r\n🐸 = Frogs (indicator of ecosystem health)\r\n🌴 = Solar-powered robotic trees (provide shade, dispense bird seed, and monitor bird populations)\r\n🐦 = Various bird species (attracted by the oasis and bird feeders)\r\n🌾 = Tall grass (natural habitat for small animals)\r\n🍂 = Sensor-equipped deciduous trees (monitor environmental conditions and wildlife activity)\r\n🐿️ = Squirrels (attracted by the bird seed and trees)\r\n\U0001f985 = Eagles (attracted by the abundant prey and nesting opportunities)\r\n🐕🐈 = Visitor area with pet-friendly amenities\r\n\U0001f98c = Deer (attracted by the lush vegetation and water sources)\r\n\r\nEngineering features:\r\n\r\n Solar-powered robotic trees that provide shade, dispense bird seed, and monitor bird populations using built-in cameras and sensors\r\n Sensor-equipped deciduous trees that monitor environmental conditions (temperature, humidity, air quality) and wildlife activity\r\n A natural filtration system using reeds and aquatic plants to maintain water quality in the lake and wetland areas\r\n Strategically placed bird feeders that dispense a variety of seeds to attract a diverse range of bird species while minimizing access for rats and pests\r\n An intelligent pest management system that uses ultrasonic devices and targeted scent deterrents to keep rats and other unwanted animals away from the oasis\r\n A network of hidden cameras and microphones throughout the oasis to monitor animal behavior, track population dynamics, and gather data for research purposes\r\n An eco-friendly visitor area with pet-friendly amenities, educational displays, and interactive exhibits showcasing the oasis's unique features and wildlife \ No newline at end of file diff --git a/MultiImageClient/settings - Fill this in and rename it.json b/MultiImageClient/settings - Fill this in and rename it.json index acdd453..b7d2b74 100644 --- a/MultiImageClient/settings - Fill this in and rename it.json +++ b/MultiImageClient/settings - Fill this in and rename it.json @@ -1,13 +1,76 @@ -{ - "LoadPromptsFrom": "D:\\proj\\IdeogramAPI\\prompts.txt", - "IdeogramApiKey": "FILL IN YOUR IDEOGRAM API KEY HERE from https://ideogram.ai/manage-api which you have to pay for", - "OpenAIApiKey": "Optional: Fill in the OpenAI key here: from https://platform.openai.com/api-keys which you have to pay for", - "BFLApiKey": "Optional: Fill in the BFL key here: from https://beta.bigfriendly.com/ which you have to pay for", - "AnthropicApiKey": "Optionally: fill in the anthropic key here. from https://console.anthropic.com/settings/keys which you have to pay for", +{ + "PromptFiles": [ + "REQUIRED: absolute path to a prompts .txt file (one prompt per line). You can list more than one file.", + "Example second file - or remove this array entry." + ], + "LoadPromptsFrom": "", + "IdeogramApiKey": "Optional: Ideogram API key from https://ideogram.ai/manage-api (paid). Required for Ideogram v2/v2a/v3 generators.", + "OpenAIApiKey": "Optional: OpenAI key from https://platform.openai.com/api-keys (paid). Required for DALL-E 3 and GPT Image 1 / GPT Image 1 Mini generators.", + "BFLApiKey": "Optional: Black Forest Labs (Flux) key from https://api.bfl.ai/ (paid). Required for BFL v1.1 and BFL v1.1 Ultra generators.", + "AnthropicApiKey": "Optional: Anthropic key from https://console.anthropic.com/settings/keys (paid). Required for Claude-based prompt rewriting steps.", + "RecraftApiKey": "Optional: Recraft API key from https://www.recraft.ai/ (paid). Required for Recraft generators.", + "GoogleGeminiApiKey": "Optional: Google Gemini (AI Studio) key from https://ai.google.dev/gemini-api/docs/api-key. Used by the Gemini image (NanoBanana) generator and as an AI Studio fallback for Imagen.", + "GoogleCloudApiKey": "Optional: Google Cloud / Vertex AI API key (alternative to service-account auth).", + "GoogleCloudLocation": "REQUIRED for Imagen 4: Google Cloud region, e.g. us-central1. See https://cloud.google.com/vertex-ai/docs/general/locations", + "GoogleCloudProjectId": "REQUIRED for Imagen 4: Google Cloud project id from https://console.cloud.google.com/", + "GoogleServiceAccountKeyPath": "REQUIRED for Imagen 4: absolute path to a service-account JSON key file (IAM & Admin > Service Accounts > Keys).", "ImageDownloadBaseFolder": "D:\\proj\\IdeogramAPI\\ideogramSaves", "LogFilePath": "D:\\proj\\IdeogramAPI\\ideogram.log", "SaveJsonLog": true, - "SaveRawImage": true, "EnableLogging": true, - "AnnotationSide": "right" -} \ No newline at end of file + "AnnotationSide": "right", + "FlatImageMirrorPath": "", + "TypedPromptsAppendFile": "", + "LocalFlux2ComfyEndpoint": "http://127.0.0.1:8188", + "LocalFlux2AllowRemoteEndpoint": false, + "LocalFlux2WorkflowPath": "Optional: absolute path to a ComfyUI API-format workflow JSON for local Flux2 Klein + uncensored text encoder.", + "LocalFlux2PositivePromptNodeId": "Optional: ComfyUI node id whose text input receives the prompt. Leave blank to auto-detect.", + "LocalFlux2NegativePromptNodeId": "Optional: ComfyUI node id whose text input receives LocalFlux2NegativePrompt.", + "LocalFlux2NegativePrompt": "", + "LocalFlux2ModelName": "Optional: model filename to patch into loader nodes, e.g. flux2-klein-4b.gguf.", + "LocalFlux2TextEncoderName": "Optional: ablated text encoder filename to patch into text encoder loader nodes, e.g. qwen3-4b-abl-q4_0.gguf.", + "LocalFlux2VaeName": "Optional: VAE filename to patch into VAELoader nodes.", + "LocalFlux2Width": 1024, + "LocalFlux2Height": 1024, + "LocalFlux2Steps": 28, + "LocalFlux2Guidance": 1.0, + "LocalFlux2Seed": null, + "LocalFlux2TimeoutSeconds": 900, + "LocalFlux2WorkflowInputOverrides": {}, + "ByteDanceArkApiKey": "Optional: BytePlus/ModelArk API key. Required for direct Seedream.", + "ByteDanceArkBaseUrl": "https://ark.ap-southeast.bytepluses.com/api/v3", + "SeedreamModel": "seedream-4-5-251128", + "SeedreamSize": "2K", + "SeedreamWatermark": false, + "MiniMaxApiKey": "Optional: MiniMax API key. Required for Hailuo image-01.", + "HailuoModel": "image-01", + "HailuoAspectRatio": "1:1", + "HailuoResponseFormat": "base64", + "KreaApiKey": "Optional: Krea API token. Required for Krea 2.", + "KreaModelVariant": "medium", + "KreaAspectRatio": "1:1", + "KreaResolution": "1K", + "KreaCreativity": "medium", + "BriaApiKey": "Optional: BRIA API token. Required for BRIA FIBO.", + "BriaBaseUrl": "https://engine.prod.bria-api.com/v2", + "BriaAspectRatio": "1:1", + "BriaResolution": "1MP", + "BriaNumResults": 1, + "MagnificApiKey": "Optional: Magnific API key. Required for Mystic.", + "MagnificAspectRatio": "square_1_1", + "MagnificModel": "realism", + "MagnificResolution": "1K", + "LumaApiKey": "Optional: Luma Dream Machine API key. Required for Photon.", + "LumaPhotonModel": "photon-1", + "LumaAspectRatio": "1:1", + "RunwayApiKey": "Optional: Runway API key. Required for Gen-4 Image.", + "RunwayApiVersion": "2024-11-06", + "RunwayImageModel": "gen4_image", + "RunwayImageRatio": "1024:1024", + "StabilityApiKey": "Optional: Stability AI API key. Required for SD3.5.", + "StabilityModel": "sd3.5-large", + "StabilityAspectRatio": "1:1", + "StabilityOutputFormat": "png", + "StabilityNegativePrompt": "", + "DirectImageApiTimeoutSeconds": 600 +} diff --git a/RecraftAPI/RecraftAPIClient.csproj b/RecraftAPI/RecraftAPIClient.csproj new file mode 100644 index 0000000..42a609b --- /dev/null +++ b/RecraftAPI/RecraftAPIClient.csproj @@ -0,0 +1,27 @@ + + + + Library + net9.0 + 13.0 + enable + enable + {5A29AA17-8556-4456-8FFC-E8E2AF015537} + AnyCPU;ARM64 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/RecraftAPI/RecraftClient.cs b/RecraftAPI/RecraftClient.cs new file mode 100644 index 0000000..b04a144 --- /dev/null +++ b/RecraftAPI/RecraftClient.cs @@ -0,0 +1,183 @@ +using Newtonsoft.Json; +using System.Text; + +namespace RecraftAPIClient +{ + public class RecraftClient + { + private readonly HttpClient _httpClient; + private readonly string _apiKey; + private const string _baseUrl = "https://external.api.recraft.ai/v1"; + + public RecraftClient(string apiKey) + { + _apiKey = apiKey; + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); + } + + + public async Task GenerateImageAsync(string prompt, string artistic_level, string substyle, string style, RecraftImageSize size, RecraftModel model = RecraftModel.recraftv3) + { + var stringSubstyle = ""; + string serialized = ""; + var modelString = model.ToString(); + + if (style == "any") { + serialized = JsonConvert.SerializeObject(new + { + prompt, + model = modelString, + style = style, + size = size.ToString().TrimStart('_'), + response_format = "url" + }); + } + else + { + serialized = JsonConvert.SerializeObject(new + { + prompt, + model = modelString, + artistic_level = artistic_level, + style = style, + substyle = substyle, + size = size.ToString().TrimStart('_'), + response_format = "url" + }); + } + + + + var content = new StringContent( + serialized, + Encoding.UTF8, + "application/json" + ); + + var response = await _httpClient.PostAsync($"{_baseUrl}/images/generations", content); + //var response = await _httpClient.PostAsync($"{_baseUrl}", content); + await EnsureSuccessfulResponse(response); + + var responseContent = await response.Content.ReadAsStringAsync(); + + return JsonConvert.DeserializeObject(responseContent); + } + + public async Task CreateStyleAsync(byte[] imageData, RecraftStyle style) + { + using var content = new MultipartFormDataContent(); + content.Add(new ByteArrayContent(imageData), "file", "image.png"); + content.Add(new StringContent(style.ToString().ToLower()), "style"); + + var response = await _httpClient.PostAsync($"{_baseUrl}/styles", content); + await EnsureSuccessfulResponse(response); + + var responseContent = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseContent); + } + + public async Task VectorizeImageAsync(byte[] imageData, string responseFormat = "url") + { + using var content = new MultipartFormDataContent(); + content.Add(new ByteArrayContent(imageData), "file", "image.png"); + if (responseFormat != "url") + { + content.Add(new StringContent(responseFormat), "response_format"); + } + + var response = await _httpClient.PostAsync($"{_baseUrl}/images/vectorize", content); + await EnsureSuccessfulResponse(response); + + var responseContent = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseContent); + } + + public async Task RemoveBackgroundAsync(byte[] imageData, string responseFormat = "url") + { + using var content = new MultipartFormDataContent(); + content.Add(new ByteArrayContent(imageData), "file", "image.png"); + if (responseFormat != "url") + { + content.Add(new StringContent(responseFormat), "response_format"); + } + + var response = await _httpClient.PostAsync($"{_baseUrl}/images/removeBackground", content); + await EnsureSuccessfulResponse(response); + + var responseContent = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseContent); + } + + public async Task ClarityUpscaleAsync(byte[] imageData, string responseFormat = "url") + { + using var content = new MultipartFormDataContent(); + content.Add(new ByteArrayContent(imageData), "file", "image.png"); + if (responseFormat != "url") + { + content.Add(new StringContent(responseFormat), "response_format"); + } + + var response = await _httpClient.PostAsync($"{_baseUrl}/images/clarityUpscale", content); + await EnsureSuccessfulResponse(response); + + var responseContent = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseContent); + } + + public async Task GenerativeUpscaleAsync(byte[] imageData, string responseFormat = "url") + { + using var content = new MultipartFormDataContent(); + content.Add(new ByteArrayContent(imageData), "file", "image.png"); + if (responseFormat != "url") + { + content.Add(new StringContent(responseFormat), "response_format"); + } + + var response = await _httpClient.PostAsync($"{_baseUrl}/images/generativeUpscale", content); + await EnsureSuccessfulResponse(response); + + var responseContent = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseContent); + } + + private async Task EnsureSuccessfulResponse(HttpResponseMessage response) + { + if (!response.IsSuccessStatusCode) + { + var error = await response.Content.ReadAsStringAsync(); + throw new HttpRequestException($"API request failed: {response.StatusCode} - {error}"); + } + } + } + + public class GenerationResponse + { + [JsonProperty("data")] + public List Data { get; set; } + + [JsonProperty("created")] + public long Created { get; set; } + } + + public class StyleResponse + { + [JsonProperty("id")] + public string Id { get; set; } + } + + public class ImageResponse + { + [JsonProperty("image")] + public ImageData Image { get; set; } + } + + public class ImageData + { + [JsonProperty("url")] + public string Url { get; set; } + + [JsonProperty("b64_json")] + public string Base64Json { get; set; } + } +} \ No newline at end of file diff --git a/RecraftAPI/RecraftDigitalIllustrationSubstyle.cs b/RecraftAPI/RecraftDigitalIllustrationSubstyle.cs new file mode 100644 index 0000000..c102f01 --- /dev/null +++ b/RecraftAPI/RecraftDigitalIllustrationSubstyle.cs @@ -0,0 +1,48 @@ +using System.Runtime.Serialization; + +namespace RecraftAPIClient +{ + public enum RecraftDigitalIllustrationSubstyle + { + [EnumMember(Value = "2d_art_poster")] + _2d_art_poster = 1, + [EnumMember(Value = "2d_art_poster_2")] + _2d_art_poster_2 = 2, + engraving_color = 3, + grain = 4, + hand_drawn = 5, + hand_drawn_outline = 6, + handmade_3d = 7, + infantile_sketch = 8, + pixel_art = 9, + antiquarian = 10, + bold_fantasy = 11, + child_book = 12, + cover = 13, + crosshatch = 14, + digital_engraving = 15, + expressionism = 16, + freehand_details = 17, + grain_20 = 18, + graphic_intensity = 19, + hard_comics = 20, + long_shadow = 21, + modern_folk = 22, + multicolor = 23, + neon_calm = 24, + noir = 25, + nostalgic_pastel = 26, + outline_details = 27, + pastel_gradient = 28, + pastel_sketch = 29, + pop_art = 30, + pop_renaissance = 31, + street_art = 32, + tablet_sketch = 33, + urban_glow = 34, + urban_sketching = 35, + vanilla_dreams = 36, + young_adult_book = 37, + young_adult_book_2 = 38, + } +} diff --git a/RecraftAPI/RecraftImageSize.cs b/RecraftAPI/RecraftImageSize.cs new file mode 100644 index 0000000..7ca0643 --- /dev/null +++ b/RecraftAPI/RecraftImageSize.cs @@ -0,0 +1,23 @@ +namespace RecraftAPIClient +{ + public enum RecraftImageSize + { + _1024x1024 = 1, + _1365x1024 = 2, + _1024x1365 = 3, + _1536x1024 = 4, + _1024x1536 = 5, + _1820x1024 = 6, + _1024x1820 = 7, + _1024x2048 = 8, + _2048x1024 = 9, + _1434x1024 = 10, + _1024x1434 = 11, + _1024x1280 = 12, + _1280x1024 = 13, + _1024x1707 = 14, + _1707x1024 = 15, + // V4 / V4.1 Pro models use a 2K size set; 1:1 is 2048x2048. + _2048x2048 = 16 + } +} diff --git a/RecraftAPI/RecraftModel.cs b/RecraftAPI/RecraftModel.cs new file mode 100644 index 0000000..37da94c --- /dev/null +++ b/RecraftAPI/RecraftModel.cs @@ -0,0 +1,18 @@ +namespace RecraftAPIClient +{ + /// Recraft raster/vector generation model. The API string is + /// the lowercase enum name (e.g. "recraftv3", "recraftv4", "recraftv4_1"). + /// V4.1 (2026) is the current flagship; the API default model is + /// recraftv4_1 as of June 2026. The _vector/_utility variants are + /// expressed via the style parameter on our side, not separate enum + /// members, except where Recraft exposes them as distinct model ids. + public enum RecraftModel + { + recraftv2, + recraftv3, + recraftv4, + recraftv4pro, + recraftv4_1, + recraftv4_1_pro, + } +} diff --git a/RecraftAPI/RecraftRealisticImageSubstyle.cs b/RecraftAPI/RecraftRealisticImageSubstyle.cs new file mode 100644 index 0000000..85792fe --- /dev/null +++ b/RecraftAPI/RecraftRealisticImageSubstyle.cs @@ -0,0 +1,25 @@ +namespace RecraftAPIClient +{ + public enum RecraftRealisticImageSubstyle + { + b_and_w = 1, + enterprise = 2, + evening_light = 3, + faded_nostalgia = 4, + forest_life = 5, + hard_flash = 6, + hdr = 7, + motion_blur = 8, + mystic_naturalism = 9, + natural_light = 10, + natural_tones = 11, + organic_calm = 12, + real_life_glow = 13, + retro_realism = 14, + retro_snapshot = 15, + studio_portrait = 16, + urban_drama = 17, + village_realism = 18, + warm_folk = 19, + } +} diff --git a/RecraftAPI/RecraftStyle.cs b/RecraftAPI/RecraftStyle.cs new file mode 100644 index 0000000..4ae6762 --- /dev/null +++ b/RecraftAPI/RecraftStyle.cs @@ -0,0 +1,10 @@ +namespace RecraftAPIClient +{ + public enum RecraftStyle + { + digital_illustration = 1, + realistic_image = 2, + vector_illustration = 3, + any = 4, + } +} diff --git a/RecraftAPI/RecraftVectorIllustrationSubstyle.cs b/RecraftAPI/RecraftVectorIllustrationSubstyle.cs new file mode 100644 index 0000000..ae8ba6e --- /dev/null +++ b/RecraftAPI/RecraftVectorIllustrationSubstyle.cs @@ -0,0 +1,29 @@ +namespace RecraftAPIClient +{ + public enum RecraftVectorIllustrationSubstyle + { + bold_stroke = 1, + chemistry = 2, + colored_stencil = 3, + contour_pop_art = 4, + cosmics = 5, + cutout = 6, + depressive = 7, + editorial = 8, + emotional_flat = 9, + engraving = 10, + infographical = 11, + line_art = 12, + line_circuit = 13, + linocut = 14, + marker_outline = 15, + mosaic = 16, + naivector = 17, + roundish_flat = 18, + segmented_colors = 19, + sharp_contrast = 20, + thin = 21, + vector_photo = 22, + vivid_shapes = 23, + } +} diff --git a/XAIGrokAPI/XAIGrokAPIClient.csproj b/XAIGrokAPI/XAIGrokAPIClient.csproj new file mode 100644 index 0000000..7a5a23a --- /dev/null +++ b/XAIGrokAPI/XAIGrokAPIClient.csproj @@ -0,0 +1,21 @@ + + + + Library + net9.0 + 13.0 + enable + enable + {C6F1B6C2-3D41-4D9A-8F1E-5B7A2C3D4E5F} + AnyCPU;ARM64 + + + + + + + + + + + diff --git a/XAIGrokAPI/XAIGrokClient.cs b/XAIGrokAPI/XAIGrokClient.cs new file mode 100644 index 0000000..7523ad9 --- /dev/null +++ b/XAIGrokAPI/XAIGrokClient.cs @@ -0,0 +1,452 @@ +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace XAIGrokAPIClient +{ + /// Hand-rolled REST client for xAI's image endpoints. Targets the two + /// documented routes: + /// + /// POST https://api.x.ai/v1/images/generations (text -> image) + /// POST https://api.x.ai/v1/images/edits (text + image -> image) + /// + /// We deliberately do NOT use the OpenAI SDK here — the user wanted full + /// control over the wire shape. That means: + /// * every supported field in the xAI REST reference is exposed on the + /// request DTOs below (aspect_ratio, quality, resolution, n, user, + /// response_format, ...), + /// * unset/null fields are omitted from the JSON so we don't accidentally + /// send "size"/"style" (xAI explicitly rejects those as unsupported), + /// * we return the raw response shape including usage.cost_in_usd_ticks + /// so callers can track actual spend. + /// + /// Models supported (2026-04): + /// grok-imagine-image — $0.02/image, 300 rpm + /// grok-imagine-image-pro — $0.07/image, 30 rpm + /// grok-imagine-video — async text/image -> video (mp4), 1-15s + /// + /// Docs: https://docs.x.ai/developers/model-capabilities/images/generation + /// https://docs.x.ai/developers/rest-api-reference/inference/images + /// https://docs.x.ai/developers/model-capabilities/video/generation + /// https://docs.x.ai/developers/rest-api-reference/inference/videos + public class XAIGrokClient + { + public const string BaseUrl = "https://api.x.ai/v1"; + public const string ModelGrokImagine = "grok-imagine-image"; + public const string ModelGrokImaginePro = "grok-imagine-image-pro"; + public const string ModelGrokImagineVideo = "grok-imagine-video"; + + private readonly HttpClient _httpClient; + private readonly string _apiKey; + + public XAIGrokClient(string apiKey, HttpClient? httpClient = null) + { + if (string.IsNullOrWhiteSpace(apiKey)) + { + throw new ArgumentException("xAI API key is required.", nameof(apiKey)); + } + _apiKey = apiKey; + _httpClient = httpClient ?? new HttpClient + { + Timeout = TimeSpan.FromMinutes(5), + }; + } + + /// Text-to-image. Returns the full parsed response so callers can pull + /// URLs or b64_json blobs and read usage.cost_in_usd_ticks. + public Task GenerateAsync(XAIGrokGenerateRequest request, CancellationToken ct = default) + => PostAsync("/images/generations", request, ct); + + /// Text + image -> image. Mirrors GenerateAsync but hits /images/edits + /// and expects request.Image (or request.Images) to be populated. + public Task EditAsync(XAIGrokEditRequest request, CancellationToken ct = default) + => PostAsync("/images/edits", request, ct); + + // ---------- Video (asynchronous: start -> poll) ---------- + + /// Step 1 of the video flow: POST /v1/videos/generations. Returns + /// immediately with a request_id; the video is NOT ready yet. + public Task StartVideoAsync(XAIGrokVideoGenerateRequest request, CancellationToken ct = default) + => PostAsync("/videos/generations", request, ct); + + /// POST /v1/videos/extensions: continue an existing video (by url or + /// Files-API file_id) from its last frame. Deferred like generations; + /// poll the returned request_id. The finished clip is the original + /// PLUS the extension, combined. + public Task StartVideoExtensionAsync(XAIGrokVideoExtendRequest request, CancellationToken ct = default) + => PostAsync("/videos/extensions", request, ct); + + /// Step 2: GET /v1/videos/{request_id}. status is one of + /// pending | done | expired | failed. When done, Video.Url holds a + /// temporary mp4 URL — download promptly, xAI expires them. (When the + /// request used storage_options, Video.FileOutput carries the durable + /// Files-API file_id instead.) + public Task GetVideoAsync(string requestId, CancellationToken ct = default) + => GetAsync($"/videos/{requestId}", ct); + + // ---------- Files API (the only enumerable server-side history) ---------- + + /// One page of GET /v1/files. The response always includes a + /// pagination_token; keep calling with it until data.Count < limit. + /// Server max limit is 100. + public Task ListFilesPageAsync( + int limit = 100, + string? paginationToken = null, + string sortBy = "created_at", + string order = "asc", + CancellationToken ct = default) + { + var path = $"/files?limit={limit}&sort_by={sortBy}&order={order}"; + if (!string.IsNullOrEmpty(paginationToken)) + { + path += $"&pagination_token={Uri.EscapeDataString(paginationToken)}"; + } + return GetAsync(path, ct); + } + + /// Walks every page of GET /v1/files and returns the full inventory + /// of stored files for the authenticated team, oldest first. + public async Task> ListAllFilesAsync(CancellationToken ct = default) + { + var all = new List(); + string? token = null; + const int pageSize = 100; + while (true) + { + var page = await ListFilesPageAsync(pageSize, token, ct: ct).ConfigureAwait(false); + all.AddRange(page.Data); + if (page.Data.Count < pageSize || string.IsNullOrEmpty(page.PaginationToken)) + { + break; + } + token = page.PaginationToken; + } + return all; + } + + /// GET /v1/files/{file_id}/content — raw bytes of a stored file. + public async Task DownloadFileContentAsync(string fileId, CancellationToken ct = default) + { + using var req = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/files/{fileId}/content"); + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey); + + using var res = await _httpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false); + if (!res.IsSuccessStatusCode) + { + var text = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + throw new XAIGrokException( + $"xAI /files/{fileId}/content returned {(int)res.StatusCode} {res.StatusCode}: {text}", + (int)res.StatusCode, + text); + } + return await res.Content.ReadAsByteArrayAsync(ct).ConfigureAwait(false); + } + + private async Task GetAsync(string path, CancellationToken ct) + where TRes : XAIGrokResponseBase + { + using var req = new HttpRequestMessage(HttpMethod.Get, BaseUrl + path); + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey); + req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + using var res = await _httpClient.SendAsync(req, ct).ConfigureAwait(false); + var text = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + + if (!res.IsSuccessStatusCode) + { + throw new XAIGrokException( + $"xAI {path} returned {(int)res.StatusCode} {res.StatusCode}: {text}", + (int)res.StatusCode, + text); + } + + var parsed = JsonConvert.DeserializeObject(text) + ?? throw new XAIGrokException( + $"xAI {path} returned an empty/unparseable body.", + (int)res.StatusCode, + text); + parsed.RawBody = text; + return parsed; + } + + /// Convenience wrapper that does the whole start -> poll loop and only + /// returns once the video reaches a terminal state (done/failed/expired) + /// or the timeout elapses (throws TimeoutException). Defaults follow + /// xAI's docs: 5s poll cadence, 10 minute ceiling. + public async Task GenerateVideoAsync( + XAIGrokVideoGenerateRequest request, + TimeSpan? pollInterval = null, + TimeSpan? timeout = null, + CancellationToken ct = default) + { + var start = await StartVideoAsync(request, ct).ConfigureAwait(false); + if (string.IsNullOrEmpty(start.RequestId)) + { + throw new XAIGrokException("xAI /videos/generations returned no request_id.", 200, start.RawBody ?? ""); + } + return await PollVideoToCompletionAsync(start.RequestId, pollInterval, timeout, ct).ConfigureAwait(false); + } + + /// Extension counterpart of GenerateVideoAsync: start the extension + /// and poll until terminal. Same defaults (5s cadence, 10 min ceiling). + public async Task ExtendVideoAsync( + XAIGrokVideoExtendRequest request, + TimeSpan? pollInterval = null, + TimeSpan? timeout = null, + CancellationToken ct = default) + { + var start = await StartVideoExtensionAsync(request, ct).ConfigureAwait(false); + if (string.IsNullOrEmpty(start.RequestId)) + { + throw new XAIGrokException("xAI /videos/extensions returned no request_id.", 200, start.RawBody ?? ""); + } + return await PollVideoToCompletionAsync(start.RequestId, pollInterval, timeout, ct).ConfigureAwait(false); + } + + private async Task PollVideoToCompletionAsync( + string requestId, + TimeSpan? pollInterval, + TimeSpan? timeout, + CancellationToken ct) + { + var interval = pollInterval ?? TimeSpan.FromSeconds(5); + var ceiling = timeout ?? TimeSpan.FromMinutes(10); + var deadline = DateTime.UtcNow + ceiling; + while (true) + { + ct.ThrowIfCancellationRequested(); + var result = await GetVideoAsync(requestId, ct).ConfigureAwait(false); + if (!string.Equals(result.Status, "pending", StringComparison.OrdinalIgnoreCase)) + { + result.RequestId = requestId; + return result; + } + if (DateTime.UtcNow >= deadline) + { + throw new TimeoutException( + $"xAI video request {requestId} still pending after {ceiling.TotalMinutes:0.#} minutes."); + } + await Task.Delay(interval, ct).ConfigureAwait(false); + } + } + + private Task PostAsync(string path, TReq body, CancellationToken ct) + => PostAsync(path, body, ct); + + private async Task PostAsync(string path, TReq body, CancellationToken ct) + where TRes : XAIGrokResponseBase + { + var serializer = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Include, + }; + var json = JsonConvert.SerializeObject(body, serializer); + + using var req = new HttpRequestMessage(HttpMethod.Post, BaseUrl + path) + { + Content = new StringContent(json, Encoding.UTF8, "application/json"), + }; + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey); + req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + using var res = await _httpClient.SendAsync(req, ct).ConfigureAwait(false); + var text = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + + if (!res.IsSuccessStatusCode) + { + throw new XAIGrokException( + $"xAI {path} returned {(int)res.StatusCode} {res.StatusCode}: {text}", + (int)res.StatusCode, + text); + } + + var parsed = JsonConvert.DeserializeObject(text) + ?? throw new XAIGrokException( + $"xAI {path} returned an empty/unparseable body.", + (int)res.StatusCode, + text); + parsed.RawBody = text; + return parsed; + } + } + + /// Base for all parsed xAI responses so the generic POST helper can stash + /// the raw JSON body for debugging regardless of concrete shape. + public abstract class XAIGrokResponseBase + { + /// Original JSON body as returned by xAI. Not part of the wire format. + [JsonIgnore] + public string? RawBody { get; set; } + } + + // ---------- Request DTOs ---------- + + /// Request body for POST /v1/images/generations. Only `Prompt` is required + /// per the xAI REST reference; everything else is optional. We intentionally + /// DO NOT expose `size` or `style` — xAI's docs mark both as unsupported and + /// sending them is a footgun. + public class XAIGrokGenerateRequest + { + [JsonProperty("prompt")] + public string Prompt { get; set; } = string.Empty; + + [JsonProperty("model")] + public string? Model { get; set; } + + [JsonProperty("n")] + public int? N { get; set; } + + /// "1:1" | "3:4" | "4:3" | "9:16" | "16:9" | "2:3" | "3:2" | + /// "9:19.5" | "19.5:9" | "9:20" | "20:9" | "1:2" | "2:1" | "auto" + [JsonProperty("aspect_ratio")] + public string? AspectRatio { get; set; } + + /// "low" | "medium" | "high" + [JsonProperty("quality")] + public string? Quality { get; set; } + + /// "1k" | "2k" + [JsonProperty("resolution")] + public string? Resolution { get; set; } + + /// "url" (default) or "b64_json" for inline base64 (no data URI prefix). + [JsonProperty("response_format")] + public string? ResponseFormat { get; set; } + + /// Optional end-user identifier for xAI abuse monitoring. + [JsonProperty("user")] + public string? User { get; set; } + } + + /// Request body for POST /v1/images/edits. xAI supports up to 5 input images + /// per request; they can be supplied as public URLs or base64 data URIs via + /// `XAIGrokImageInput`. The aspect ratio of the output defaults to the first + /// input's AR; override with `AspectRatio` to force a specific shape. + public class XAIGrokEditRequest + { + [JsonProperty("prompt")] + public string Prompt { get; set; } = string.Empty; + + [JsonProperty("model")] + public string? Model { get; set; } + + [JsonProperty("n")] + public int? N { get; set; } + + /// Single-image convenience. If you need multiple inputs, populate + /// `Images` instead (and leave this null). + [JsonProperty("image")] + public XAIGrokImageInput? Image { get; set; } + + /// Multi-image editing. Up to 5 entries per xAI docs. + [JsonProperty("images")] + public List? Images { get; set; } + + [JsonProperty("aspect_ratio")] + public string? AspectRatio { get; set; } + + [JsonProperty("response_format")] + public string? ResponseFormat { get; set; } + + [JsonProperty("user")] + public string? User { get; set; } + } + + /// One input image for /images/edits. Exactly one of Url / Base64Data + /// should be populated. `Type` tells xAI how to interpret the payload + /// ("image_url" for Url, "base64" for Base64Data). + public class XAIGrokImageInput + { + [JsonProperty("type")] + public string Type { get; set; } = "image_url"; + + /// Public HTTPS URL the xAI backend will fetch. + [JsonProperty("url")] + public string? Url { get; set; } + + /// Full base64 data-URI, e.g. "data:image/png;base64,AAAA..." + [JsonProperty("data")] + public string? Base64Data { get; set; } + + public static XAIGrokImageInput FromUrl(string url) => new() + { + Type = "image_url", + Url = url, + }; + + public static XAIGrokImageInput FromBase64(string dataUri) => new() + { + Type = "base64", + Base64Data = dataUri, + }; + } + + // ---------- Response DTOs ---------- + + public class XAIGrokImageResponse : XAIGrokResponseBase + { + [JsonProperty("data")] + public List Data { get; set; } = new(); + + [JsonProperty("usage")] + public XAIGrokUsage? Usage { get; set; } + + /// Server-populated only on some xAI responses; Unix seconds. + [JsonProperty("created")] + public long? Created { get; set; } + } + + public class XAIGrokImageData + { + [JsonProperty("url")] + public string? Url { get; set; } + + [JsonProperty("b64_json")] + public string? Base64Json { get; set; } + + [JsonProperty("mime_type")] + public string? MimeType { get; set; } + + /// Deprecated in current xAI docs — always returned as empty string — + /// but we keep the field in case xAI reactivates it. + [JsonProperty("revised_prompt")] + public string? RevisedPrompt { get; set; } + } + + public class XAIGrokUsage + { + /// Cost of this request in USD ticks. One tick == $1e-8 per xAI billing + /// conventions; convert with `cost_in_usd_ticks / 1e8` for dollars. + [JsonProperty("cost_in_usd_ticks")] + public long? CostInUsdTicks { get; set; } + + /// Convenience conversion to USD. Returns null if ticks weren't reported. + [JsonIgnore] + public decimal? CostUsd => CostInUsdTicks.HasValue + ? CostInUsdTicks.Value / 100_000_000m + : (decimal?)null; + } + + /// Thrown when xAI returns a non-2xx. Carries the raw response body so + /// callers can surface detailed errors (content-policy refusals, 402 + /// insufficient credits, 422 validation, etc.). + public class XAIGrokException : Exception + { + public int StatusCode { get; } + public string ResponseBody { get; } + + public XAIGrokException(string message, int statusCode, string responseBody) + : base(message) + { + StatusCode = statusCode; + ResponseBody = responseBody; + } + } +} diff --git a/XAIGrokAPI/XAIGrokFileModels.cs b/XAIGrokAPI/XAIGrokFileModels.cs new file mode 100644 index 0000000..1ac5d29 --- /dev/null +++ b/XAIGrokAPI/XAIGrokFileModels.cs @@ -0,0 +1,55 @@ +using Newtonsoft.Json; + +using System.Collections.Generic; + +namespace XAIGrokAPIClient +{ + // DTOs for xAI's Files API: + // + // GET /v1/files (paginated list, team-scoped) + // GET /v1/files/{file_id} (metadata) + // GET /v1/files/{file_id}/content (raw bytes) + // + // Video generations submitted with storage_options land here as stored + // files (video.file_output.file_id), which makes the Files API the only + // server-side enumerable "history" xAI offers. Docs: + // https://docs.x.ai/developers/rest-api-reference/files + + public class XAIGrokFileListResponse : XAIGrokResponseBase + { + [JsonProperty("data")] + public List Data { get; set; } = new(); + + /// Always returned; pass back as ?pagination_token= for the next + /// page. End of list when data.Count < requested limit. + [JsonProperty("pagination_token")] + public string? PaginationToken { get; set; } + } + + public class XAIGrokFileObject + { + [JsonProperty("id")] + public string Id { get; set; } = string.Empty; + + [JsonProperty("filename")] + public string? Filename { get; set; } + + [JsonProperty("bytes")] + public long Bytes { get; set; } + + /// Unix seconds. + [JsonProperty("created_at")] + public long CreatedAt { get; set; } + + /// Unix seconds; null = permanent. + [JsonProperty("expires_at")] + public long? ExpiresAt { get; set; } + + /// Always "file"; OpenAI-compat field. + [JsonProperty("object")] + public string? ObjectType { get; set; } + + [JsonProperty("purpose")] + public string? Purpose { get; set; } + } +} diff --git a/XAIGrokAPI/XAIGrokVideoModels.cs b/XAIGrokAPI/XAIGrokVideoModels.cs new file mode 100644 index 0000000..46dbdb0 --- /dev/null +++ b/XAIGrokAPI/XAIGrokVideoModels.cs @@ -0,0 +1,204 @@ +using Newtonsoft.Json; + +namespace XAIGrokAPIClient +{ + // DTOs for xAI's asynchronous video endpoints: + // + // POST https://api.x.ai/v1/videos/generations -> { request_id } + // GET https://api.x.ai/v1/videos/{request_id} -> { status, video, ... } + // + // status: pending | done | expired | failed. When done, video.url is a + // TEMPORARY mp4 URL — callers must download promptly. + // + // Docs: https://docs.x.ai/developers/rest-api-reference/inference/videos + + /// Request body for POST /v1/videos/generations. Text-to-video uses + /// Prompt alone; image-to-video adds Image (the source becomes the first + /// frame). Reference-to-video would use reference_images (not exposed yet + /// — add when needed). + public class XAIGrokVideoGenerateRequest + { + [JsonProperty("prompt")] + public string? Prompt { get; set; } + + /// e.g. XAIGrokClient.ModelGrokImagineVideo ("grok-imagine-video"). + [JsonProperty("model")] + public string? Model { get; set; } + + /// Seconds, range [1, 15]. Default 8 when omitted. + [JsonProperty("duration")] + public int? Duration { get; set; } + + /// "1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3". + /// Default 16:9 for text-to-video; image-to-video inherits the + /// input image's AR unless overridden (which stretches). + [JsonProperty("aspect_ratio")] + public string? AspectRatio { get; set; } + + /// "480p" (default, faster) | "720p" | "1080p". + [JsonProperty("resolution")] + public string? Resolution { get; set; } + + /// Optional image-to-video source. Exactly one of Url / FileId. + [JsonProperty("image")] + public XAIGrokVideoImageInput? Image { get; set; } + + /// When set, xAI stores the finished mp4 in the team's Files API + /// storage (durable, enumerable via GET /v1/files) instead of only a + /// temporary URL. This is what makes server-side history/sync + /// possible — always set it for archival workflows. + [JsonProperty("storage_options")] + public XAIGrokVideoStorageOptions? StorageOptions { get; set; } + } + + public class XAIGrokVideoStorageOptions + { + /// Filename for the stored file (required when storing). + [JsonProperty("filename")] + public string Filename { get; set; } = string.Empty; + + /// Seconds until auto-expiry, max 2592000 (30 days). Null/omitted = + /// the file never expires (preferred for archives). + [JsonProperty("expires_after", NullValueHandling = NullValueHandling.Ignore)] + public int? ExpiresAfter { get; set; } + } + + /// Source image for image-to-video. Url is a public HTTPS URL; FileId + /// references the xAI Files API. Mutually exclusive. + public class XAIGrokVideoImageInput + { + [JsonProperty("url")] + public string? Url { get; set; } + + [JsonProperty("file_id")] + public string? FileId { get; set; } + } + + /// Source video for extend-video (and edit-video). Url is an + /// xAI-hosted or public HTTPS mp4 URL; FileId references the xAI Files + /// API (what storage_options gives us). Mutually exclusive. + public class XAIGrokVideoVideoInput + { + [JsonProperty("url")] + public string? Url { get; set; } + + [JsonProperty("file_id")] + public string? FileId { get; set; } + } + + /// Request body for POST /v1/videos/extensions: continue an existing + /// video from its last frame. xAI returns the ORIGINAL + EXTENSION + /// combined into one clip. Same deferred flow as generations — the + /// response is a request_id polled via GET /v1/videos/{id}. + public class XAIGrokVideoExtendRequest + { + /// What should happen in the extension. + [JsonProperty("prompt")] + public string? Prompt { get; set; } + + [JsonProperty("model")] + public string? Model { get; set; } + + /// The video to extend. Required. + [JsonProperty("video")] + public XAIGrokVideoVideoInput? Video { get; set; } + + /// Seconds of NEW footage to add, range [1, 15]. + [JsonProperty("duration")] + public int? Duration { get; set; } + + [JsonProperty("resolution")] + public string? Resolution { get; set; } + + /// Same semantics as on generate: store the combined clip durably + /// in the Files API so sync can always recover it. + [JsonProperty("storage_options")] + public XAIGrokVideoStorageOptions? StorageOptions { get; set; } + } + + public class XAIGrokVideoStartResponse : XAIGrokResponseBase + { + [JsonProperty("request_id")] + public string? RequestId { get; set; } + } + + /// Poll result from GET /v1/videos/{request_id}. + public class XAIGrokVideoResult : XAIGrokResponseBase + { + public const string StatusPending = "pending"; + public const string StatusDone = "done"; + public const string StatusFailed = "failed"; + public const string StatusExpired = "expired"; + + [JsonProperty("status")] + public string? Status { get; set; } + + [JsonProperty("video")] + public XAIGrokVideoData? Video { get; set; } + + [JsonProperty("model")] + public string? Model { get; set; } + + /// Populated when status == "failed". + [JsonProperty("error")] + public XAIGrokVideoError? Error { get; set; } + + /// Not part of the wire format; filled in by GenerateVideoAsync so + /// callers can quote the id when contacting xAI about failures. + [JsonIgnore] + public string? RequestId { get; set; } + + [JsonIgnore] + public bool IsDone => string.Equals(Status, StatusDone, System.StringComparison.OrdinalIgnoreCase); + } + + public class XAIGrokVideoData + { + /// Temporary xAI-hosted mp4 URL. + [JsonProperty("url")] + public string? Url { get; set; } + + /// Seconds. + [JsonProperty("duration")] + public int? Duration { get; set; } + + /// False means xAI's moderation filtered the output. + [JsonProperty("respect_moderation")] + public bool? RespectModeration { get; set; } + + /// Unix seconds when the stored file expires, when reported. + [JsonProperty("expires_at")] + public long? ExpiresAt { get; set; } + + /// Present when the request included storage_options: the durable + /// Files-API copy of the clip. + [JsonProperty("file_output")] + public XAIGrokVideoFileOutput? FileOutput { get; set; } + } + + public class XAIGrokVideoFileOutput + { + [JsonProperty("file_id")] + public string FileId { get; set; } = string.Empty; + + [JsonProperty("filename")] + public string? Filename { get; set; } + + [JsonProperty("public_url")] + public string? PublicUrl { get; set; } + + [JsonProperty("expires_at")] + public long? ExpiresAt { get; set; } + } + + /// error.code values: invalid_argument | permission_denied | + /// failed_precondition | service_unavailable | internal_error. + public class XAIGrokVideoError + { + [JsonProperty("code")] + public string? Code { get; set; } + + [JsonProperty("message")] + public string? Message { get; set; } + } +} diff --git a/djangoManager/.gitignore b/djangoManager/.gitignore index 633cb0b..f375335 100644 --- a/djangoManager/.gitignore +++ b/djangoManager/.gitignore @@ -1,8 +1,8 @@ -**/env/** -**/env2/** -**/__pycache__/ -env/ -env2/ -.\env2\ -.\env\ +**/env/** +**/env2/** +**/__pycache__/ +env/ +env2/ +.\env2\ +.\env\ djangoManager\env \ No newline at end of file diff --git a/djangoManager/data.html b/djangoManager/data.html index 3480edb..473fa5a 100644 --- a/djangoManager/data.html +++ b/djangoManager/data.html @@ -1,177 +1,177 @@ -Midjourney AI Styles library: 5300+ Artists, Techniques, Artistic Movements + Genres, Titles | Andrei Kovalev's Midlibrary
-
-
-
Show filters
Hide filters
Tag
5400+
Midjourney styles
× Reset filters
The Elder Scrolls Midjourney AI style | Titles | Midlibrary
Titles

Landscapes, Fantasy, Characters, Moody, Subdued, Detailed
The Elder Scrolls
October 20, 2023
The Elder Scrolls
Collection Name (custom)
117
FavoritesFavorites
The Witcher Midjourney AI style | Titles | Midlibrary
Titles

Characters, Dark, Illustrative, Realistic, Moody
The Witcher
October 20, 2023
The Witcher
Collection Name (custom)
117
FavoritesFavorites
Revok Midjourney AI style | Street Artists | Midlibrary
American street artist and muralist
Street Artists

Vivid, Urban, Abstract, Geometric, Bold lines
Revok
October 20, 2023
United States
Collection Name (custom)
117
FavoritesFavorites
Shigeo Fukuda Midjourney AI style | Designers | Midlibrary
Designers

Illustrative, BW, Bold lines, Animals, Surreal, Bold, Vivid
Fukuda
October 20, 2023
Shigeo Fukuda
Collection Name (custom)
117
FavoritesFavorites
Roberto Burle Marx Midjourney AI style | Various Artists | Midlibrary
Various Artists

Landscapes, Geometric, Subdued, Realistic, Illustrative
Burle Marx
October 20, 2023
Roberto Burle Marx
Collection Name (custom)
117
FavoritesFavorites
Sophie Taeuber-Arp Midjourney AI style | Painters | Midlibrary
Painters

Vivid, Geometric, Abstract, Illustrative
Taeuber
October 20, 2023
Sophie Taeuber-Arp
Collection Name (custom)
117
FavoritesFavorites
Yusaku Kamekura Midjourney AI style | Designers | Midlibrary
Designers

Patterns, Illustrative, Geometric, Portraits, Psychedelic, Surreal
Kamekura
October 20, 2023
Yusaku Kamekura
Collection Name (custom)
117
FavoritesFavorites
Sigmar Polke Midjourney AI style | Painters | Midlibrary
Painters

Portraits, Vivid, Fine lines, Illustrative, Drawing, Patterns
Polke
October 20, 2023
Sigmar Polke
Collection Name (custom)
117
FavoritesFavorites
Ryo Takemasa Midjourney AI style | Illustrators | Midlibrary
Illustrators

Detailed, Subdued, Pastel, Landscapes, Animals
Takemasa
October 20, 2023
Ryo Takemasa
Collection Name (custom)
117
FavoritesFavorites
Stan Brakhage Midjourney AI style | Filmmakers | Midlibrary
Filmmakers

Vivid, Expressive, Motion, Moody, Abstract
Brakhage
October 20, 2023
Stan Brakhage
Collection Name (custom)
117
FavoritesFavorites
Pietro Bernini Midjourney AI style | Sculptors + Installation Artists | Midlibrary
Sculptors + Installation Artists

Detailed, Realistic, Subdued, Scenes, Classical
Bernini
October 20, 2023
Pietro Bernini
Collection Name (custom)
117
FavoritesFavorites
Roberto Cavalli Midjourney AI style | Fashion Designers | Midlibrary
Fashion Designers

Detailed, Patterns, Moody, Animals
Cavalli
October 20, 2023
Roberto Cavalli
Collection Name (custom)
117
FavoritesFavorites
Tillie Walden Midjourney AI style | Illustrators | Midlibrary
Illustrators

Fine lines, Characters, Pastel, Moody, Scenes
Walden
October 20, 2023
Tillie Walden
Collection Name (custom)
117
FavoritesFavorites
Toshio Shibata Midjourney AI style | Photographers | Midlibrary
Photographers

Urban, Landscapes, Subdued
Shibata
October 20, 2023
Toshio Shibata
Collection Name (custom)
117
FavoritesFavorites
Rogier van der Weyden Midjourney AI style | Painters | Midlibrary
Painters

Portraits, Subdued, Religious, Classical
van der Weyden
October 20, 2023
Rogier van der Weyden
Collection Name (custom)
117
FavoritesFavorites
Tithi Luadthong Midjourney AI style | Various Artists | Midlibrary
Various Artists

Vivid, Landscapes, Floral, Moody, Dreamy
Luadthong
October 20, 2023
Tithi Luadthong
Collection Name (custom)
117
FavoritesFavorites
Rick and Morty Midjourney AI style | Titles | Midlibrary
Titles

Vivid, Characters, Scenes, Illustrative, Sci-fi
Rick and Morty
October 20, 2023
Rick and Morty
Collection Name (custom)
117
FavoritesFavorites
Tomie dePaola Midjourney AI style | Illustrators | Midlibrary
Illustrators

Cute, Characters, Pastel, Detailed, Scenes
dePaola
October 20, 2023
Tomie dePaola
Collection Name (custom)
117
FavoritesFavorites
Sparth Midjourney AI style | Various Artists | Midlibrary
Various Artists

Sci-fi, Broad brushstrokes, Landscapes, Painterly, Vivid, Subdued
Sparth
October 20, 2023
Sparth
Collection Name (custom)
117
FavoritesFavorites
Matt Rhodes Midjourney AI style | Illustrators | Midlibrary
Illustrators

Characters, Landscapes, Fantasy, Fine lines, Subdued, Moody
Rhodes
October 19, 2023
Matt Rhodes
Collection Name (custom)
117
FavoritesFavorites
Mr. Brainwash Midjourney AI style | Street Artists | Midlibrary
Street Artists

Vivid, Expressive, Bold lines, Broad brushstrokes
Mr. Brainwash
October 19, 2023
Mr. Brainwash
Collection Name (custom)
117
FavoritesFavorites
Paul Gustav Fischer Midjourney AI style | Painters | Midlibrary
Painters

Urban, Moody, Scenes
Fischer
October 19, 2023
Paul Gustav Fischer
Collection Name (custom)
117
FavoritesFavorites
Marion Peck Midjourney AI style | Painters | Midlibrary
Painters

Illustrative, Portraits, Subdued, Animals, Characters, Detailed
Peck
October 19, 2023
Marion Peck
Collection Name (custom)
117
FavoritesFavorites
Mark Ruwedel Midjourney AI style | Photographers | Midlibrary
Photographers

BW, Landscapes, Urban
Ruwedel
October 19, 2023
Mark Ruwedel
Collection Name (custom)
117
FavoritesFavorites
Max Gimblett Midjourney AI style | Various Artists | Midlibrary
Various Artists

Expressive, Broad brushstrokes, Vivid, Painterly, Moody
Gimblett
October 19, 2023
Max Gimblett
Collection Name (custom)
117
FavoritesFavorites
Mary Sibande Midjourney AI style | Sculptors + Installation Artists | Midlibrary
Sculptors + Installation Artists

Vivid, Surreal, Realistic, Expressive, Animals
Sibande
October 19, 2023
Mary Sibande
Collection Name (custom)
117
FavoritesFavorites
Peter Halley Midjourney AI style | Sculptors + Installation Artists | Midlibrary
Sculptors + Installation Artists

Vivid, Geometric, Urban, Illustrative
Halley
October 19, 2023
Peter Halley
Collection Name (custom)
117
FavoritesFavorites
Nendoroid Midjourney AI style | Titles | Midlibrary
Titles

Characters, Cute, Vivid
Nendoroid
October 19, 2023
Nendoroid
Collection Name (custom)
117
FavoritesFavorites
Norman Lewis Midjourney AI style | Painters | Midlibrary
Painters

Moody, Broad brushstrokes, Abstract, Expressive, Scenes
Lewis
October 19, 2023
Norman Lewis
Collection Name (custom)
117
FavoritesFavorites
Mira Schendel Midjourney AI style | Various Artists | Midlibrary
Various Artists

Subdued, Abstract, Geometric, Fine lines
Schendel
October 19, 2023
Mira Schendel
Collection Name (custom)
117
FavoritesFavorites
Neil Blevins Midjourney AI style | Various Artists | Midlibrary
Various Artists

Landscapes, Moody, Sci-fi, Surreal
Blevins
October 19, 2023
Neil Blevins
Collection Name (custom)
117
FavoritesFavorites
Paul Souders Midjourney AI style | Photographers | Midlibrary
Photographers

Detailed, Animals, Landscapes
Souders
October 19, 2023
Paul Souders
Collection Name (custom)
117
FavoritesFavorites

All samples are produced by Midlibrary team using Midjourney AI (if not stated otherwise). Naturally, they are not representative of real artists' works/real-world prototypes.

Support Midlibrary on
Patreon →

Ver. 2.9.1

All content in the Midlibrary catalog is generated by the Midlibrary team using Midjourney AI. We do not feature real artists' images, artworks, or any copyrighted material in our catalog. The samples provided by Midlibrary are intended for educational and illustrative purposes only and are not representative of real artists' works or real-world prototypes. Midlibrary is a non-profit initiative, not affiliated with real artists or authors, aiming to educate and inspire through the demonstration of the technology's potential in creative explorations.
I understand, don't show this again
Encountered a bug?

We do our best to keep this website running as smoothly as possible. However, stuff happens, and we thank you for letting us know!

-
Thank you!
Midlibrary Groundskeeper has been notified.
✕ Close
Something went wrong while \nsubmitting the form. Please, check if you filled all fields.
We're here to help! If you're unable to resolve the issue, please, contact us.
-
Subscribe to Midlibrary Newsletter

We regularly publish new Midjourney Guides, compile new Style Tops, update the website, and have fun! Want to be the first to get Midlibrary news? Subscribe to our newsletter and never miss a thing!

Thank you for subscribing!
Please, expect emails from [email protected]. If you're not receiving our newsletter for a long enough time, please, check your Spam folder.
✕ Close
Something went wrong... Please, check if you filled all required fields.
If you're unable to resolve the issue, please, contact us.

Personal Libraries are available to our Patreon Community

Midlibrary Team

Learn more about the benefits of supporting us by becoming Midlibrary Patron—and start your Personal Library ↗︎

You have just become a Patron, and cannot log in?

Please, allow our team some time (usually not more than 24 hours) to set up your Personal Library.

You may be using different emails for your Patreon and Discord accounts. If that is the case, please, send your Discord email to [email protected].

If the issue perists, or you didn't get a response to your email, please, inform us via Bug Report form

Close

We are currently updating the Personal Libraires' infrastructure

Midlibrary Style Contributor

In the nearest future, it will allow you to access your Collections much quicker, add covers to them, tag the styles you save to quickly find them, and—most importantly—save your --sref (numerical) styles!

However, at the moment, logging in to your Library is unavailable. We apologize for the inconvenience. If you are a Midlibrary Patron, please, check this Patreon post ↗︎ for Personal Libraries status updates.

To start creating Collections and save favorite styles:

Log in with Discord →
Personal Style Collections

Learn more about Personal Style Libraries, saving favorite styles, and organizing them into Collections.

Learn more about supporting Midlibrary and the benefits of joining our Patreon community →

Close
\ No newline at end of file diff --git a/djangoManager/djangoManager.pyproj b/djangoManager/djangoManager.pyproj index 78233c7..f627fe2 100644 --- a/djangoManager/djangoManager.pyproj +++ b/djangoManager/djangoManager.pyproj @@ -1,69 +1,69 @@ - - - - Debug - 2.0 - {19ff410a-88e1-4558-8f50-8c6fccac2b07} - - - - . - . - {888888a0-9f3d-457c-b088-3a5042f75d52} - Standard Python launcher - - - true - - - - - 10.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + Debug + 2.0 + {19ff410a-88e1-4558-8f50-8c6fccac2b07} + + + + . + . + {888888a0-9f3d-457c-b088-3a5042f75d52} + Standard Python launcher + + + true + + + + + 10.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/djangoManager/imageMaker/ideogram/ideogramClient.py b/djangoManager/imageMaker/ideogram/ideogramClient.py index 227942c..ab96e79 100644 --- a/djangoManager/imageMaker/ideogram/ideogramClient.py +++ b/djangoManager/imageMaker/ideogram/ideogramClient.py @@ -1,155 +1,155 @@ -import aiohttp -import json, datetime -from typing import Dict, Any, List, Optional -from enum import Enum, auto -import asyncio -from dataclasses import dataclass, asdict - - -class IdeogramAspectRatio(Enum): - ASPECT_1_1 = "1:1" - ASPECT_16_9 = "16:9" - ASPECT_9_16 = "9:16" - ASPECT_4_3 = "4:3" - ASPECT_3_4 = "3:4" - -class IdeogramModel(Enum): - V_1 = "1" - V_2 = "2" - -class IdeogramMagicPromptOption(Enum): - ON = "on" - OFF = "off" - -class IdeogramStyleType(Enum): - GENERAL = "general" - ANIME = "anime" - PHOTOGRAPHY = "photography" - -@dataclass -class IdeogramDetails: - aspect_ratio: Optional[IdeogramAspectRatio] = None - model: Optional[IdeogramModel] = None - magic_prompt_option: Optional[IdeogramMagicPromptOption] = None - style_type: Optional[IdeogramStyleType] = None - negative_prompt: str = "" - -@dataclass -class IdeogramGenerateRequest: - prompt: str - aspect_ratio: Optional[IdeogramAspectRatio] = None - resolution: Optional[str] = None - model: Optional[IdeogramModel] = None - magic_prompt_option: Optional[IdeogramMagicPromptOption] = None - style_type: Optional[IdeogramStyleType] = None - negative_prompt: str = "" - seed: Optional[int] = None - - def to_dict(self): - return {k: v.value if isinstance(v, Enum) else v for k, v in asdict(self).items() if v is not None} - -@dataclass -class ImageObject: - url: str - prompt: str - resolution: str - is_image_safe: bool - seed: int - -@dataclass -class GenerateResponse: - created: datetime - data: List[ImageObject] - - -class IdeogramClient: - BASE_URL = "https://api.ideogram.ai" - - def __init__(self, api_key: str): - self.api_key = api_key - self._session: Optional[aiohttp.ClientSession] = None - - async def _get_session(self) -> aiohttp.ClientSession: - if self._session is None or self._session.closed: - self._session = aiohttp.ClientSession(headers={"Api-Key": self.api_key}) - return self._session - - async def generate_image_async(self, request: IdeogramGenerateRequest) -> GenerateResponse: - session = await self._get_session() - json_request = json.dumps({"image_request": request.to_dict()}, default=str) - - async with session.post(f"{self.BASE_URL}/generate", data=json_request, headers={"Content-Type": "application/json"}) as response: - if response.status != 200: - error_content = await response.text() - raise aiohttp.ClientResponseError( - response.request_info, - response.history, - status=response.status, - message=f"API request failed with status code {response.status}. Response: {error_content}" - ) - - content = await response.json() - return GenerateResponse( - created=datetime.fromisoformat(content["created"]), - data=[ImageObject(**img) for img in content["data"]] - ) - - async def close(self): - if self._session and not self._session.closed: - await self._session.close() - -class IdeogramService: - def __init__(self, api_key: str, max_concurrency: int): - self.ideogram_client = IdeogramClient(api_key) - self.semaphore = asyncio.Semaphore(max_concurrency) - - async def process_prompt_async(self, prompt_details: Dict[str, Any], stats: Dict[str, Any]) -> Dict[str, Any]: - async with self.semaphore: - try: - ideogram_details = prompt_details.get("ideogram_details", {}) - request = IdeogramGenerateRequest( - prompt=prompt_details["prompt"], - **ideogram_details - ) - - stats["ideogram_request_count"] += 1 - response = await self.ideogram_client.generate_image_async(request) - - if response.data and len(response.data) == 1: - image_object = response.data[0] - returned_prompt = image_object.prompt - - if returned_prompt != prompt_details["prompt"]: - prompt_details["prompt"] = returned_prompt - prompt_details["prompt_rewrite"] = { - "source": "Ideogram rewrite", - "rewrite": returned_prompt - } - - return { - "response": response, - "is_success": True, - "url": image_object.url, - "prompt_details": prompt_details, - "generator": "Ideogram" - } - elif response.data and len(response.data) > 1: - raise Exception("Multiple images returned? I can't handle this! Who knew!") - else: - return { - "is_success": False, - "error_message": "No images generated", - "prompt_details": prompt_details, - "generator": "Ideogram" - } - except Exception as ex: - return { - "is_success": False, - "error_message": str(ex), - "prompt_details": prompt_details, - "generator": "Ideogram" - } - - # If you still need a synchronous version, you can add this method: - def process_prompt(self, prompt_details: Dict[str, Any], stats: Dict[str, Any]) -> Dict[str, Any]: - return asyncio.run(self.process_prompt_async(prompt_details, stats)) +import aiohttp +import json, datetime +from typing import Dict, Any, List, Optional +from enum import Enum, auto +import asyncio +from dataclasses import dataclass, asdict + + +class IdeogramAspectRatio(Enum): + ASPECT_1_1 = "1:1" + ASPECT_16_9 = "16:9" + ASPECT_9_16 = "9:16" + ASPECT_4_3 = "4:3" + ASPECT_3_4 = "3:4" + +class IdeogramModel(Enum): + V_1 = "1" + V_2 = "2" + +class IdeogramMagicPromptOption(Enum): + ON = "on" + OFF = "off" + +class IdeogramStyleType(Enum): + GENERAL = "general" + ANIME = "anime" + PHOTOGRAPHY = "photography" + +@dataclass +class IdeogramDetails: + aspect_ratio: Optional[IdeogramAspectRatio] = None + model: Optional[IdeogramModel] = None + magic_prompt_option: Optional[IdeogramMagicPromptOption] = None + style_type: Optional[IdeogramStyleType] = None + negative_prompt: str = "" + +@dataclass +class IdeogramGenerateRequest: + prompt: str + aspect_ratio: Optional[IdeogramAspectRatio] = None + resolution: Optional[str] = None + model: Optional[IdeogramModel] = None + magic_prompt_option: Optional[IdeogramMagicPromptOption] = None + style_type: Optional[IdeogramStyleType] = None + negative_prompt: str = "" + seed: Optional[int] = None + + def to_dict(self): + return {k: v.value if isinstance(v, Enum) else v for k, v in asdict(self).items() if v is not None} + +@dataclass +class ImageObject: + url: str + prompt: str + resolution: str + is_image_safe: bool + seed: int + +@dataclass +class GenerateResponse: + created: datetime + data: List[ImageObject] + + +class IdeogramClient: + BASE_URL = "https://api.ideogram.ai" + + def __init__(self, api_key: str): + self.api_key = api_key + self._session: Optional[aiohttp.ClientSession] = None + + async def _get_session(self) -> aiohttp.ClientSession: + if self._session is None or self._session.closed: + self._session = aiohttp.ClientSession(headers={"Api-Key": self.api_key}) + return self._session + + async def generate_image_async(self, request: IdeogramGenerateRequest) -> GenerateResponse: + session = await self._get_session() + json_request = json.dumps({"image_request": request.to_dict()}, default=str) + + async with session.post(f"{self.BASE_URL}/generate", data=json_request, headers={"Content-Type": "application/json"}) as response: + if response.status != 200: + error_content = await response.text() + raise aiohttp.ClientResponseError( + response.request_info, + response.history, + status=response.status, + message=f"API request failed with status code {response.status}. Response: {error_content}" + ) + + content = await response.json() + return GenerateResponse( + created=datetime.fromisoformat(content["created"]), + data=[ImageObject(**img) for img in content["data"]] + ) + + async def close(self): + if self._session and not self._session.closed: + await self._session.close() + +class IdeogramService: + def __init__(self, api_key: str, max_concurrency: int): + self.ideogram_client = IdeogramClient(api_key) + self.semaphore = asyncio.Semaphore(max_concurrency) + + async def process_prompt_async(self, prompt_details: Dict[str, Any], stats: Dict[str, Any]) -> Dict[str, Any]: + async with self.semaphore: + try: + ideogram_details = prompt_details.get("ideogram_details", {}) + request = IdeogramGenerateRequest( + prompt=prompt_details["prompt"], + **ideogram_details + ) + + stats["ideogram_request_count"] += 1 + response = await self.ideogram_client.generate_image_async(request) + + if response.data and len(response.data) == 1: + image_object = response.data[0] + returned_prompt = image_object.prompt + + if returned_prompt != prompt_details["prompt"]: + prompt_details["prompt"] = returned_prompt + prompt_details["prompt_rewrite"] = { + "source": "Ideogram rewrite", + "rewrite": returned_prompt + } + + return { + "response": response, + "is_success": True, + "url": image_object.url, + "prompt_details": prompt_details, + "generator": "Ideogram" + } + elif response.data and len(response.data) > 1: + raise Exception("Multiple images returned? I can't handle this! Who knew!") + else: + return { + "is_success": False, + "error_message": "No images generated", + "prompt_details": prompt_details, + "generator": "Ideogram" + } + except Exception as ex: + return { + "is_success": False, + "error_message": str(ex), + "prompt_details": prompt_details, + "generator": "Ideogram" + } + + # If you still need a synchronous version, you can add this method: + def process_prompt(self, prompt_details: Dict[str, Any], stats: Dict[str, Any]) -> Dict[str, Any]: + return asyncio.run(self.process_prompt_async(prompt_details, stats)) diff --git a/djangoManager/imageMaker/imageMaker/asgi.py b/djangoManager/imageMaker/imageMaker/asgi.py index 2c1669b..04adb38 100644 --- a/djangoManager/imageMaker/imageMaker/asgi.py +++ b/djangoManager/imageMaker/imageMaker/asgi.py @@ -1,15 +1,15 @@ -""" -ASGI config for imageMaker project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ -""" - -import os -from django.core.asgi import get_asgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'imageMaker.settings') - -application = get_asgi_application() +""" +ASGI config for imageMaker project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ +""" + +import os +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'imageMaker.settings') + +application = get_asgi_application() diff --git a/djangoManager/imageMaker/imageMaker/request_logging_middleware.py b/djangoManager/imageMaker/imageMaker/request_logging_middleware.py index 6322609..23ddf21 100644 --- a/djangoManager/imageMaker/imageMaker/request_logging_middleware.py +++ b/djangoManager/imageMaker/imageMaker/request_logging_middleware.py @@ -1,138 +1,138 @@ -import logging, json, traceback, time -from django.conf import settings -from urllib.parse import urlencode -from django.db import connection -from django.db.backends.utils import CursorWrapper -from pprint import pformat - -logger = logging.getLogger(__name__) - -class QueryLoggerCursorWrapper(CursorWrapper): - def __init__(self, cursor, db, query_logger): - self.cursor = cursor - self.db = db - self.query_logger = query_logger - - def execute(self, sql, params=None): - start = time.time() - try: - return self.cursor.execute(sql, params) - finally: - duration = time.time() - start - self.query_logger.log_query(sql, params, duration) - - def executemany(self, sql, param_list): - start = time.time() - try: - return self.cursor.executemany(sql, param_list) - finally: - duration = time.time() - start - self.query_logger.log_query(sql, param_list, duration) - -class QueryLogger: - def __init__(self): - self.queries = [] - - def log_query(self, sql, params, duration): - self.queries.append({ - 'sql': self.format_sql(sql, params), - 'time': f"{duration:.3f}" - }) - - def format_sql(self, sql, params): - if params: - return sql % tuple(map(repr, params)) - return sql - -class RequestLoggingMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - query_logger = QueryLogger() - original_cursor = connection.cursor - - def cursor(): - return QueryLoggerCursorWrapper(original_cursor(), connection, query_logger) - - connection.cursor = cursor - - start_time = time.time() - response = self.get_response(request) - duration = time.time() - start_time - - connection.cursor = original_cursor - - bads=['/favicon.ico', '/robots.txt', '/jsi18n/'] - if not any(bad in request.path for bad in bads) and settings.DEBUG: - log_data = { - 'method': request.method, - 'full_path': self.get_full_path(request), - 'status_code': response.status_code, - 'ip': self.get_client_ip(request), - 'duration': f"{duration:.3f}s", - 'queries': query_logger.queries - } - log_message = self.format_log_message(log_data) - logger.info(log_message) - # Add detailed logging - # logger.info(f"Request: {(request.POST)}") - # logger.info(f"Response: {(response.content)}") - for q in query_logger.queries: - logger.info(f"\t{q['sql']} (Time: {q['time']}s)") - - # Remove this line as it's causing the debugger to pause execution - # import ipdb;ipdb.set_trace() - - if settings.LOCAL and request.method == 'POST' and '/terrainparkour/' not in request.path and '/login/?next' not in request.path and request.path.startswith('/terrain/'): - try: - content = response.content.decode('utf-8') - loadedThing = json.loads(content) - if 'res' not in loadedThing: - logger.info(f"no res in {loadedThing} (perhaps okay)") - else: - item = loadedThing['res'] - formatted_item = pformat(item, indent=4, width=100) - # logger.info(f"\nResponse content:\n{formatted_item}") - except json.JSONDecodeError: - logger.info(f"Unable to decode JSON response: {content}") - except Exception as e: - logger.info(f"Error processing response: {str(e)}") - - return response - - def get_client_ip(self, request): - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - return x_forwarded_for.split(',')[0] if x_forwarded_for else request.META.get('REMOTE_ADDR') - - def get_full_path(self, request): - full_path = request.get_full_path() + " " - remoteActionName = "" - values=[] - if request.method in ['POST', ]: - full_path='postEndpoint\t' - el = next(request.POST.items()) - try: - vv=json.loads(el[0]) - for key, value in sorted(vv.items()): - if key=='remoteActionName': - remoteActionName=value - if remoteActionName=='userData': - break - if key=='data': - if type(value)==list: - continue - for k,v in sorted(value.items()): - values.append((k,v)) - continue - if key=='secret': - continue - except: - pass - - combo = ' '.join([f"{el[0]}={el[1]}" for el in values]) - return f"{full_path:10}{remoteActionName:30}{combo}" - - def format_log_message(self, log_data): - base_message = f"{log_data['method']:20}\t{log_data['status_code']}\t{log_data['duration']}\t{log_data['full_path']}" +import logging, json, traceback, time +from django.conf import settings +from urllib.parse import urlencode +from django.db import connection +from django.db.backends.utils import CursorWrapper +from pprint import pformat + +logger = logging.getLogger(__name__) + +class QueryLoggerCursorWrapper(CursorWrapper): + def __init__(self, cursor, db, query_logger): + self.cursor = cursor + self.db = db + self.query_logger = query_logger + + def execute(self, sql, params=None): + start = time.time() + try: + return self.cursor.execute(sql, params) + finally: + duration = time.time() - start + self.query_logger.log_query(sql, params, duration) + + def executemany(self, sql, param_list): + start = time.time() + try: + return self.cursor.executemany(sql, param_list) + finally: + duration = time.time() - start + self.query_logger.log_query(sql, param_list, duration) + +class QueryLogger: + def __init__(self): + self.queries = [] + + def log_query(self, sql, params, duration): + self.queries.append({ + 'sql': self.format_sql(sql, params), + 'time': f"{duration:.3f}" + }) + + def format_sql(self, sql, params): + if params: + return sql % tuple(map(repr, params)) + return sql + +class RequestLoggingMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + query_logger = QueryLogger() + original_cursor = connection.cursor + + def cursor(): + return QueryLoggerCursorWrapper(original_cursor(), connection, query_logger) + + connection.cursor = cursor + + start_time = time.time() + response = self.get_response(request) + duration = time.time() - start_time + + connection.cursor = original_cursor + + bads=['/favicon.ico', '/robots.txt', '/jsi18n/'] + if not any(bad in request.path for bad in bads) and settings.DEBUG: + log_data = { + 'method': request.method, + 'full_path': self.get_full_path(request), + 'status_code': response.status_code, + 'ip': self.get_client_ip(request), + 'duration': f"{duration:.3f}s", + 'queries': query_logger.queries + } + log_message = self.format_log_message(log_data) + logger.info(log_message) + # Add detailed logging + # logger.info(f"Request: {(request.POST)}") + # logger.info(f"Response: {(response.content)}") + for q in query_logger.queries: + logger.info(f"\t{q['sql']} (Time: {q['time']}s)") + + # Remove this line as it's causing the debugger to pause execution + # import ipdb;ipdb.set_trace() + + if settings.LOCAL and request.method == 'POST' and '/terrainparkour/' not in request.path and '/login/?next' not in request.path and request.path.startswith('/terrain/'): + try: + content = response.content.decode('utf-8') + loadedThing = json.loads(content) + if 'res' not in loadedThing: + logger.info(f"no res in {loadedThing} (perhaps okay)") + else: + item = loadedThing['res'] + formatted_item = pformat(item, indent=4, width=100) + # logger.info(f"\nResponse content:\n{formatted_item}") + except json.JSONDecodeError: + logger.info(f"Unable to decode JSON response: {content}") + except Exception as e: + logger.info(f"Error processing response: {str(e)}") + + return response + + def get_client_ip(self, request): + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + return x_forwarded_for.split(',')[0] if x_forwarded_for else request.META.get('REMOTE_ADDR') + + def get_full_path(self, request): + full_path = request.get_full_path() + " " + remoteActionName = "" + values=[] + if request.method in ['POST', ]: + full_path='postEndpoint\t' + el = next(request.POST.items()) + try: + vv=json.loads(el[0]) + for key, value in sorted(vv.items()): + if key=='remoteActionName': + remoteActionName=value + if remoteActionName=='userData': + break + if key=='data': + if type(value)==list: + continue + for k,v in sorted(value.items()): + values.append((k,v)) + continue + if key=='secret': + continue + except: + pass + + combo = ' '.join([f"{el[0]}={el[1]}" for el in values]) + return f"{full_path:10}{remoteActionName:30}{combo}" + + def format_log_message(self, log_data): + base_message = f"{log_data['method']:20}\t{log_data['status_code']}\t{log_data['duration']}\t{log_data['full_path']}" return f"{base_message}\nIP: {log_data['ip']}\nQueries: {len(log_data['queries'])}" \ No newline at end of file diff --git a/djangoManager/imageMaker/imageMaker/settings.py b/djangoManager/imageMaker/imageMaker/settings.py index 08fe7ff..eb08eea 100644 --- a/djangoManager/imageMaker/imageMaker/settings.py +++ b/djangoManager/imageMaker/imageMaker/settings.py @@ -1,209 +1,209 @@ -""" -Django settings for imageMaker project. - -Generated by 'django-admin startproject' using Django 5.1.2. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/5.1/ref/settings/ -""" - -from pathlib import Path -import os -from images.settings_loader import load_settings - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-5pj1)7gpkepu6ul^nslcvlbh6#07sd1q_e-y^$_f8mr%@w-g7)' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - - -# Application definition -STATIC_URL = '/static/' - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'images', -] - -STATICFILES_DIRS = [ - BASE_DIR / "static", -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'imageMaker.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - BASE_DIR / 'templates', - BASE_DIR / 'images' / 'templates', - ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'imageMaker.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/5.1/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'imagemaker', - 'USER': 'root', - 'PASSWORD': 'root', - 'HOST': 'localhost', - 'PORT': '3306', - "OPTIONS": {"charset": "utf8mb4", "init_command": "SET NAMES 'utf8mb4', GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY,',''));"}, - } -} - - -# Password validation -# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/5.1/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.1/howto/static-files/ - -STATIC_URL = 'static/' - -# Default primary key field type -# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -# Logging Configuration -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', - 'style': '{', - }, - 'simple': { - 'format': '{levelname} {message}', - 'style': '{', - }, - }, - 'handlers': { - 'file': { - 'level': 'INFO', - 'class': 'logging.FileHandler', - 'filename': os.path.join(BASE_DIR, 'logs', 'django.log'), - 'formatter': 'verbose', - }, - 'console': { - 'level': 'INFO', - 'class': 'logging.StreamHandler', - 'formatter': 'simple', - }, - }, - 'loggers': { - 'django': { - 'handlers': ['file', 'console'], - 'level': 'INFO', - 'propagate': True, - }, - 'imageMaker.request_logging_middleware': { - 'handlers': ['file', 'console'], - 'level': 'INFO', - 'propagate': False, - }, - }, -} - -# Ensure the logs directory exists -LOGS_DIR = os.path.join(BASE_DIR, 'logs') -os.makedirs(LOGS_DIR, exist_ok=True) - -# Add this to enable request logging -LOCAL = True - -DATA_UPLOAD_MAX_MEMORY_SIZE = 104857600 # 100MB - -# Load custom settings -CUSTOM_SETTINGS = load_settings() - -if CUSTOM_SETTINGS: - # Use the custom settings in your Django configuration - MEDIA_ROOT = CUSTOM_SETTINGS.image_download_base_folder - IDEOGRAM_API_KEY = CUSTOM_SETTINGS.ideogram_api_key - OPENAI_API_KEY = CUSTOM_SETTINGS.openai_api_key - ANTHROPIC_API_KEY = CUSTOM_SETTINGS.anthropic_api_key - BFL_API_KEY = CUSTOM_SETTINGS.bfl_api_key - - # You can add more custom settings as needed -else: - # Fallback values if settings couldn't be loaded - MEDIA_ROOT = os.path.join(BASE_DIR, 'media') - IDEOGRAM_API_KEY = 'your_fallback_ideogram_api_key_here' - OPENAI_API_KEY = 'your_fallback_openai_api_key_here' - ANTHROPIC_API_KEY = 'your_fallback_anthropic_api_key_here' - BFL_API_KEY = 'your_fallback_bfl_api_key_here' +""" +Django settings for imageMaker project. + +Generated by 'django-admin startproject' using Django 5.1.2. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" + +from pathlib import Path +import os +from images.settings_loader import load_settings + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-5pj1)7gpkepu6ul^nslcvlbh6#07sd1q_e-y^$_f8mr%@w-g7)' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition +STATIC_URL = '/static/' + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'images', +] + +STATICFILES_DIRS = [ + BASE_DIR / "static", +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'imageMaker.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + BASE_DIR / 'templates', + BASE_DIR / 'images' / 'templates', + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'imageMaker.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'imagemaker', + 'USER': 'root', + 'PASSWORD': 'root', + 'HOST': 'localhost', + 'PORT': '3306', + "OPTIONS": {"charset": "utf8mb4", "init_command": "SET NAMES 'utf8mb4', GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY,',''));"}, + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Logging Configuration +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', + 'style': '{', + }, + 'simple': { + 'format': '{levelname} {message}', + 'style': '{', + }, + }, + 'handlers': { + 'file': { + 'level': 'INFO', + 'class': 'logging.FileHandler', + 'filename': os.path.join(BASE_DIR, 'logs', 'django.log'), + 'formatter': 'verbose', + }, + 'console': { + 'level': 'INFO', + 'class': 'logging.StreamHandler', + 'formatter': 'simple', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['file', 'console'], + 'level': 'INFO', + 'propagate': True, + }, + 'imageMaker.request_logging_middleware': { + 'handlers': ['file', 'console'], + 'level': 'INFO', + 'propagate': False, + }, + }, +} + +# Ensure the logs directory exists +LOGS_DIR = os.path.join(BASE_DIR, 'logs') +os.makedirs(LOGS_DIR, exist_ok=True) + +# Add this to enable request logging +LOCAL = True + +DATA_UPLOAD_MAX_MEMORY_SIZE = 104857600 # 100MB + +# Load custom settings +CUSTOM_SETTINGS = load_settings() + +if CUSTOM_SETTINGS: + # Use the custom settings in your Django configuration + MEDIA_ROOT = CUSTOM_SETTINGS.image_download_base_folder + IDEOGRAM_API_KEY = CUSTOM_SETTINGS.ideogram_api_key + OPENAI_API_KEY = CUSTOM_SETTINGS.openai_api_key + ANTHROPIC_API_KEY = CUSTOM_SETTINGS.anthropic_api_key + BFL_API_KEY = CUSTOM_SETTINGS.bfl_api_key + + # You can add more custom settings as needed +else: + # Fallback values if settings couldn't be loaded + MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + IDEOGRAM_API_KEY = 'your_fallback_ideogram_api_key_here' + OPENAI_API_KEY = 'your_fallback_openai_api_key_here' + ANTHROPIC_API_KEY = 'your_fallback_anthropic_api_key_here' + BFL_API_KEY = 'your_fallback_bfl_api_key_here' diff --git a/djangoManager/imageMaker/imageMaker/urls.py b/djangoManager/imageMaker/imageMaker/urls.py index 134a8f1..2821f81 100644 --- a/djangoManager/imageMaker/imageMaker/urls.py +++ b/djangoManager/imageMaker/imageMaker/urls.py @@ -1,20 +1,20 @@ -from django.contrib import admin -from django.urls import path -from images import views -from images import image_views, image_viewer -from django.views.decorators.csrf import csrf_exempt - -urlpatterns = [ - path('admin/', admin.site.urls), - path('', views.index, name='index'), - path('upload/', views.upload_json, name='upload_json'), - - path('text-input/', views.text_input, name='text_input'), - path('images/', image_views.list_images, name='list_images'), - path('images//', image_views.image_details, name='image_details'), - path('prompt-search/', views.prompt_search, name='prompt-search'), - - path('view-image-dir/', image_viewer.view_image_dir, name='view_image_dir'), - path('get-image-list/', image_viewer.get_image_list, name='get_image_list'), - path('get-image/', image_viewer.get_image, name='get_image'), -] +from django.contrib import admin +from django.urls import path +from images import views +from images import image_views, image_viewer +from django.views.decorators.csrf import csrf_exempt + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', views.index, name='index'), + path('upload/', views.upload_json, name='upload_json'), + + path('text-input/', views.text_input, name='text_input'), + path('images/', image_views.list_images, name='list_images'), + path('images//', image_views.image_details, name='image_details'), + path('prompt-search/', views.prompt_search, name='prompt-search'), + + path('view-image-dir/', image_viewer.view_image_dir, name='view_image_dir'), + path('get-image-list/', image_viewer.get_image_list, name='get_image_list'), + path('get-image/', image_viewer.get_image, name='get_image'), +] diff --git a/djangoManager/imageMaker/imageMaker/wsgi.py b/djangoManager/imageMaker/imageMaker/wsgi.py index 31ff3c0..db9d6dc 100644 --- a/djangoManager/imageMaker/imageMaker/wsgi.py +++ b/djangoManager/imageMaker/imageMaker/wsgi.py @@ -1,16 +1,16 @@ -""" -WSGI config for imageMaker project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'imageMaker.settings') - -application = get_wsgi_application() +""" +WSGI config for imageMaker project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'imageMaker.settings') + +application = get_wsgi_application() diff --git a/djangoManager/imageMaker/images/admin.py b/djangoManager/imageMaker/images/admin.py index 2b6c7b5..06cb6de 100644 --- a/djangoManager/imageMaker/images/admin.py +++ b/djangoManager/imageMaker/images/admin.py @@ -1,278 +1,278 @@ -from django.contrib import admin -from django.utils.html import mark_safe -from django.urls import path, reverse -from django.http import HttpResponseRedirect -from django.shortcuts import render, redirect -from django.contrib.admin import helpers -import os -import requests -from .models import ( - Prompt, CreationType, Rewriter, PromptTag, Tag, ImageGeneration, - ImageSaveType, ImageSave, ImageProducerType, ImageProducerTraits, - ImageProducerVersion -) - -# Register your models here. - -class BaseModelAdmin(admin.ModelAdmin): - def get_fields(self, request, obj=None): - fields = list(super().get_fields(request, obj)) - for field in ['created', 'updated']: - if field in fields: - fields.remove(field) - - fields.append(field) - return fields - - def get_list_display(self, request): - list_display = super().get_list_display(request) - if not isinstance(list_display, list): - list_display = list(list_display) - if 'created' not in list_display: - list_display.append('created') - if 'updated' not in list_display: - list_display.append('updated') - return list_display - -@admin.register(Prompt) -class PromptAdmin(BaseModelAdmin): - list_display = ('id', 'Text', 'Creator', 'CreationType',) - list_filter = ('CreationType',) - search_fields = ('Text', 'Creator__username') # Add this line - - actions = ['produce_generation'] - - def get_creator(self, obj): - if obj.Creator: - url = reverse("admin:auth_user_change", args=[obj.Creator.id]) - return mark_safe(f'{obj.Creator.username}') - return "N/A" - - get_creator.short_description = 'Creator' - - def get_creation_type(self, obj): - return mark_safe(obj.CreationType.clink(text=obj.CreationType.Name) if obj.CreationType else "N/A") - get_creation_type.short_description = 'Creation Type' - - def get_tags(self, obj): - return mark_safe(", ".join([pt.Tag.clink(text=pt.Tag.Name) for pt in obj.prompttags.all()])) - get_tags.short_description = 'Tags' - - def get_image_generations(self, obj): - return mark_safe(", ".join([gen.clink(text=f"Gen {gen.id}") for gen in obj.image_generations.all()])) - get_image_generations.short_description = 'Image Generations' - - def produce_generation(self, request, queryset): - if 'apply' in request.POST: - # This is the form submission - if len(queryset) != 1: - self.message_user(request, "Please select only one prompt for generation.") - return - - prompt = queryset[0] - producer_id = request.POST.get('image_producer') - details = request.POST.get('details') - - # Create the ImageGeneration object - image_producer = ImageProducerType.objects.get(id=producer_id) - image_generation = ImageGeneration.objects.create( - User=request.user, - Prompt=prompt, - ImageProducerVersion=image_producer.imageproducerversion_set.first(), - Details=details, - URI="placeholder_uri" # Replace with actual generated image URI - ) - - self.message_user(request, f"Generation created for prompt: {prompt.Text[:50]}...") - return redirect('admin:images_imagegeneration_change', image_generation.id) - - else: - # This is the initial action call, display the form - if len(queryset) != 1: - self.message_user(request, "Please select only one prompt for generation.") - return - - prompt = queryset[0] - image_producers = ImageProducerType.objects.all() - - context = { - 'title': f"Generate Image for Prompt: {prompt.Text[:50]}...", - 'prompt': prompt, - 'image_producers': image_producers, - 'queryset': queryset, - 'opts': self.model._meta, - 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, - } - return render(request, 'admin/prompt_generate.html', context) - - produce_generation.short_description = "Produce generation for selected prompt" - - def get_urls(self): - urls = super().get_urls() - custom_urls = [ - path('/generate/', self.admin_site.admin_view(self.generate_view), name='prompt-generate'), - ] - return custom_urls + urls - - def generate_view(self, request, prompt_id): - prompt = Prompt.objects.get(id=prompt_id) - image_producers = ImageProducerType.objects.all() - import ipdb;ipdb.set_trace() - if request.method == 'POST': - - producer_id = request.POST.get('image_producer') - details = request.POST.get('details') - - # Here you would typically call your image generation service - # For now, we'll just create a placeholder ImageGeneration object - image_producer = ImageProducerType.objects.get(id=producer_id) - image_generation = ImageGeneration.objects.create( - User=request.user, - Prompt=prompt, - ImageProducerVersion=image_producer.imageproducerversion_set.first(), # Assuming there's at least one version - Details=details, - URI="placeholder_uri" # Replace with actual generated image URI - ) - - self.message_user(request, f"Generation created for prompt: {prompt.Text[:50]}...") - return redirect('admin:images_imagegeneration_change', image_generation.id) - - context = { - 'title': f"Generate Image for Prompt: {prompt.Text[:50]}...", - 'prompt': prompt, - 'image_producers': image_producers, - 'opts': self.model._meta, - } - return render(request, 'admin/prompt_generate.html', context) - -@admin.register(CreationType) -class CreationTypeAdmin(BaseModelAdmin): - list_display = ('Name',) - -@admin.register(Rewriter) -class RewriterAdmin(BaseModelAdmin): - list_display = ('id', 'Name') - -@admin.register(PromptTag) -class PromptTagAdmin(BaseModelAdmin): - list_display = ('id', 'get_prompt', 'get_tag') - - def get_prompt(self, obj): - return mark_safe(obj.Prompt.clink(text=obj.Prompt.Text[:60] + '...')) - get_prompt.short_description = 'Prompt' - - def get_tag(self, obj): - return mark_safe(obj.Tag.clink(text=obj.Tag.Name)) - get_tag.short_description = 'Tag' - -@admin.register(Tag) -class TagAdmin(BaseModelAdmin): - list_display = ('id', 'Name', 'Hidden', 'HideContainingPrompts', 'get_prompts') - - def get_prompts(self, obj): - return mark_safe(", ".join([pt.Prompt.clink(text=pt.Prompt.Text[:20] + '...') for pt in obj.prompttags.all()])) - get_prompts.short_description = 'Prompts' - -@admin.register(ImageGeneration) -class ImageGenerationAdmin(BaseModelAdmin): - list_display = ('id', 'get_user', 'get_prompt', 'get_image_producer_version', 'get_image_saves', 'display_image') - - def get_user(self, obj): - if obj.User: - url = reverse("admin:auth_user_change", args=[obj.User.id]) - return mark_safe(f'{obj.User.username}') - return "N/A" - get_user.short_description = 'User' - - def get_prompt(self, obj): - return mark_safe(obj.Prompt.clink(text=obj.Prompt.Text[:20] + '...')) - get_prompt.short_description = 'Prompt' - - def get_image_producer_version(self, obj): - return mark_safe(obj.ImageProducerVersion.clink(text=f"{obj.ImageProducerVersion.ImageProducerType.Name} {obj.ImageProducerVersion.Version}")) - get_image_producer_version.short_description = 'Image Producer Version' - - def get_image_saves(self, obj): - return mark_safe(", ".join([save.clink(text=f"{save.ImageSaveType.Name} {save.id}") for save in obj.image_saves.all()])) - get_image_saves.short_description = 'Image Saves' - - def display_image(self, obj): - if obj.URI: - return mark_safe(f'') - return "No image" - display_image.short_description = 'Image' - -@admin.register(ImageSaveType) -class ImageSaveTypeAdmin(BaseModelAdmin): - list_display = ('id', 'Name') - -@admin.register(ImageSave) -class ImageSaveAdmin(BaseModelAdmin): - list_display = ('id', 'get_image_generation', 'get_image_save_type', 'FilePath', 'display_image') - list_filter = ('ImageSaveType',) - actions = ['download_image'] - - def get_image_generation(self, obj): - return mark_safe(obj.ImageGeneration.clink(text=f"Gen {obj.ImageGeneration.id}")) - get_image_generation.short_description = 'Image Generation' - - def get_image_save_type(self, obj): - return mark_safe(obj.ImageSaveType.clink(text=obj.ImageSaveType.Name)) - get_image_save_type.short_description = 'Image Save Type' - - def display_image(self, obj): - if os.path.exists(obj.FilePath): - return mark_safe(f'') - return "Not downloaded" - display_image.short_description = 'Image' - - def download_image(self, request, queryset): - for image_save in queryset: - if not os.path.exists(image_save.FilePath): - try: - response = requests.get(image_save.ImageGeneration.URI) - response.raise_for_status() - os.makedirs(os.path.dirname(image_save.FilePath), exist_ok=True) - with open(image_save.FilePath, 'wb') as f: - f.write(response.content) - self.message_user(request, f"Successfully downloaded image for ImageSave {image_save.id}") - except Exception as e: - self.message_user(request, f"Failed to download image for ImageSave {image_save.id}: {str(e)}") - else: - self.message_user(request, f"Image for ImageSave {image_save.id} already exists") - return HttpResponseRedirect(request.get_full_path()) - download_image.short_description = "Download selected images" - -@admin.register(ImageProducerType) -class ImageProducerTypeAdmin(BaseModelAdmin): - list_display = ('id', 'Name', 'get_traits') - - def get_traits(self, obj): - traits = obj.imageproducertraits_set.first() - if traits: - return mark_safe(traits.clink(text=f"Ext: {traits.SaveExtension}")) - return "No traits" - get_traits.short_description = 'Traits' - -@admin.register(ImageProducerTraits) -class ImageProducerTraitsAdmin(BaseModelAdmin): - list_display = ('id', 'get_image_producer', 'SaveExtension') - - def get_image_producer(self, obj): - return mark_safe(obj.ImageProducerType.clink(text=obj.ImageProducerType.Name)) - get_image_producer.short_description = 'Image Producer' - -@admin.register(ImageProducerVersion) -class ImageProducerVersionAdmin(BaseModelAdmin): - list_display = ('id', 'get_image_producer', 'Version') - list_filter = ('ImageProducerType',) - - def get_image_producer(self, obj): - return mark_safe(obj.ImageProducerType.clink(text=obj.ImageProducerType.Name)) - get_image_producer.short_description = 'Image Producer' - -# Register other models with their respective admin classes - - - - +from django.contrib import admin +from django.utils.html import mark_safe +from django.urls import path, reverse +from django.http import HttpResponseRedirect +from django.shortcuts import render, redirect +from django.contrib.admin import helpers +import os +import requests +from .models import ( + Prompt, CreationType, Rewriter, PromptTag, Tag, ImageGeneration, + ImageSaveType, ImageSave, ImageProducerType, ImageProducerTraits, + ImageProducerVersion +) + +# Register your models here. + +class BaseModelAdmin(admin.ModelAdmin): + def get_fields(self, request, obj=None): + fields = list(super().get_fields(request, obj)) + for field in ['created', 'updated']: + if field in fields: + fields.remove(field) + + fields.append(field) + return fields + + def get_list_display(self, request): + list_display = super().get_list_display(request) + if not isinstance(list_display, list): + list_display = list(list_display) + if 'created' not in list_display: + list_display.append('created') + if 'updated' not in list_display: + list_display.append('updated') + return list_display + +@admin.register(Prompt) +class PromptAdmin(BaseModelAdmin): + list_display = ('id', 'Text', 'Creator', 'CreationType',) + list_filter = ('CreationType',) + search_fields = ('Text', 'Creator__username') # Add this line + + actions = ['produce_generation'] + + def get_creator(self, obj): + if obj.Creator: + url = reverse("admin:auth_user_change", args=[obj.Creator.id]) + return mark_safe(f'{obj.Creator.username}') + return "N/A" + + get_creator.short_description = 'Creator' + + def get_creation_type(self, obj): + return mark_safe(obj.CreationType.clink(text=obj.CreationType.Name) if obj.CreationType else "N/A") + get_creation_type.short_description = 'Creation Type' + + def get_tags(self, obj): + return mark_safe(", ".join([pt.Tag.clink(text=pt.Tag.Name) for pt in obj.prompttags.all()])) + get_tags.short_description = 'Tags' + + def get_image_generations(self, obj): + return mark_safe(", ".join([gen.clink(text=f"Gen {gen.id}") for gen in obj.image_generations.all()])) + get_image_generations.short_description = 'Image Generations' + + def produce_generation(self, request, queryset): + if 'apply' in request.POST: + # This is the form submission + if len(queryset) != 1: + self.message_user(request, "Please select only one prompt for generation.") + return + + prompt = queryset[0] + producer_id = request.POST.get('image_producer') + details = request.POST.get('details') + + # Create the ImageGeneration object + image_producer = ImageProducerType.objects.get(id=producer_id) + image_generation = ImageGeneration.objects.create( + User=request.user, + Prompt=prompt, + ImageProducerVersion=image_producer.imageproducerversion_set.first(), + Details=details, + URI="placeholder_uri" # Replace with actual generated image URI + ) + + self.message_user(request, f"Generation created for prompt: {prompt.Text[:50]}...") + return redirect('admin:images_imagegeneration_change', image_generation.id) + + else: + # This is the initial action call, display the form + if len(queryset) != 1: + self.message_user(request, "Please select only one prompt for generation.") + return + + prompt = queryset[0] + image_producers = ImageProducerType.objects.all() + + context = { + 'title': f"Generate Image for Prompt: {prompt.Text[:50]}...", + 'prompt': prompt, + 'image_producers': image_producers, + 'queryset': queryset, + 'opts': self.model._meta, + 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, + } + return render(request, 'admin/prompt_generate.html', context) + + produce_generation.short_description = "Produce generation for selected prompt" + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path('/generate/', self.admin_site.admin_view(self.generate_view), name='prompt-generate'), + ] + return custom_urls + urls + + def generate_view(self, request, prompt_id): + prompt = Prompt.objects.get(id=prompt_id) + image_producers = ImageProducerType.objects.all() + import ipdb;ipdb.set_trace() + if request.method == 'POST': + + producer_id = request.POST.get('image_producer') + details = request.POST.get('details') + + # Here you would typically call your image generation service + # For now, we'll just create a placeholder ImageGeneration object + image_producer = ImageProducerType.objects.get(id=producer_id) + image_generation = ImageGeneration.objects.create( + User=request.user, + Prompt=prompt, + ImageProducerVersion=image_producer.imageproducerversion_set.first(), # Assuming there's at least one version + Details=details, + URI="placeholder_uri" # Replace with actual generated image URI + ) + + self.message_user(request, f"Generation created for prompt: {prompt.Text[:50]}...") + return redirect('admin:images_imagegeneration_change', image_generation.id) + + context = { + 'title': f"Generate Image for Prompt: {prompt.Text[:50]}...", + 'prompt': prompt, + 'image_producers': image_producers, + 'opts': self.model._meta, + } + return render(request, 'admin/prompt_generate.html', context) + +@admin.register(CreationType) +class CreationTypeAdmin(BaseModelAdmin): + list_display = ('Name',) + +@admin.register(Rewriter) +class RewriterAdmin(BaseModelAdmin): + list_display = ('id', 'Name') + +@admin.register(PromptTag) +class PromptTagAdmin(BaseModelAdmin): + list_display = ('id', 'get_prompt', 'get_tag') + + def get_prompt(self, obj): + return mark_safe(obj.Prompt.clink(text=obj.Prompt.Text[:60] + '...')) + get_prompt.short_description = 'Prompt' + + def get_tag(self, obj): + return mark_safe(obj.Tag.clink(text=obj.Tag.Name)) + get_tag.short_description = 'Tag' + +@admin.register(Tag) +class TagAdmin(BaseModelAdmin): + list_display = ('id', 'Name', 'Hidden', 'HideContainingPrompts', 'get_prompts') + + def get_prompts(self, obj): + return mark_safe(", ".join([pt.Prompt.clink(text=pt.Prompt.Text[:20] + '...') for pt in obj.prompttags.all()])) + get_prompts.short_description = 'Prompts' + +@admin.register(ImageGeneration) +class ImageGenerationAdmin(BaseModelAdmin): + list_display = ('id', 'get_user', 'get_prompt', 'get_image_producer_version', 'get_image_saves', 'display_image') + + def get_user(self, obj): + if obj.User: + url = reverse("admin:auth_user_change", args=[obj.User.id]) + return mark_safe(f'{obj.User.username}') + return "N/A" + get_user.short_description = 'User' + + def get_prompt(self, obj): + return mark_safe(obj.Prompt.clink(text=obj.Prompt.Text[:20] + '...')) + get_prompt.short_description = 'Prompt' + + def get_image_producer_version(self, obj): + return mark_safe(obj.ImageProducerVersion.clink(text=f"{obj.ImageProducerVersion.ImageProducerType.Name} {obj.ImageProducerVersion.Version}")) + get_image_producer_version.short_description = 'Image Producer Version' + + def get_image_saves(self, obj): + return mark_safe(", ".join([save.clink(text=f"{save.ImageSaveType.Name} {save.id}") for save in obj.image_saves.all()])) + get_image_saves.short_description = 'Image Saves' + + def display_image(self, obj): + if obj.URI: + return mark_safe(f'') + return "No image" + display_image.short_description = 'Image' + +@admin.register(ImageSaveType) +class ImageSaveTypeAdmin(BaseModelAdmin): + list_display = ('id', 'Name') + +@admin.register(ImageSave) +class ImageSaveAdmin(BaseModelAdmin): + list_display = ('id', 'get_image_generation', 'get_image_save_type', 'FilePath', 'display_image') + list_filter = ('ImageSaveType',) + actions = ['download_image'] + + def get_image_generation(self, obj): + return mark_safe(obj.ImageGeneration.clink(text=f"Gen {obj.ImageGeneration.id}")) + get_image_generation.short_description = 'Image Generation' + + def get_image_save_type(self, obj): + return mark_safe(obj.ImageSaveType.clink(text=obj.ImageSaveType.Name)) + get_image_save_type.short_description = 'Image Save Type' + + def display_image(self, obj): + if os.path.exists(obj.FilePath): + return mark_safe(f'') + return "Not downloaded" + display_image.short_description = 'Image' + + def download_image(self, request, queryset): + for image_save in queryset: + if not os.path.exists(image_save.FilePath): + try: + response = requests.get(image_save.ImageGeneration.URI) + response.raise_for_status() + os.makedirs(os.path.dirname(image_save.FilePath), exist_ok=True) + with open(image_save.FilePath, 'wb') as f: + f.write(response.content) + self.message_user(request, f"Successfully downloaded image for ImageSave {image_save.id}") + except Exception as e: + self.message_user(request, f"Failed to download image for ImageSave {image_save.id}: {str(e)}") + else: + self.message_user(request, f"Image for ImageSave {image_save.id} already exists") + return HttpResponseRedirect(request.get_full_path()) + download_image.short_description = "Download selected images" + +@admin.register(ImageProducerType) +class ImageProducerTypeAdmin(BaseModelAdmin): + list_display = ('id', 'Name', 'get_traits') + + def get_traits(self, obj): + traits = obj.imageproducertraits_set.first() + if traits: + return mark_safe(traits.clink(text=f"Ext: {traits.SaveExtension}")) + return "No traits" + get_traits.short_description = 'Traits' + +@admin.register(ImageProducerTraits) +class ImageProducerTraitsAdmin(BaseModelAdmin): + list_display = ('id', 'get_image_producer', 'SaveExtension') + + def get_image_producer(self, obj): + return mark_safe(obj.ImageProducerType.clink(text=obj.ImageProducerType.Name)) + get_image_producer.short_description = 'Image Producer' + +@admin.register(ImageProducerVersion) +class ImageProducerVersionAdmin(BaseModelAdmin): + list_display = ('id', 'get_image_producer', 'Version') + list_filter = ('ImageProducerType',) + + def get_image_producer(self, obj): + return mark_safe(obj.ImageProducerType.clink(text=obj.ImageProducerType.Name)) + get_image_producer.short_description = 'Image Producer' + +# Register other models with their respective admin classes + + + + diff --git a/djangoManager/imageMaker/images/apps.py b/djangoManager/imageMaker/images/apps.py index 2921a20..eb9cfad 100644 --- a/djangoManager/imageMaker/images/apps.py +++ b/djangoManager/imageMaker/images/apps.py @@ -1,6 +1,6 @@ -from django.apps import AppConfig - - -class ImagesConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'images' +from django.apps import AppConfig + + +class ImagesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'images' diff --git a/djangoManager/imageMaker/images/cmdlineIdeogramClient.py b/djangoManager/imageMaker/images/cmdlineIdeogramClient.py index c850812..d20c099 100644 --- a/djangoManager/imageMaker/images/cmdlineIdeogramClient.py +++ b/djangoManager/imageMaker/images/cmdlineIdeogramClient.py @@ -1,76 +1,76 @@ -import subprocess -import argparse -import json -import os -import time - -def generate_image(prompt, api_key, output_dir, num_images=1, width=1024, height=1024, timeout=300): - command = [ - "IdeogramClient.exe", - "--api-key", api_key, - "--prompt", prompt, - "--num-images", str(num_images), - "--width", str(width), - "--height", str(height), - "--output-dir", output_dir - ] - - try: - # Start the process - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - - start_time = time.time() - while True: - # Check if the process has finished - return_code = process.poll() - if return_code is not None: - break - - # Check if we've exceeded the timeout - if time.time() - start_time > timeout: - process.kill() - raise subprocess.TimeoutExpired(command, timeout) - - # Wait a bit before checking again - time.sleep(1) - - stdout, stderr = process.communicate() - - if return_code != 0: - raise subprocess.CalledProcessError(return_code, command, stdout, stderr) - - # Parse the JSON output - output = json.loads(stdout) - - print(f"Generated {len(output['images'])} image(s):") - for i, image_path in enumerate(output['images'], 1): - print(f" {i}. {image_path}") - - except subprocess.TimeoutExpired: - print(f"Error: Process timed out after {timeout} seconds") - except subprocess.CalledProcessError as e: - print(f"Error: {e}") - print(f"Error output: {e.stderr}") - except json.JSONDecodeError: - print(f"Error: Unable to parse JSON output from IdeogramClient.exe") - print(f"Raw output: {stdout}") - -def main(): - parser = argparse.ArgumentParser(description="Generate images using Ideogram API") - parser.add_argument("prompt", help="Text prompt for image generation") - parser.add_argument("--api-key", required=True, help="Ideogram API key") - parser.add_argument("--output-dir", default="./output", help="Directory to save generated images") - parser.add_argument("--num-images", type=int, default=1, help="Number of images to generate") - parser.add_argument("--width", type=int, default=1024, help="Image width") - parser.add_argument("--height", type=int, default=1024, help="Image height") - parser.add_argument("--timeout", type=int, default=300, help="Timeout in seconds") - - args = parser.parse_args() - - # Ensure output directory exists - os.makedirs(args.output_dir, exist_ok=True) - - generate_image(args.prompt, args.api_key, args.output_dir, args.num_images, args.width, args.height, args.timeout) - -if __name__ == "__main__": - main() +import subprocess +import argparse +import json +import os +import time + +def generate_image(prompt, api_key, output_dir, num_images=1, width=1024, height=1024, timeout=300): + command = [ + "IdeogramClient.exe", + "--api-key", api_key, + "--prompt", prompt, + "--num-images", str(num_images), + "--width", str(width), + "--height", str(height), + "--output-dir", output_dir + ] + + try: + # Start the process + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + start_time = time.time() + while True: + # Check if the process has finished + return_code = process.poll() + if return_code is not None: + break + + # Check if we've exceeded the timeout + if time.time() - start_time > timeout: + process.kill() + raise subprocess.TimeoutExpired(command, timeout) + + # Wait a bit before checking again + time.sleep(1) + + stdout, stderr = process.communicate() + + if return_code != 0: + raise subprocess.CalledProcessError(return_code, command, stdout, stderr) + + # Parse the JSON output + output = json.loads(stdout) + + print(f"Generated {len(output['images'])} image(s):") + for i, image_path in enumerate(output['images'], 1): + print(f" {i}. {image_path}") + + except subprocess.TimeoutExpired: + print(f"Error: Process timed out after {timeout} seconds") + except subprocess.CalledProcessError as e: + print(f"Error: {e}") + print(f"Error output: {e.stderr}") + except json.JSONDecodeError: + print(f"Error: Unable to parse JSON output from IdeogramClient.exe") + print(f"Raw output: {stdout}") + +def main(): + parser = argparse.ArgumentParser(description="Generate images using Ideogram API") + parser.add_argument("prompt", help="Text prompt for image generation") + parser.add_argument("--api-key", required=True, help="Ideogram API key") + parser.add_argument("--output-dir", default="./output", help="Directory to save generated images") + parser.add_argument("--num-images", type=int, default=1, help="Number of images to generate") + parser.add_argument("--width", type=int, default=1024, help="Image width") + parser.add_argument("--height", type=int, default=1024, help="Image height") + parser.add_argument("--timeout", type=int, default=300, help="Timeout in seconds") + + args = parser.parse_args() + + # Ensure output directory exists + os.makedirs(args.output_dir, exist_ok=True) + + generate_image(args.prompt, args.api_key, args.output_dir, args.num_images, args.width, args.height, args.timeout) + +if __name__ == "__main__": + main() diff --git a/djangoManager/imageMaker/images/forms.py b/djangoManager/imageMaker/images/forms.py index de2c6aa..cb822e7 100644 --- a/djangoManager/imageMaker/images/forms.py +++ b/djangoManager/imageMaker/images/forms.py @@ -1,35 +1,35 @@ -from django import forms -from .models import Prompt, ImageProducerType - -class ImageGenerationForm(forms.Form): - prompt = forms.CharField(widget=forms.Textarea) - existing_prompt = forms.ModelChoiceField( - queryset=Prompt.objects.none(), # Start with an empty queryset - required=False, - empty_label="Choose an existing prompt (optional)" - ) - generator = forms.ChoiceField(choices=ImageProducerType.ImageProducerTypeChoices.choices) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['existing_prompt'].widget.attrs.update({ - 'class': 'select2', - 'data-ajax--url': '/prompt-search/', # Update this URL as needed - 'data-ajax--cache': 'true', - 'data-ajax--delay': '25', - 'data-ajax--data-type': 'json', - 'data-minimum-input-length': '2', - }) - - def clean(self): - cleaned_data = super().clean() - prompt = cleaned_data.get('prompt') - existing_prompt = cleaned_data.get('existing_prompt') - - if not prompt and not existing_prompt: - raise forms.ValidationError("Please enter a prompt or choose an existing one.") - - if existing_prompt: - cleaned_data['prompt'] = existing_prompt.Text - - return cleaned_data +from django import forms +from .models import Prompt, ImageProducerType + +class ImageGenerationForm(forms.Form): + prompt = forms.CharField(widget=forms.Textarea) + existing_prompt = forms.ModelChoiceField( + queryset=Prompt.objects.none(), # Start with an empty queryset + required=False, + empty_label="Choose an existing prompt (optional)" + ) + generator = forms.ChoiceField(choices=ImageProducerType.ImageProducerTypeChoices.choices) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['existing_prompt'].widget.attrs.update({ + 'class': 'select2', + 'data-ajax--url': '/prompt-search/', # Update this URL as needed + 'data-ajax--cache': 'true', + 'data-ajax--delay': '25', + 'data-ajax--data-type': 'json', + 'data-minimum-input-length': '2', + }) + + def clean(self): + cleaned_data = super().clean() + prompt = cleaned_data.get('prompt') + existing_prompt = cleaned_data.get('existing_prompt') + + if not prompt and not existing_prompt: + raise forms.ValidationError("Please enter a prompt or choose an existing one.") + + if existing_prompt: + cleaned_data['prompt'] = existing_prompt.Text + + return cleaned_data diff --git a/djangoManager/imageMaker/images/image_viewer.py b/djangoManager/imageMaker/images/image_viewer.py index 86f1b68..a65b963 100644 --- a/djangoManager/imageMaker/images/image_viewer.py +++ b/djangoManager/imageMaker/images/image_viewer.py @@ -1,42 +1,42 @@ -import os -from django.conf import settings -from django.shortcuts import render -from django.http import JsonResponse, HttpResponse - - -from django.shortcuts import render, redirect -from django.contrib import messages -from django.core.files.storage import default_storage -from django.core.files.base import ContentFile -import json -from .models import * -from typing import Any, List - -from django.db.models import Q -from .models import Prompt - - -def view_image_dir(request: Any) -> Any: - image_dir="/mnt/d/proj/multiImageClient/MultiImageClient/saves/2024-10-15-Tuesday" - return render(request, 'view_image_dir.html', {'image_dir': image_dir}) - -def get_image_list(request: Any) -> JsonResponse: - image_dir = request.GET.get('dir') - if not image_dir or not os.path.exists(image_dir): - return JsonResponse({'error': 'Invalid directory'}, status=400) - - images = [f for f in os.listdir(image_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.gif'))] - return JsonResponse({'images': images}) - -def get_image(request: Any) -> HttpResponse: - image_dir = request.GET.get('dir') - image_name = request.GET.get('name') - if not image_dir or not image_name: - return HttpResponse('Invalid request', status=400) - - image_path = os.path.join(image_dir, image_name) - if not os.path.exists(image_path): - return HttpResponse('Image not found', status=404) - - with open(image_path, 'rb') as img_file: +import os +from django.conf import settings +from django.shortcuts import render +from django.http import JsonResponse, HttpResponse + + +from django.shortcuts import render, redirect +from django.contrib import messages +from django.core.files.storage import default_storage +from django.core.files.base import ContentFile +import json +from .models import * +from typing import Any, List + +from django.db.models import Q +from .models import Prompt + + +def view_image_dir(request: Any) -> Any: + image_dir="/mnt/d/proj/multiImageClient/MultiImageClient/saves/2024-10-15-Tuesday" + return render(request, 'view_image_dir.html', {'image_dir': image_dir}) + +def get_image_list(request: Any) -> JsonResponse: + image_dir = request.GET.get('dir') + if not image_dir or not os.path.exists(image_dir): + return JsonResponse({'error': 'Invalid directory'}, status=400) + + images = [f for f in os.listdir(image_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.gif'))] + return JsonResponse({'images': images}) + +def get_image(request: Any) -> HttpResponse: + image_dir = request.GET.get('dir') + image_name = request.GET.get('name') + if not image_dir or not image_name: + return HttpResponse('Invalid request', status=400) + + image_path = os.path.join(image_dir, image_name) + if not os.path.exists(image_path): + return HttpResponse('Image not found', status=404) + + with open(image_path, 'rb') as img_file: return HttpResponse(img_file.read(), content_type='image/png') # Adjust content_type if needed \ No newline at end of file diff --git a/djangoManager/imageMaker/images/image_views.py b/djangoManager/imageMaker/images/image_views.py index 1496d0a..7d8865a 100644 --- a/djangoManager/imageMaker/images/image_views.py +++ b/djangoManager/imageMaker/images/image_views.py @@ -1,195 +1,195 @@ -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib import messages -from django.http import HttpRequest, HttpResponse, JsonResponse -from typing import Any -from .models import Prompt, ImageGeneration, ImageProducerType, ImageProducerVersion, ImageSave, ImageSaveType -from .forms import ImageGenerationForm -from ideogram.ideogramClient import IdeogramService -from django.conf import settings -import os -import aiohttp -import asyncio -from asgiref.sync import sync_to_async, async_to_sync -import requests -import json -import time -from django.views.decorators.http import require_http_methods -from django.views.decorators.csrf import csrf_protect -from django.utils.decorators import method_decorator -from django.core.cache import cache - -@sync_to_async -def create_or_get_prompt(text, user): - return Prompt.objects.get_or_create(Text=text, Creator=user) - -@sync_to_async -def get_producer_version(generator): - return get_object_or_404(ImageProducerVersion, ImageProducerType__Name=generator) - -@sync_to_async -def create_image_generation(user, prompt, producer_version, details, uri, result): - return ImageGeneration.objects.create( - User=user, - Prompt=prompt, - ImageProducerVersion=producer_version, - Details=details, - URI=uri, - Result=result - ) - -@sync_to_async -def create_image_save(image_generation, image_save_type, file_path): - return ImageSave.objects.create( - ImageGeneration=image_generation, - ImageSaveType=image_save_type, - FilePath=file_path - ) - -@csrf_protect -@require_http_methods(["POST"]) -async def generate_image_async(request: HttpRequest) -> JsonResponse: - import ipdb; ipdb.set_trace() - try: - data = json.loads(request.body) - form = ImageGenerationForm(data) - if form.is_valid(): - prompt = form.cleaned_data['prompt'] - generator = form.cleaned_data['generator'] - - # Get user ID asynchronously - user_id = await sync_to_async(lambda: request.user.id)() - - # Create a task ID - task_id = f"{user_id}_{int(time.time())}" - - # Start the image generation process asynchronously - asyncio.create_task(process_image_generation(user_id, prompt, generator, task_id)) - - return JsonResponse({"status": "processing", "task_id": task_id}) - else: - return JsonResponse({"status": "error", "errors": form.errors}, status=400) - except json.JSONDecodeError: - return JsonResponse({"status": "error", "message": "Invalid JSON"}, status=400) - except Exception as e: - return JsonResponse({"status": "error", "message": str(e)}, status=500) - -async def process_image_generation(user_id, prompt, generator, task_id): - # Implementation of the image generation process - # You'll need to use sync_to_async for database operations here as well - user = await sync_to_async(lambda: User.objects.get(id=user_id))() - prompt_obj, _ = await create_or_get_prompt(prompt, user) - producer_version = await get_producer_version(generator) - - ideogram_service = IdeogramService(settings.IDEOGRAM_API_KEY, max_concurrency=1) - prompt_details = {"prompt": prompt, "ideogram_details": {}} - - result = await ideogram_service.process_prompt_async(prompt_details, {"ideogram_request_count": 0}) - - if result["is_success"]: - image_generation = await create_image_generation( - user, - prompt_obj, - producer_version, - result.get("prompt_details", {}), - result["url"], - result - ) - - image_path = await download_and_save_image(result["url"], image_generation.id) - - await create_image_save( - image_generation, - await sync_to_async(ImageSaveType.objects.get)(Name=ImageSaveType.ImageSaveTypeChoices.RAW), - image_path - ) - - # Store the result somewhere (e.g., cache or database) associated with the task_id - # For simplicity, let's assume we have a function to do this - await store_task_result(task_id, {"status": "success", "image_id": image_generation.id}) - else: - await store_task_result(task_id, {"status": "error", "message": result.get("error_message", "Unknown error")}) - -@require_http_methods(["GET"]) -def check_image_status(request: HttpRequest) -> JsonResponse: - task_id = request.GET.get('task_id') - if not task_id: - return JsonResponse({"status": "error", "message": "No task ID provided"}, status=400) - - # Retrieve the task result (implement this function based on your storage method) - result = get_task_result(task_id) - - if result is None: - return JsonResponse({"status": "processing"}) - else: - return JsonResponse(result) - -def list_images(request: HttpRequest) -> HttpResponse: - images = ImageGeneration.objects.all().order_by('-created') - return render(request, 'list_images.html', {'images': images}) - -def image_details(request: HttpRequest, image_id: int) -> HttpResponse: - image = get_object_or_404(ImageGeneration, id=image_id) - return render(request, 'image_details.html', {'image': image}) - -def generate_image(request: HttpRequest) -> HttpResponse: - if request.method == 'POST': - import ipdb; ipdb.set_trace() - form = ImageGenerationForm(request.POST) - if form.is_valid(): - prompt = form.cleaned_data['prompt'] - generator = form.cleaned_data['generator'] - - prompt_obj, _ = Prompt.objects.get_or_create(Text=prompt, Creator=request.user) - producer_version = ImageProducerVersion.objects.filter(ImageProducerType__Name=generator).first() - - ideogram_service = IdeogramService(settings.IDEOGRAM_API_KEY, max_concurrency=1) - prompt_details = {"prompt": prompt, "ideogram_details": {}} - - result = ideogram_service.process_prompt(prompt_details, {"ideogram_request_count": 0}) - - if result["is_success"]: - image_generation = ImageGeneration.objects.create( - User=request.user, - Prompt=prompt_obj, - ImageProducerVersion=producer_version, - Details=result.get("prompt_details", {}), - URI=result["url"], - Result=result - ) - - image_path = download_and_save_image(result["url"], image_generation.id) - - ImageSave.objects.create( - ImageGeneration=image_generation, - ImageSaveType=ImageSaveType.objects.get(Name=ImageSaveType.ImageSaveTypeChoices.RAW), - FilePath=image_path - ) - - messages.success(request, 'Image generated successfully.') - return redirect('image_details', image_id=image_generation.id) - else: - messages.error(request, f'Image generation failed: {result.get("error_message", "Unknown error")}') - else: - messages.error(request, 'Invalid form submission.') - else: - form = ImageGenerationForm() - - return render(request, 'generate_image.html', {'form': form}) - -def download_and_save_image(url: str, image_id: int) -> str: - response = requests.get(url) - if response.status_code == 200: - file_name = f"generated_image_{image_id}.png" - file_path = os.path.join(settings.MEDIA_ROOT, 'generated_images', file_name) - os.makedirs(os.path.dirname(file_path), exist_ok=True) - with open(file_path, 'wb') as f: - f.write(response.content) - return file_path - else: - raise Exception(f"Failed to download image: HTTP {response.status_code}") - -def store_task_result(task_id: str, result: dict) -> None: - cache.set(f"task_result_{task_id}", result, timeout=3600) # Stores the result for 1 hour - -def get_task_result(task_id: str) -> dict: - return cache.get(f"task_result_{task_id}") +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib import messages +from django.http import HttpRequest, HttpResponse, JsonResponse +from typing import Any +from .models import Prompt, ImageGeneration, ImageProducerType, ImageProducerVersion, ImageSave, ImageSaveType +from .forms import ImageGenerationForm +from ideogram.ideogramClient import IdeogramService +from django.conf import settings +import os +import aiohttp +import asyncio +from asgiref.sync import sync_to_async, async_to_sync +import requests +import json +import time +from django.views.decorators.http import require_http_methods +from django.views.decorators.csrf import csrf_protect +from django.utils.decorators import method_decorator +from django.core.cache import cache + +@sync_to_async +def create_or_get_prompt(text, user): + return Prompt.objects.get_or_create(Text=text, Creator=user) + +@sync_to_async +def get_producer_version(generator): + return get_object_or_404(ImageProducerVersion, ImageProducerType__Name=generator) + +@sync_to_async +def create_image_generation(user, prompt, producer_version, details, uri, result): + return ImageGeneration.objects.create( + User=user, + Prompt=prompt, + ImageProducerVersion=producer_version, + Details=details, + URI=uri, + Result=result + ) + +@sync_to_async +def create_image_save(image_generation, image_save_type, file_path): + return ImageSave.objects.create( + ImageGeneration=image_generation, + ImageSaveType=image_save_type, + FilePath=file_path + ) + +@csrf_protect +@require_http_methods(["POST"]) +async def generate_image_async(request: HttpRequest) -> JsonResponse: + import ipdb; ipdb.set_trace() + try: + data = json.loads(request.body) + form = ImageGenerationForm(data) + if form.is_valid(): + prompt = form.cleaned_data['prompt'] + generator = form.cleaned_data['generator'] + + # Get user ID asynchronously + user_id = await sync_to_async(lambda: request.user.id)() + + # Create a task ID + task_id = f"{user_id}_{int(time.time())}" + + # Start the image generation process asynchronously + asyncio.create_task(process_image_generation(user_id, prompt, generator, task_id)) + + return JsonResponse({"status": "processing", "task_id": task_id}) + else: + return JsonResponse({"status": "error", "errors": form.errors}, status=400) + except json.JSONDecodeError: + return JsonResponse({"status": "error", "message": "Invalid JSON"}, status=400) + except Exception as e: + return JsonResponse({"status": "error", "message": str(e)}, status=500) + +async def process_image_generation(user_id, prompt, generator, task_id): + # Implementation of the image generation process + # You'll need to use sync_to_async for database operations here as well + user = await sync_to_async(lambda: User.objects.get(id=user_id))() + prompt_obj, _ = await create_or_get_prompt(prompt, user) + producer_version = await get_producer_version(generator) + + ideogram_service = IdeogramService(settings.IDEOGRAM_API_KEY, max_concurrency=1) + prompt_details = {"prompt": prompt, "ideogram_details": {}} + + result = await ideogram_service.process_prompt_async(prompt_details, {"ideogram_request_count": 0}) + + if result["is_success"]: + image_generation = await create_image_generation( + user, + prompt_obj, + producer_version, + result.get("prompt_details", {}), + result["url"], + result + ) + + image_path = await download_and_save_image(result["url"], image_generation.id) + + await create_image_save( + image_generation, + await sync_to_async(ImageSaveType.objects.get)(Name=ImageSaveType.ImageSaveTypeChoices.RAW), + image_path + ) + + # Store the result somewhere (e.g., cache or database) associated with the task_id + # For simplicity, let's assume we have a function to do this + await store_task_result(task_id, {"status": "success", "image_id": image_generation.id}) + else: + await store_task_result(task_id, {"status": "error", "message": result.get("error_message", "Unknown error")}) + +@require_http_methods(["GET"]) +def check_image_status(request: HttpRequest) -> JsonResponse: + task_id = request.GET.get('task_id') + if not task_id: + return JsonResponse({"status": "error", "message": "No task ID provided"}, status=400) + + # Retrieve the task result (implement this function based on your storage method) + result = get_task_result(task_id) + + if result is None: + return JsonResponse({"status": "processing"}) + else: + return JsonResponse(result) + +def list_images(request: HttpRequest) -> HttpResponse: + images = ImageGeneration.objects.all().order_by('-created') + return render(request, 'list_images.html', {'images': images}) + +def image_details(request: HttpRequest, image_id: int) -> HttpResponse: + image = get_object_or_404(ImageGeneration, id=image_id) + return render(request, 'image_details.html', {'image': image}) + +def generate_image(request: HttpRequest) -> HttpResponse: + if request.method == 'POST': + import ipdb; ipdb.set_trace() + form = ImageGenerationForm(request.POST) + if form.is_valid(): + prompt = form.cleaned_data['prompt'] + generator = form.cleaned_data['generator'] + + prompt_obj, _ = Prompt.objects.get_or_create(Text=prompt, Creator=request.user) + producer_version = ImageProducerVersion.objects.filter(ImageProducerType__Name=generator).first() + + ideogram_service = IdeogramService(settings.IDEOGRAM_API_KEY, max_concurrency=1) + prompt_details = {"prompt": prompt, "ideogram_details": {}} + + result = ideogram_service.process_prompt(prompt_details, {"ideogram_request_count": 0}) + + if result["is_success"]: + image_generation = ImageGeneration.objects.create( + User=request.user, + Prompt=prompt_obj, + ImageProducerVersion=producer_version, + Details=result.get("prompt_details", {}), + URI=result["url"], + Result=result + ) + + image_path = download_and_save_image(result["url"], image_generation.id) + + ImageSave.objects.create( + ImageGeneration=image_generation, + ImageSaveType=ImageSaveType.objects.get(Name=ImageSaveType.ImageSaveTypeChoices.RAW), + FilePath=image_path + ) + + messages.success(request, 'Image generated successfully.') + return redirect('image_details', image_id=image_generation.id) + else: + messages.error(request, f'Image generation failed: {result.get("error_message", "Unknown error")}') + else: + messages.error(request, 'Invalid form submission.') + else: + form = ImageGenerationForm() + + return render(request, 'generate_image.html', {'form': form}) + +def download_and_save_image(url: str, image_id: int) -> str: + response = requests.get(url) + if response.status_code == 200: + file_name = f"generated_image_{image_id}.png" + file_path = os.path.join(settings.MEDIA_ROOT, 'generated_images', file_name) + os.makedirs(os.path.dirname(file_path), exist_ok=True) + with open(file_path, 'wb') as f: + f.write(response.content) + return file_path + else: + raise Exception(f"Failed to download image: HTTP {response.status_code}") + +def store_task_result(task_id: str, result: dict) -> None: + cache.set(f"task_result_{task_id}", result, timeout=3600) # Stores the result for 1 hour + +def get_task_result(task_id: str) -> dict: + return cache.get(f"task_result_{task_id}") diff --git a/djangoManager/imageMaker/images/migrations/0001_initial.py b/djangoManager/imageMaker/images/migrations/0001_initial.py index be2ffb4..c631c38 100644 --- a/djangoManager/imageMaker/images/migrations/0001_initial.py +++ b/djangoManager/imageMaker/images/migrations/0001_initial.py @@ -1,170 +1,170 @@ -# Generated by Django 5.1.2 on 2024-10-11 23:09 - -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='CreationType', - fields=[ - ('created', models.DateTimeField(default=django.utils.timezone.now)), - ('updated', models.DateTimeField(auto_now=True)), - ('id', models.AutoField(primary_key=True, serialize=False)), - ('Name', models.CharField(choices=[('User Typed', 'User Typed'), ('Image Combination', 'Image Combination'), ('Text Combination', 'Text Combination'), ('Text Rewrite', 'Text Rewrite')], default='User Typed', max_length=20)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='ImageProducer', - fields=[ - ('created', models.DateTimeField(default=django.utils.timezone.now)), - ('updated', models.DateTimeField(auto_now=True)), - ('id', models.AutoField(primary_key=True, serialize=False)), - ('Name', models.CharField(choices=[('Midjourney', 'Midjourney'), ('De3', 'De3'), ('BFL', 'BFL'), ('Ideogram', 'Ideogram')], default='BFL', max_length=20)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='ImageSaveType', - fields=[ - ('created', models.DateTimeField(default=django.utils.timezone.now)), - ('updated', models.DateTimeField(auto_now=True)), - ('id', models.AutoField(primary_key=True, serialize=False)), - ('Name', models.CharField(choices=[('Raw', 'Raw'), ('Full Annotation', 'Full Annotation'), ('Initial Idea', 'Initial Idea'), ('Final Prompt', 'Final Prompt')], default='Raw', max_length=20)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Rewriter', - fields=[ - ('created', models.DateTimeField(default=django.utils.timezone.now)), - ('updated', models.DateTimeField(auto_now=True)), - ('id', models.AutoField(primary_key=True, serialize=False)), - ('Name', models.CharField(choices=[('Claude', 'Claude')], default='Claude', max_length=20)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Tag', - fields=[ - ('created', models.DateTimeField(default=django.utils.timezone.now)), - ('updated', models.DateTimeField(auto_now=True)), - ('id', models.AutoField(primary_key=True, serialize=False)), - ('Name', models.CharField(max_length=255)), - ('Hidden', models.BooleanField(default=False)), - ('HideContainingPrompts', models.BooleanField(default=False)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='ImageProducerTraits', - fields=[ - ('created', models.DateTimeField(default=django.utils.timezone.now)), - ('updated', models.DateTimeField(auto_now=True)), - ('id', models.AutoField(primary_key=True, serialize=False)), - ('SaveExtension', models.CharField(max_length=255)), - ('ImageProducer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='images.imageproducer')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='ImageProducerVersion', - fields=[ - ('created', models.DateTimeField(default=django.utils.timezone.now)), - ('updated', models.DateTimeField(auto_now=True)), - ('id', models.AutoField(primary_key=True, serialize=False)), - ('Version', models.CharField(max_length=255)), - ('ImageProducer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='images.imageproducer')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='ImageGeneration', - fields=[ - ('created', models.DateTimeField(default=django.utils.timezone.now)), - ('updated', models.DateTimeField(auto_now=True)), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('Details', models.JSONField(default=dict)), - ('URI', models.CharField(max_length=1024)), - ('Result', models.JSONField(default=dict)), - ('User', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('ImageProducerVersion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='images.imageproducerversion')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='ImageSave', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(default=django.utils.timezone.now)), - ('updated', models.DateTimeField(auto_now=True)), - ('FilePath', models.CharField(max_length=1024)), - ('ImageGeneration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='image_save', to='images.imagegeneration')), - ('ImageSaveType', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='images.imagesavetype')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Prompt', - fields=[ - ('created', models.DateTimeField(default=django.utils.timezone.now)), - ('updated', models.DateTimeField(auto_now=True)), - ('id', models.AutoField(primary_key=True, serialize=False)), - ('Text', models.TextField()), - ('CreationTypeData', models.JSONField(default=dict, null=True)), - ('CreationType', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='images.creationtype')), - ('Creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('Tags', models.ManyToManyField(related_name='prompts', to='images.tag')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='imagegeneration', - name='Prompt', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='image_generations', to='images.prompt'), - ), - migrations.CreateModel( - name='PromptTag', - fields=[ - ('created', models.DateTimeField(default=django.utils.timezone.now)), - ('updated', models.DateTimeField(auto_now=True)), - ('id', models.AutoField(primary_key=True, serialize=False)), - ('Prompt', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='prompttags', to='images.prompt')), - ('Tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='prompttags', to='images.tag')), - ], - options={ - 'abstract': False, - }, - ), - ] +# Generated by Django 5.1.2 on 2024-10-11 23:09 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='CreationType', + fields=[ + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('updated', models.DateTimeField(auto_now=True)), + ('id', models.AutoField(primary_key=True, serialize=False)), + ('Name', models.CharField(choices=[('User Typed', 'User Typed'), ('Image Combination', 'Image Combination'), ('Text Combination', 'Text Combination'), ('Text Rewrite', 'Text Rewrite')], default='User Typed', max_length=20)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ImageProducer', + fields=[ + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('updated', models.DateTimeField(auto_now=True)), + ('id', models.AutoField(primary_key=True, serialize=False)), + ('Name', models.CharField(choices=[('Midjourney', 'Midjourney'), ('De3', 'De3'), ('BFL', 'BFL'), ('Ideogram', 'Ideogram')], default='BFL', max_length=20)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ImageSaveType', + fields=[ + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('updated', models.DateTimeField(auto_now=True)), + ('id', models.AutoField(primary_key=True, serialize=False)), + ('Name', models.CharField(choices=[('Raw', 'Raw'), ('Full Annotation', 'Full Annotation'), ('Initial Idea', 'Initial Idea'), ('Final Prompt', 'Final Prompt')], default='Raw', max_length=20)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Rewriter', + fields=[ + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('updated', models.DateTimeField(auto_now=True)), + ('id', models.AutoField(primary_key=True, serialize=False)), + ('Name', models.CharField(choices=[('Claude', 'Claude')], default='Claude', max_length=20)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('updated', models.DateTimeField(auto_now=True)), + ('id', models.AutoField(primary_key=True, serialize=False)), + ('Name', models.CharField(max_length=255)), + ('Hidden', models.BooleanField(default=False)), + ('HideContainingPrompts', models.BooleanField(default=False)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ImageProducerTraits', + fields=[ + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('updated', models.DateTimeField(auto_now=True)), + ('id', models.AutoField(primary_key=True, serialize=False)), + ('SaveExtension', models.CharField(max_length=255)), + ('ImageProducer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='images.imageproducer')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ImageProducerVersion', + fields=[ + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('updated', models.DateTimeField(auto_now=True)), + ('id', models.AutoField(primary_key=True, serialize=False)), + ('Version', models.CharField(max_length=255)), + ('ImageProducer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='images.imageproducer')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ImageGeneration', + fields=[ + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('updated', models.DateTimeField(auto_now=True)), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('Details', models.JSONField(default=dict)), + ('URI', models.CharField(max_length=1024)), + ('Result', models.JSONField(default=dict)), + ('User', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('ImageProducerVersion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='images.imageproducerversion')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ImageSave', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('updated', models.DateTimeField(auto_now=True)), + ('FilePath', models.CharField(max_length=1024)), + ('ImageGeneration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='image_save', to='images.imagegeneration')), + ('ImageSaveType', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='images.imagesavetype')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Prompt', + fields=[ + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('updated', models.DateTimeField(auto_now=True)), + ('id', models.AutoField(primary_key=True, serialize=False)), + ('Text', models.TextField()), + ('CreationTypeData', models.JSONField(default=dict, null=True)), + ('CreationType', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='images.creationtype')), + ('Creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('Tags', models.ManyToManyField(related_name='prompts', to='images.tag')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='imagegeneration', + name='Prompt', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='image_generations', to='images.prompt'), + ), + migrations.CreateModel( + name='PromptTag', + fields=[ + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('updated', models.DateTimeField(auto_now=True)), + ('id', models.AutoField(primary_key=True, serialize=False)), + ('Prompt', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='prompttags', to='images.prompt')), + ('Tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='prompttags', to='images.tag')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/djangoManager/imageMaker/images/models.py b/djangoManager/imageMaker/images/models.py index 24aeecd..b55fb76 100644 --- a/djangoManager/imageMaker/images/models.py +++ b/djangoManager/imageMaker/images/models.py @@ -1,182 +1,182 @@ -from django.db import models -from django.contrib.auth.models import User -from django.utils import timezone -from typing import Optional, Dict, Any - -#Rules: -# never remove this rules comment -# we don't use i18n now, thanks -# we use uppercase for field names -# don't abbreviate items in an enum class. - -class BaseModel(models.Model): - created = models.DateTimeField(default=timezone.now) - updated = models.DateTimeField(auto_now=True) - - def clink(self, text: Optional[str] = None, wrap: bool = True, skip_btn: bool = False, klasses: Optional[list[str]] = None, tooltip: Optional[str] = None) -> str: - if skip_btn: - klass = "" - else: - klass = "btn btn-default" - if klasses: - klass += " ".join(klasses) - if wrap: - wrap = "" - else: - wrap = " nb" - if not text: - text = self - if not tooltip: - tooltip = "" - - return '%s' % (klass, wrap, tooltip, 'images', self.__class__.__name__.lower(), self.id, text) - - class Meta: - app_label = 'images' - abstract = True - -class Prompt(BaseModel): - id = models.AutoField(primary_key=True) - Text = models.TextField() - Creator = models.ForeignKey(User, on_delete=models.CASCADE) - CreationType = models.ForeignKey('CreationType', on_delete=models.CASCADE, null=True) - CreationTypeData = models.JSONField(default=dict, null=True, blank=True) - #for every creationType there will be a format for the creation type data which we can deserialize. - - def __str__(self) -> str: - return self.Text - - -class CreationType(BaseModel): - class CreationTypeChoices(models.TextChoices): - USER_TYPED = 'User Typed', 'User Typed' - IMAGE_COMBINATION = 'Image Combination', 'Image Combination' - TEXT_COMBINATION = 'Text Combination', 'Text Combination' - TEXT_REWRITE = 'Text Rewrite', 'Text Rewrite' - LOADED_FROM_FILE = "Loaded From File", "Loaded From File" - - id = models.AutoField(primary_key=True) - Name = models.CharField( - max_length=20, - choices=CreationTypeChoices.choices, - default=CreationTypeChoices.USER_TYPED - ) - - def __str__(self): - return self.Name - - -class Rewriter(BaseModel): - class RewriterChoices(models.TextChoices): - CLAUDE = 'Claude', 'Claude' - LLAMA = 'Llama', 'Llama' - - id = models.AutoField(primary_key=True) - Name = models.CharField( - max_length=20, - choices=RewriterChoices.choices, - default=RewriterChoices.CLAUDE - ) - -class PromptTag(BaseModel): - id = models.AutoField(primary_key=True) - Prompt = models.ForeignKey(Prompt, related_name="prompttags", on_delete=models.CASCADE) - Tag = models.ForeignKey('Tag', related_name="prompttags", on_delete=models.CASCADE) - - def __str__(self): - return f"{self.Tag}" - -class Tag(BaseModel): - id = models.AutoField(primary_key=True) - Name = models.CharField(max_length=255) - Hidden = models.BooleanField(default=False) - HideContainingPrompts = models.BooleanField(default=False) - - def __str__(self): - return self.Name - -#user generated an image -class ImageGeneration(BaseModel): - id = models.BigAutoField(primary_key=True) - User = models.ForeignKey(User, on_delete=models.CASCADE) - Prompt = models.ForeignKey(Prompt, related_name="image_generations", on_delete=models.CASCADE) - - ImageProducerVersion = models.ForeignKey('ImageProducerVersion', on_delete=models.CASCADE) - Details = models.JSONField(default=dict, blank=True) - #this will be deserialized based on the imageProducerVersion - #this is details of the generation parameters etc. like resolution - - URI = models.CharField(max_length=1024) - Result = models.JSONField(default=dict, blank=True) - - def __str__(self): - return f"Gen {self.Prompt.Text[:20]} by {self.User.username} on {self.ImageProducerVersion.ImageProducerType.Name} {self.ImageProducerVersion.Version}" - -#okay so the way requests from all image producers except BFL are async is annoying -# whereas BFL just has two steps - 1. upload the request (sync) then an endpoing 2. to check if its done yet. -# so this class holds the request before it can be finally saved into an ImageGeneration -class BFLRequest(BaseModel): - id = models.AutoField(primary_key=True) - Prompt = models.ForeignKey(Prompt, on_delete=models.CASCADE) - Status = models.BooleanField(default=False) - User = models.ForeignKey(User, on_delete=models.CASCADE) - -# raw or the various types of annotations for saved images. -class ImageSaveType(BaseModel): - class ImageSaveTypeChoices(models.TextChoices): - RAW = 'Raw', 'Raw' - FULL_ANNOTATION = 'Full Annotation', 'Full Annotation' - INITIAL_IDEA = 'Initial Idea', 'Initial Idea' - FINAL_PROMPT = 'Final Prompt', 'Final Prompt' - - id = models.AutoField(primary_key=True) - Name = models.CharField( - max_length=20, - choices=ImageSaveTypeChoices.choices, - default=ImageSaveTypeChoices.RAW - ) - - def __str__(self): - return self.Name - -# we saved a version of an image -class ImageSave(BaseModel): - ImageGeneration = models.ForeignKey(ImageGeneration, related_name='image_saves', on_delete=models.CASCADE) - ImageSaveType = models.ForeignKey(ImageSaveType, on_delete=models.CASCADE) - FilePath = models.CharField(max_length=1024) - -class ImageProducerType(BaseModel): - class ImageProducerTypeChoices(models.TextChoices): - MIDJOURNEY = 'Midjourney', 'Midjourney' - DE3 = 'De3', 'De3' - BFL = 'BFL', 'BFL' - IDEOGRAM = 'Ideogram', 'Ideogram' - - id = models.AutoField(primary_key=True) - Name = models.CharField( - max_length=20, - choices=ImageProducerTypeChoices.choices, - default=ImageProducerTypeChoices.BFL - ) - - def __str__(self): - return self.Name - -#details of a specific image producer -class ImageProducerTraits(BaseModel): - id = models.AutoField(primary_key=True) - ImageProducerType = models.ForeignKey('ImageProducerType', on_delete=models.CASCADE) - SaveExtension = models.CharField(max_length=255) - - def __str__(self): - return f"{self.ImageProducerType} {self.SaveExtension}" - -#an api, e.g. -#A version label of a producer, like ideogram2? -class ImageProducerVersion(BaseModel): - id = models.AutoField(primary_key=True) - ImageProducerType = models.ForeignKey(ImageProducerType, on_delete=models.CASCADE) - Version = models.CharField(max_length=255) - - def __str__(self): - return f"{self.ImageProducerType} {self.Version}" +from django.db import models +from django.contrib.auth.models import User +from django.utils import timezone +from typing import Optional, Dict, Any + +#Rules: +# never remove this rules comment +# we don't use i18n now, thanks +# we use uppercase for field names +# don't abbreviate items in an enum class. + +class BaseModel(models.Model): + created = models.DateTimeField(default=timezone.now) + updated = models.DateTimeField(auto_now=True) + + def clink(self, text: Optional[str] = None, wrap: bool = True, skip_btn: bool = False, klasses: Optional[list[str]] = None, tooltip: Optional[str] = None) -> str: + if skip_btn: + klass = "" + else: + klass = "btn btn-default" + if klasses: + klass += " ".join(klasses) + if wrap: + wrap = "" + else: + wrap = " nb" + if not text: + text = self + if not tooltip: + tooltip = "" + + return '%s' % (klass, wrap, tooltip, 'images', self.__class__.__name__.lower(), self.id, text) + + class Meta: + app_label = 'images' + abstract = True + +class Prompt(BaseModel): + id = models.AutoField(primary_key=True) + Text = models.TextField() + Creator = models.ForeignKey(User, on_delete=models.CASCADE) + CreationType = models.ForeignKey('CreationType', on_delete=models.CASCADE, null=True) + CreationTypeData = models.JSONField(default=dict, null=True, blank=True) + #for every creationType there will be a format for the creation type data which we can deserialize. + + def __str__(self) -> str: + return self.Text + + +class CreationType(BaseModel): + class CreationTypeChoices(models.TextChoices): + USER_TYPED = 'User Typed', 'User Typed' + IMAGE_COMBINATION = 'Image Combination', 'Image Combination' + TEXT_COMBINATION = 'Text Combination', 'Text Combination' + TEXT_REWRITE = 'Text Rewrite', 'Text Rewrite' + LOADED_FROM_FILE = "Loaded From File", "Loaded From File" + + id = models.AutoField(primary_key=True) + Name = models.CharField( + max_length=20, + choices=CreationTypeChoices.choices, + default=CreationTypeChoices.USER_TYPED + ) + + def __str__(self): + return self.Name + + +class Rewriter(BaseModel): + class RewriterChoices(models.TextChoices): + CLAUDE = 'Claude', 'Claude' + LLAMA = 'Llama', 'Llama' + + id = models.AutoField(primary_key=True) + Name = models.CharField( + max_length=20, + choices=RewriterChoices.choices, + default=RewriterChoices.CLAUDE + ) + +class PromptTag(BaseModel): + id = models.AutoField(primary_key=True) + Prompt = models.ForeignKey(Prompt, related_name="prompttags", on_delete=models.CASCADE) + Tag = models.ForeignKey('Tag', related_name="prompttags", on_delete=models.CASCADE) + + def __str__(self): + return f"{self.Tag}" + +class Tag(BaseModel): + id = models.AutoField(primary_key=True) + Name = models.CharField(max_length=255) + Hidden = models.BooleanField(default=False) + HideContainingPrompts = models.BooleanField(default=False) + + def __str__(self): + return self.Name + +#user generated an image +class ImageGeneration(BaseModel): + id = models.BigAutoField(primary_key=True) + User = models.ForeignKey(User, on_delete=models.CASCADE) + Prompt = models.ForeignKey(Prompt, related_name="image_generations", on_delete=models.CASCADE) + + ImageProducerVersion = models.ForeignKey('ImageProducerVersion', on_delete=models.CASCADE) + Details = models.JSONField(default=dict, blank=True) + #this will be deserialized based on the imageProducerVersion + #this is details of the generation parameters etc. like resolution + + URI = models.CharField(max_length=1024) + Result = models.JSONField(default=dict, blank=True) + + def __str__(self): + return f"Gen {self.Prompt.Text[:20]} by {self.User.username} on {self.ImageProducerVersion.ImageProducerType.Name} {self.ImageProducerVersion.Version}" + +#okay so the way requests from all image producers except BFL are async is annoying +# whereas BFL just has two steps - 1. upload the request (sync) then an endpoing 2. to check if its done yet. +# so this class holds the request before it can be finally saved into an ImageGeneration +class BFLRequest(BaseModel): + id = models.AutoField(primary_key=True) + Prompt = models.ForeignKey(Prompt, on_delete=models.CASCADE) + Status = models.BooleanField(default=False) + User = models.ForeignKey(User, on_delete=models.CASCADE) + +# raw or the various types of annotations for saved images. +class ImageSaveType(BaseModel): + class ImageSaveTypeChoices(models.TextChoices): + RAW = 'Raw', 'Raw' + FULL_ANNOTATION = 'Full Annotation', 'Full Annotation' + INITIAL_IDEA = 'Initial Idea', 'Initial Idea' + FINAL_PROMPT = 'Final Prompt', 'Final Prompt' + + id = models.AutoField(primary_key=True) + Name = models.CharField( + max_length=20, + choices=ImageSaveTypeChoices.choices, + default=ImageSaveTypeChoices.RAW + ) + + def __str__(self): + return self.Name + +# we saved a version of an image +class ImageSave(BaseModel): + ImageGeneration = models.ForeignKey(ImageGeneration, related_name='image_saves', on_delete=models.CASCADE) + ImageSaveType = models.ForeignKey(ImageSaveType, on_delete=models.CASCADE) + FilePath = models.CharField(max_length=1024) + +class ImageProducerType(BaseModel): + class ImageProducerTypeChoices(models.TextChoices): + MIDJOURNEY = 'Midjourney', 'Midjourney' + DE3 = 'De3', 'De3' + BFL = 'BFL', 'BFL' + IDEOGRAM = 'Ideogram', 'Ideogram' + + id = models.AutoField(primary_key=True) + Name = models.CharField( + max_length=20, + choices=ImageProducerTypeChoices.choices, + default=ImageProducerTypeChoices.BFL + ) + + def __str__(self): + return self.Name + +#details of a specific image producer +class ImageProducerTraits(BaseModel): + id = models.AutoField(primary_key=True) + ImageProducerType = models.ForeignKey('ImageProducerType', on_delete=models.CASCADE) + SaveExtension = models.CharField(max_length=255) + + def __str__(self): + return f"{self.ImageProducerType} {self.SaveExtension}" + +#an api, e.g. +#A version label of a producer, like ideogram2? +class ImageProducerVersion(BaseModel): + id = models.AutoField(primary_key=True) + ImageProducerType = models.ForeignKey(ImageProducerType, on_delete=models.CASCADE) + Version = models.CharField(max_length=255) + + def __str__(self): + return f"{self.ImageProducerType} {self.Version}" diff --git a/djangoManager/imageMaker/images/settings_loader.py b/djangoManager/imageMaker/images/settings_loader.py index 89661cc..cd3892c 100644 --- a/djangoManager/imageMaker/images/settings_loader.py +++ b/djangoManager/imageMaker/images/settings_loader.py @@ -1,45 +1,45 @@ -import json -import os -from typing import Dict, Any - -class SettingsLoader: - def __init__(self, settings_dict: Dict[str, Any]): - self.image_download_base_folder = settings_dict.get('ImageDownloadBaseFolder', '') - self.save_json_log = settings_dict.get('SaveJsonLog', False) - self.enable_logging = settings_dict.get('EnableLogging', False) - self.annotation_side = settings_dict.get('AnnotationSide', '') - self.bfl_api_key = settings_dict.get('BFLApiKey', '') - self.ideogram_api_key = settings_dict.get('IdeogramApiKey', '') - self.openai_api_key = settings_dict.get('OpenAIApiKey', '') - self.anthropic_api_key = settings_dict.get('AnthropicApiKey', '') - - @classmethod - @classmethod - def load_from_file(cls, file_path: str) -> 'SettingsLoader': - if not os.path.exists(file_path): - print(os.getcwd()) - raise FileNotFoundError(f"Settings file not found: {file_path}") - - with open(file_path, 'r', encoding='utf-8-sig') as f: - settings_dict = json.load(f) - - return cls(settings_dict) - - def __str__(self): - return ( - f"Current settings:\n" - f"Image Download Base:\t{self.image_download_base_folder}\n" - f"Save JSON Log:\t{self.save_json_log}\n" - f"Enable Logging:\t\t{self.enable_logging}\n" - f"Annotation Side:\t{self.annotation_side}" - ) - -def load_settings(settings_file_path: str = '../../MultiImageClient/settings.json') -> SettingsLoader: - try: - settings = SettingsLoader.load_from_file(settings_file_path) - print(settings) - return settings - except Exception as e: - print(f"Error loading settings: {e}") - return None - +import json +import os +from typing import Dict, Any + +class SettingsLoader: + def __init__(self, settings_dict: Dict[str, Any]): + self.image_download_base_folder = settings_dict.get('ImageDownloadBaseFolder', '') + self.save_json_log = settings_dict.get('SaveJsonLog', False) + self.enable_logging = settings_dict.get('EnableLogging', False) + self.annotation_side = settings_dict.get('AnnotationSide', '') + self.bfl_api_key = settings_dict.get('BFLApiKey', '') + self.ideogram_api_key = settings_dict.get('IdeogramApiKey', '') + self.openai_api_key = settings_dict.get('OpenAIApiKey', '') + self.anthropic_api_key = settings_dict.get('AnthropicApiKey', '') + + @classmethod + @classmethod + def load_from_file(cls, file_path: str) -> 'SettingsLoader': + if not os.path.exists(file_path): + print(os.getcwd()) + raise FileNotFoundError(f"Settings file not found: {file_path}") + + with open(file_path, 'r', encoding='utf-8-sig') as f: + settings_dict = json.load(f) + + return cls(settings_dict) + + def __str__(self): + return ( + f"Current settings:\n" + f"Image Download Base:\t{self.image_download_base_folder}\n" + f"Save JSON Log:\t{self.save_json_log}\n" + f"Enable Logging:\t\t{self.enable_logging}\n" + f"Annotation Side:\t{self.annotation_side}" + ) + +def load_settings(settings_file_path: str = '../../MultiImageClient/settings.json') -> SettingsLoader: + try: + settings = SettingsLoader.load_from_file(settings_file_path) + print(settings) + return settings + except Exception as e: + print(f"Error loading settings: {e}") + return None + diff --git a/djangoManager/imageMaker/images/templates/admin/base_site.html b/djangoManager/imageMaker/images/templates/admin/base_site.html index 6f45a5e..b2f5df5 100644 --- a/djangoManager/imageMaker/images/templates/admin/base_site.html +++ b/djangoManager/imageMaker/images/templates/admin/base_site.html @@ -1,55 +1,55 @@ -{% extends "admin/base.html" %} -{% load i18n %} -{% load static %} - -{% block extrahead %} -{{ block.super }} - - - - - - - -{% block extra_css %}{% endblock %} -{% endblock %} - -{% block footer %} -{{ block.super }} - -{% endblock %} - -{% block bodyend %} -{{ block.super }} - - -{% block extra_js %}{% endblock %} +{% extends "admin/base.html" %} +{% load i18n %} +{% load static %} + +{% block extrahead %} +{{ block.super }} + + + + + + + +{% block extra_css %}{% endblock %} +{% endblock %} + +{% block footer %} +{{ block.super }} + +{% endblock %} + +{% block bodyend %} +{{ block.super }} + + +{% block extra_js %}{% endblock %} {% endblock %} \ No newline at end of file diff --git a/djangoManager/imageMaker/images/templates/base.html b/djangoManager/imageMaker/images/templates/base.html index 686ba1c..1c5da9f 100644 --- a/djangoManager/imageMaker/images/templates/base.html +++ b/djangoManager/imageMaker/images/templates/base.html @@ -1,71 +1,71 @@ -{% load static %} - - - - - - - Image Maker - - - - - - - - - - - -
- -
- -
- {% block content %} - {% endblock %} -
- - - - - - - +{% load static %} + + + + + + + Image Maker + + + + + + + + + + + +
+ +
+ +
+ {% block content %} + {% endblock %} +
+ + + + + + + \ No newline at end of file diff --git a/djangoManager/imageMaker/images/templates/index.html b/djangoManager/imageMaker/images/templates/index.html index 7d64c2c..604bc03 100644 --- a/djangoManager/imageMaker/images/templates/index.html +++ b/djangoManager/imageMaker/images/templates/index.html @@ -1,9 +1,9 @@ -{% extends "base.html" %} - -{% block content %} -
-

Welcome to Image Maker

- - -
+{% extends "base.html" %} + +{% block content %} +
+

Welcome to Image Maker

+ + +
{% endblock %} \ No newline at end of file diff --git a/djangoManager/imageMaker/images/templates/text_input.html b/djangoManager/imageMaker/images/templates/text_input.html index 17260c1..f43ddae 100644 --- a/djangoManager/imageMaker/images/templates/text_input.html +++ b/djangoManager/imageMaker/images/templates/text_input.html @@ -1,25 +1,25 @@ -{% extends "base.html" %} - -{% block content %} -
-

Text Input

-
- {% csrf_token %} -
- - -
- -
- - {% if messages %} -
- {% for message in messages %} -
- {{ message }} -
- {% endfor %} -
- {% endif %} -
+{% extends "base.html" %} + +{% block content %} +
+

Text Input

+
+ {% csrf_token %} +
+ + +
+ +
+ + {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} +
{% endblock %} \ No newline at end of file diff --git a/djangoManager/imageMaker/images/templates/upload_json.html b/djangoManager/imageMaker/images/templates/upload_json.html index d2ea8b4..76a7a5b 100644 --- a/djangoManager/imageMaker/images/templates/upload_json.html +++ b/djangoManager/imageMaker/images/templates/upload_json.html @@ -1,18 +1,18 @@ -{% extends "base.html" %} - -{% block content %} -

Upload JSON File

-
- {% csrf_token %} - - -
- -{% if messages %} -
    - {% for message in messages %} - {{ message }} - {% endfor %} -
-{% endif %} +{% extends "base.html" %} + +{% block content %} +

Upload JSON File

+
+ {% csrf_token %} + + +
+ +{% if messages %} +
    + {% for message in messages %} + {{ message }} + {% endfor %} +
+{% endif %} {% endblock %} \ No newline at end of file diff --git a/djangoManager/imageMaker/images/templates/view_image_dir.html b/djangoManager/imageMaker/images/templates/view_image_dir.html index f70ff97..88d7fbb 100644 --- a/djangoManager/imageMaker/images/templates/view_image_dir.html +++ b/djangoManager/imageMaker/images/templates/view_image_dir.html @@ -1,17 +1,17 @@ -{% extends "base.html" %} -{% load static %} - -{% block content %} -
- -
- - -
-
- - - +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+ +
+ + +
+
+ + + {% endblock %} \ No newline at end of file diff --git a/djangoManager/imageMaker/images/tests.py b/djangoManager/imageMaker/images/tests.py index 7ce503c..de8bdc0 100644 --- a/djangoManager/imageMaker/images/tests.py +++ b/djangoManager/imageMaker/images/tests.py @@ -1,3 +1,3 @@ -from django.test import TestCase - -# Create your tests here. +from django.test import TestCase + +# Create your tests here. diff --git a/djangoManager/imageMaker/images/views.py b/djangoManager/imageMaker/images/views.py index bcb0800..b32d913 100644 --- a/djangoManager/imageMaker/images/views.py +++ b/djangoManager/imageMaker/images/views.py @@ -1,102 +1,102 @@ -import os -from django.conf import settings - -from django.shortcuts import render, redirect -from django.contrib import messages -from django.core.files.storage import default_storage -from django.core.files.base import ContentFile -import json -from .models import * -from typing import Any, List -from django.http import JsonResponse -from django.db.models import Q -from .models import Prompt - -# Create your views here. - -def index(request: Any) -> Any: - links = [ - - ] - return render(request, 'index.html', {}) - - - -def prompt_search(request): - query = request.GET.get('q', '') - page = int(request.GET.get('page', 1)) - per_page = 30 - offset = (page - 1) * per_page - - prompts = Prompt.objects.filter(Q(Text__icontains=query) | Q(id__icontains=query))[offset:offset+per_page] - total_count = Prompt.objects.filter(Q(Text__icontains=query) | Q(id__icontains=query)).count() - - results = [{'id': prompt.id, 'text': f"{prompt.id}: {prompt.Text[:100]}"} for prompt in prompts] - return JsonResponse({ - 'results': results, - 'total_count': total_count, - 'pagination': { - 'more': total_count > (page * per_page) - } - }) - -def upload_json(request): - if request.method == 'POST' and request.FILES['json_file']: - import ipdb;ipdb.set_trace() - json_file = request.FILES['json_file'] - - # Save the file temporarily - path = default_storage.save('tmp/json_upload.json', ContentFile(json_file.read())) - - try: - with default_storage.open(path) as f: - data = json.load(f) - - # Process the JSON data here - # You can access any model and perform operations - # For example: - # new_prompt = Prompt.objects.create(Text=data['prompt_text'], Creator=request.user) - - messages.success(request, 'JSON file uploaded and processed successfully.') - except json.JSONDecodeError: - messages.error(request, 'Invalid JSON file.') - except Exception as e: - messages.error(request, f'An error occurred: {str(e)}') - finally: - # Clean up the temporary file - default_storage.delete(path) - - return redirect('upload_json') - - return render(request, 'upload_json.html') - -def text_input(request: Any) -> Any: - - if request.method == 'POST': - text = request.POST.get('text_content', '') - if text: - # Process the text - processed_text: List[str] = set([el.strip() for el in process_text_input(text)]) - loadedFromFile = CreationType.objects.get(Name=CreationType.CreationTypeChoices.LOADED_FROM_FILE) - for el in processed_text: - prompt, created = Prompt.objects.get_or_create(Text=el, Creator=request.user, CreationType=loadedFromFile) - messages.success(request, 'Text processed successfully.') - else: - messages.error(request, 'No text was provided.') - - return redirect('text_input') - - return render(request, 'text_input.html') - -def process_text_input(text: str) -> List[str]: - # Remove leading/trailing whitespace - text = text.strip() - - # Check if the text is wrapped in quotes - if text.startswith('"') and text.endswith('"'): - # Treat as a single multiline string - return [text[1:-1].strip()] # Remove the quotes and strip again - else: - # Split by lines, strip each line, and filter out empty lines - return [line.strip() for line in text.split('\n') if line.strip()] - +import os +from django.conf import settings + +from django.shortcuts import render, redirect +from django.contrib import messages +from django.core.files.storage import default_storage +from django.core.files.base import ContentFile +import json +from .models import * +from typing import Any, List +from django.http import JsonResponse +from django.db.models import Q +from .models import Prompt + +# Create your views here. + +def index(request: Any) -> Any: + links = [ + + ] + return render(request, 'index.html', {}) + + + +def prompt_search(request): + query = request.GET.get('q', '') + page = int(request.GET.get('page', 1)) + per_page = 30 + offset = (page - 1) * per_page + + prompts = Prompt.objects.filter(Q(Text__icontains=query) | Q(id__icontains=query))[offset:offset+per_page] + total_count = Prompt.objects.filter(Q(Text__icontains=query) | Q(id__icontains=query)).count() + + results = [{'id': prompt.id, 'text': f"{prompt.id}: {prompt.Text[:100]}"} for prompt in prompts] + return JsonResponse({ + 'results': results, + 'total_count': total_count, + 'pagination': { + 'more': total_count > (page * per_page) + } + }) + +def upload_json(request): + if request.method == 'POST' and request.FILES['json_file']: + import ipdb;ipdb.set_trace() + json_file = request.FILES['json_file'] + + # Save the file temporarily + path = default_storage.save('tmp/json_upload.json', ContentFile(json_file.read())) + + try: + with default_storage.open(path) as f: + data = json.load(f) + + # Process the JSON data here + # You can access any model and perform operations + # For example: + # new_prompt = Prompt.objects.create(Text=data['prompt_text'], Creator=request.user) + + messages.success(request, 'JSON file uploaded and processed successfully.') + except json.JSONDecodeError: + messages.error(request, 'Invalid JSON file.') + except Exception as e: + messages.error(request, f'An error occurred: {str(e)}') + finally: + # Clean up the temporary file + default_storage.delete(path) + + return redirect('upload_json') + + return render(request, 'upload_json.html') + +def text_input(request: Any) -> Any: + + if request.method == 'POST': + text = request.POST.get('text_content', '') + if text: + # Process the text + processed_text: List[str] = set([el.strip() for el in process_text_input(text)]) + loadedFromFile = CreationType.objects.get(Name=CreationType.CreationTypeChoices.LOADED_FROM_FILE) + for el in processed_text: + prompt, created = Prompt.objects.get_or_create(Text=el, Creator=request.user, CreationType=loadedFromFile) + messages.success(request, 'Text processed successfully.') + else: + messages.error(request, 'No text was provided.') + + return redirect('text_input') + + return render(request, 'text_input.html') + +def process_text_input(text: str) -> List[str]: + # Remove leading/trailing whitespace + text = text.strip() + + # Check if the text is wrapped in quotes + if text.startswith('"') and text.endswith('"'): + # Treat as a single multiline string + return [text[1:-1].strip()] # Remove the quotes and strip again + else: + # Split by lines, strip each line, and filter out empty lines + return [line.strip() for line in text.split('\n') if line.strip()] + diff --git a/djangoManager/imageMaker/manage.py b/djangoManager/imageMaker/manage.py index b7c2395..6e71a3f 100644 --- a/djangoManager/imageMaker/manage.py +++ b/djangoManager/imageMaker/manage.py @@ -1,22 +1,22 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'imageMaker.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'imageMaker.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/djangoManager/imageMaker/requirements.txt b/djangoManager/imageMaker/requirements.txt index 67aae66..8db848b 100644 --- a/djangoManager/imageMaker/requirements.txt +++ b/djangoManager/imageMaker/requirements.txt @@ -1,5 +1,5 @@ -django -mysqlclient -requests -ipdb +django +mysqlclient +requests +ipdb aiohttp \ No newline at end of file diff --git a/djangoManager/imageMaker/static/css/custom.css b/djangoManager/imageMaker/static/css/custom.css index 6615822..6050a9b 100644 --- a/djangoManager/imageMaker/static/css/custom.css +++ b/djangoManager/imageMaker/static/css/custom.css @@ -1,43 +1,43 @@ -.fullscreen-viewer { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.9); - z-index: 1000; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.fullscreen-viewer img { - max-width: 90%; - max-height: 80%; - object-fit: contain; - margin-bottom: 20px; - /* Add space between image and controls */ -} - -.controls { - display: flex; - justify-content: center; - align-items: center; - width: 100%; -} - -.controls button { - margin: 0 10px; - padding: 10px 20px; - font-size: 16px; - cursor: pointer; - background-color: #333; - color: #fff; - border: none; - border-radius: 5px; -} - -.controls button:hover { - background-color: #555; +.fullscreen-viewer { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + z-index: 1000; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.fullscreen-viewer img { + max-width: 90%; + max-height: 80%; + object-fit: contain; + margin-bottom: 20px; + /* Add space between image and controls */ +} + +.controls { + display: flex; + justify-content: center; + align-items: center; + width: 100%; +} + +.controls button { + margin: 0 10px; + padding: 10px 20px; + font-size: 16px; + cursor: pointer; + background-color: #333; + color: #fff; + border: none; + border-radius: 5px; +} + +.controls button:hover { + background-color: #555; } \ No newline at end of file diff --git a/djangoManager/imageMaker/static/js/image_viewer.js b/djangoManager/imageMaker/static/js/image_viewer.js index 71a45df..bf35ac1 100644 --- a/djangoManager/imageMaker/static/js/image_viewer.js +++ b/djangoManager/imageMaker/static/js/image_viewer.js @@ -1,60 +1,60 @@ -document.addEventListener('DOMContentLoaded', () => { - const viewer = document.getElementById('fullscreen-viewer'); - const fullscreenImage = document.getElementById('fullscreen-image'); - const prevButton = document.getElementById('prev-button'); - const nextButton = document.getElementById('next-button'); - - let images = []; - let currentIndex = 0; - - function loadImageList() { - fetch(`/get-image-list?dir=${encodeURIComponent(imageDir)}`) - .then(response => response.json()) - .then(data => { - images = data.images; - if (images.length > 0) { - showImage(0); - } - }) - .catch(error => console.error('Error loading image list:', error)); - } - - function showImage(index) { - const imageName = images[index]; - fullscreenImage.src = `/get-image?dir=${encodeURIComponent(imageDir)}&name=${encodeURIComponent(imageName)}`; - currentIndex = index; - } - - function openViewer() { - viewer.style.display = 'flex'; - } - - function closeViewer() { - viewer.style.display = 'none'; - } - - prevButton.addEventListener('click', () => { - currentIndex = (currentIndex - 1 + images.length) % images.length; - showImage(currentIndex); - }); - - nextButton.addEventListener('click', () => { - currentIndex = (currentIndex + 1) % images.length; - showImage(currentIndex); - }); - - document.addEventListener('keydown', (e) => { - if (viewer.style.display === 'flex') { - if (e.key === 'ArrowLeft') { - prevButton.click(); - } else if (e.key === 'ArrowRight') { - nextButton.click(); - } else if (e.key === 'Escape') { - closeViewer(); - } - } - }); - - loadImageList(); - openViewer(); +document.addEventListener('DOMContentLoaded', () => { + const viewer = document.getElementById('fullscreen-viewer'); + const fullscreenImage = document.getElementById('fullscreen-image'); + const prevButton = document.getElementById('prev-button'); + const nextButton = document.getElementById('next-button'); + + let images = []; + let currentIndex = 0; + + function loadImageList() { + fetch(`/get-image-list?dir=${encodeURIComponent(imageDir)}`) + .then(response => response.json()) + .then(data => { + images = data.images; + if (images.length > 0) { + showImage(0); + } + }) + .catch(error => console.error('Error loading image list:', error)); + } + + function showImage(index) { + const imageName = images[index]; + fullscreenImage.src = `/get-image?dir=${encodeURIComponent(imageDir)}&name=${encodeURIComponent(imageName)}`; + currentIndex = index; + } + + function openViewer() { + viewer.style.display = 'flex'; + } + + function closeViewer() { + viewer.style.display = 'none'; + } + + prevButton.addEventListener('click', () => { + currentIndex = (currentIndex - 1 + images.length) % images.length; + showImage(currentIndex); + }); + + nextButton.addEventListener('click', () => { + currentIndex = (currentIndex + 1) % images.length; + showImage(currentIndex); + }); + + document.addEventListener('keydown', (e) => { + if (viewer.style.display === 'flex') { + if (e.key === 'ArrowLeft') { + prevButton.click(); + } else if (e.key === 'ArrowRight') { + nextButton.click(); + } else if (e.key === 'Escape') { + closeViewer(); + } + } + }); + + loadImageList(); + openViewer(); }); \ No newline at end of file diff --git a/do_flask_intern.py b/do_flask_intern.py new file mode 100644 index 0000000..1df311b --- /dev/null +++ b/do_flask_intern.py @@ -0,0 +1,238 @@ +from flask import Flask, request, jsonify +import torch +from transformers import AutoModel, AutoTokenizer +from PIL import Image +import base64 +from io import BytesIO +import torchvision.transforms as T +from torchvision.transforms.functional import InterpolationMode + +app = Flask(__name__) + +print("Loading InternVL3-1B-Pretrained base model... This will take a minute on first run") +model_name = "OpenGVLab/InternVL3-1B-Pretrained" + +# Load model and tokenizer with GPU support +model = AutoModel.from_pretrained( + model_name, + torch_dtype=torch.bfloat16, + device_map="auto", + trust_remote_code=True +) +tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) + +print(f"Model loaded! Using device: {model.device}") +print(f"GPU Memory allocated: {torch.cuda.memory_allocated() / 1024**3:.2f} GB") + +# Image preprocessing functions from InternVL3 documentation +IMAGENET_MEAN = (0.485, 0.456, 0.406) +IMAGENET_STD = (0.229, 0.224, 0.225) + +def build_transform(input_size): + MEAN, STD = IMAGENET_MEAN, IMAGENET_STD + transform = T.Compose([ + T.Lambda(lambda img: img.convert('RGB') if img.mode != 'RGB' else img), + T.Resize((input_size, input_size), interpolation=InterpolationMode.BICUBIC), + T.ToTensor(), + T.Normalize(mean=MEAN, std=STD) + ]) + return transform + +def find_closest_aspect_ratio(aspect_ratio, target_ratios, width, height, image_size): + best_ratio_diff = float('inf') + best_ratio = (1, 1) + area = width * height + for ratio in target_ratios: + target_aspect_ratio = ratio[0] / ratio[1] + ratio_diff = abs(aspect_ratio - target_aspect_ratio) + if ratio_diff < best_ratio_diff: + best_ratio_diff = ratio_diff + best_ratio = ratio + elif ratio_diff == best_ratio_diff: + if area > 0.5 * image_size * image_size * ratio[0] * ratio[1]: + best_ratio = ratio + return best_ratio + +def dynamic_preprocess(image, min_num=1, max_num=12, image_size=448, use_thumbnail=False): + orig_width, orig_height = image.size + aspect_ratio = orig_width / orig_height + + target_ratios = set( + (i, j) for n in range(min_num, max_num + 1) for i in range(1, n + 1) for j in range(1, n + 1) if + i * j <= max_num and i * j >= min_num) + target_ratios = sorted(target_ratios, key=lambda x: x[0] * x[1]) + + target_aspect_ratio = find_closest_aspect_ratio( + aspect_ratio, target_ratios, orig_width, orig_height, image_size) + + target_width = image_size * target_aspect_ratio[0] + target_height = image_size * target_aspect_ratio[1] + blocks = target_aspect_ratio[0] * target_aspect_ratio[1] + + resized_img = image.resize((target_width, target_height)) + processed_images = [] + for i in range(blocks): + box = ( + (i % (target_width // image_size)) * image_size, + (i // (target_width // image_size)) * image_size, + ((i % (target_width // image_size)) + 1) * image_size, + ((i // (target_width // image_size)) + 1) * image_size + ) + split_img = resized_img.crop(box) + processed_images.append(split_img) + assert len(processed_images) == blocks + if use_thumbnail and len(processed_images) != 1: + thumbnail_img = image.resize((image_size, image_size)) + processed_images.append(thumbnail_img) + return processed_images + +def process_image(image, input_size=448, max_num=12): + transform = build_transform(input_size=input_size) + images = dynamic_preprocess(image, image_size=input_size, use_thumbnail=True, max_num=max_num) + pixel_values = [transform(img) for img in images] + pixel_values = torch.stack(pixel_values) + return pixel_values + +@app.route('/generate', methods=['POST']) +def generate(): + """ + Expects JSON: + { + "image": "data:image/png;base64,..." or raw base64 string, + "prompt": "Describe this image", + "max_tokens": 512, + "temperature": 0.8, + "top_p": 0.9, + "top_k": 50, + "repetition_penalty": 1.1, + "do_sample": true + } + Note: The token will be automatically prepended to your prompt. + Higher temperature (0.7-1.5) = more creative/varied responses + Lower temperature (0.1-0.5) = more deterministic/focused responses + """ + try: + print("Received request to /generate") + data = request.json + print(f"Request data keys: {data.keys() if data else 'No data'}") + + image_input = data.get('image', '') + prompt = data.get('prompt', 'Describe this image in detail.') + max_tokens = data.get('max_tokens', 512) + temperature = data.get('temperature', 0.8) + top_p = data.get('top_p', 0.9) + top_k = data.get('top_k', 50) + repetition_penalty = data.get('repetition_penalty', 1.1) + do_sample = data.get('do_sample', True) + + print(f"Prompt: {prompt[:50]}...") + print(f"Generation config - max_tokens: {max_tokens}, temperature: {temperature}, top_p: {top_p}, top_k: {top_k}, do_sample: {do_sample}, repetition_penalty: {repetition_penalty}") + print(f"Image data length: {len(image_input) if image_input else 0}") + + if not image_input: + return jsonify({'error': 'No image provided'}), 400 + + print("Decoding base64 image...") + except Exception as e: + error_msg = f"Error parsing request: {str(e)}" + print(error_msg) + import traceback + traceback.print_exc() + return jsonify({'error': error_msg}), 500 + + try: + # Handle base64 images + if image_input.startswith('data:image'): + # Extract base64 data after comma + print("Decoding data URI...") + base64_data = image_input.split(',')[1] + image_bytes = base64.b64decode(base64_data) + image = Image.open(BytesIO(image_bytes)).convert('RGB') + elif image_input.startswith(('http://', 'https://')): + # URL - not implemented for security, but you could add requests here + return jsonify({'error': 'URL images not supported yet'}), 400 + else: + # Assume raw base64 + print("Decoding raw base64...") + image_bytes = base64.b64decode(image_input) + image = Image.open(BytesIO(image_bytes)).convert('RGB') + + print(f"Image loaded: {image.size}, mode: {image.mode}") + + # Process image into pixel_values tensor + print("Processing image to pixel_values...") + pixel_values = process_image(image, input_size=448, max_num=12) + print(f"Pixel values shape: {pixel_values.shape}") + + pixel_values = pixel_values.to(torch.bfloat16).cuda() + print("Moved to GPU") + + # Prepare the prompt in InternVL format (must include token) + question = f'\n{prompt}' + + generation_config = dict( + max_new_tokens=max_tokens, + do_sample=do_sample, + temperature=temperature if do_sample else 1.0, + top_p=top_p if do_sample else 1.0, + top_k=top_k if do_sample else 50, + repetition_penalty=repetition_penalty, + ) + + print("Calling model.chat()...") + # Generate response using InternVL's chat method + # Correct signature: model.chat(tokenizer, pixel_values, question, generation_config, ...) + response = model.chat( + tokenizer, + pixel_values, + question, + generation_config + ) + + print(f"Got response: {response[:100]}..." if len(response) > 100 else f"Got response: {response}") + + return jsonify({ + 'response': response + }) + + except Exception as e: + error_msg = f"Error in generate endpoint: {str(e)}" + print(error_msg) + import traceback + traceback.print_exc() + return jsonify({'error': str(e)}), 500 + +@app.route('/health', methods=['GET']) +def health(): + return jsonify({ + 'status': 'ok', + 'model': model_name, + 'device': str(model.device), + 'gpu_memory_gb': torch.cuda.memory_allocated() / 1024**3 + }) + +if __name__ == '__main__': + print("\n" + "="*60) + print("InternVL3-1B-Pretrained Server") + print("="*60) + print(f"Server running on http://localhost:11415") + print(f"Health check: http://localhost:11415/health") + print("\nExample request (with varied responses):") + print(""" + POST http://localhost:11415/generate + { + "image": "data:image/png;base64,iVBORw0KG...", + "prompt": "What is in this image?", + "max_tokens": 512, + "temperature": 0.8, + "top_p": 0.9, + "top_k": 50, + "repetition_penalty": 1.1, + "do_sample": true + } + + For more deterministic responses, use: + "temperature": 0.1, "do_sample": false + """) + print("="*60 + "\n") + app.run(host='0.0.0.0', port=11415, debug=False) \ No newline at end of file diff --git a/docs/grok-archive-restructure-prd.md b/docs/grok-archive-restructure-prd.md new file mode 100644 index 0000000..64c7046 --- /dev/null +++ b/docs/grok-archive-restructure-prd.md @@ -0,0 +1,342 @@ +# Grok Archive Restructure & Relationship Restoration — PRD + +Status: implemented (first pass). The authoritative graph has been harvested and a +working lineage visualizer is in use. This document captures the requirements, the +confirmed data model, and the pipeline. See `tools/grok-export/README.md` for commands. + +Implemented on the reference instance (`C:\grokArchive\WebExport`): +2,207 liked root posts harvested → 10,836 graph nodes (3,757 images, 7,079 videos), +5,231 authoritative edges (5,073 image→video, 112 extensions, 46 edits), 5,605 lineage +clusters; 7,029 local mp4s + 7,024 posters + all 3,757 source images present locally. + +This document captures requirements and research for reconstructing +the relationship graph inside a Grok Imagine archive so the local HTML visualizer +can navigate the real creative lineage (image → video → extension, variants, etc.). + +It is a reusable, repository-owned design doc. Archive instances remain user data and +stay outside the repo (see `docs/project-organization.md` and +`docs/grok-web-export-archive.md`). + +## 1. Problem + +We have a large local Grok Imagine archive at an archive root (instance: +`C:\grokArchive\WebExport`). It was produced by the GDPR-style `grok.com` data export +plus our own download tools under `tools/grok-export/`. Current instance scale: + +- ~1,900 image manifest entries (`Images/` 811 files, `Uploads/` 82, `Unmapped/` 1,020). +- ~7,086 video rows in `videos.csv`; ~7,034 mp4 files downloaded (~15.7 GB). +- Each video post resolves to exactly one mp4 (post UUID == `share-videos/.mp4`). + +Grok Imagine content is **inherently a graph**, but the export we have is **flat**. +Real relationships include: + +- **words → image** (text-to-image). +- **image → video** using a *mode/guide* (Normal, Fun, Custom, Spicy) with **no text prompt**. +- **image → video** with a **full text prompt** (and/or a preset template such as `I2I2V`). +- **video → longer video** ("extend"), continuing from the last frame, optionally with its + own new prompt and a chosen extension duration. +- **one image → many video variants** (e.g. the same source image animated 10+ times). +- **one video → many extension variants**. + +None of these parent/child links survive in our current flat data. The visualizer can +only show a time-ordered list plus heuristic guesses. We want to **restore the true graph** +and navigate it. + +## 2. Goals / Non-Goals + +### Goals + +1. Define the Grok Imagine domain model (entities + edge types) precisely. +2. Identify every source from which relationships can be recovered, ranked by fidelity. +3. Specify a normalized graph data model (nodes + typed edges + provenance/confidence) + that the visualizer consumes. +4. Specify the restoration pipeline (new/updated tools) that produces that graph. +5. Specify visualizer requirements for navigating the graph (root-image clusters, + variant fans, extension timelines). +6. Preserve all existing downloaded bytes; never re-encode or destroy current assets. + +### Non-Goals + +- Re-generating any media. +- Building a server backend; the visualizer stays static local HTML/JS. +- Committing any archive instance data. +- Perfect reconstruction where the authoritative data no longer exists — heuristics are + explicitly allowed and must be labeled as such. + +## 3. Domain Model (Grok Imagine) + +Confirmed from research (xAI video docs, grok internal REST behavior, community tooling) +and from inspecting our archive. + +### Entities + +- **Image post** — a generated or uploaded still. Has: id (UUID), prompt (may be empty + for uploads), create time, pixels, source frame. +- **Video post** — an animated clip. Has: id (UUID), create time, prompt (may be empty + when only a mode/guide was used), duration, poster/first frame, mp4. +- **Upload** — a user-supplied still used as a source (no generation prompt). + +### Generation modes (xAI video API "request modes") + +| Mode | Inputs | Meaning | +| --- | --- | --- | +| Text-to-video | prompt only | video from text alone | +| Image-to-video | prompt + image | image is the **starting frame** | +| Reference-to-video | prompt + reference_images | guided by reference image(s) | +| Edit-video | video + prompt | modify an existing video | +| Extend-video | video + prompt (+ duration) | continue from the **last frame**; output = original + extension stitched | + +Image→video mode/guide presets in the consumer product: **Normal, Fun, Custom, Spicy** +(Spicy is the less-moderated mode). A video created with a guide and no typed prompt is +the common "empty prompt" case we see (2,360 of 7,086 rows). Preset *templates* observed +embedded in post pages carry a `templateType` such as `I2I` (image-to-image) and `I2I2V` +(image-to-image-to-video). + +Extension facts (xAI docs + Replicate/3rd-party mirrors): source video 2–15s; extension +2–10s (default 6s); the result is the original footage **plus** the continuation stitched +into one mp4. This means an extension chain produces progressively longer clips that +share lineage and (usually) prompt. + +### Edge types we must reconstruct + +- `derivedFrom(video → image)` — image-to-video starting frame. Carries `mode/guide`, + optional `prompt`, optional `templateType`. +- `extends(video → video)` — extension/continuation. Carries `extensionDuration`, + optional new `prompt`, and ordering within a chain. +- `referenceOf(video → image[])` — reference-to-video guidance images. +- `editOf(video → video)` — edit-video. +- `variantOf(node → group)` — sibling variants sharing the same parent (image siblings or + video siblings). Derived, not a stored edge. +- `promptGroup` — items sharing normalized prompt text (already implemented heuristically). + +## 4. What We Have vs. What Is Lost + +Surviving in the archive instance: + +- `manifest.jsonl` — image assets (id, source, createTime, prompt, file, bytes). +- `videos.csv` — video posts (create_time, prompt, link → post UUID). +- `video_downloads.jsonl` / `video_failures.jsonl` / `video_download_events.jsonl` — download audit. +- `prompts.txt` — chronological prompt log (includes chat-image cards w/ model + moderated flag). +- `archive_index.js` + `archive_browser.html` — current generated index and static viewer. +- Downloaded media under `Images/`, `Uploads/`, `Unmapped/`, `Videos/`. +- Top-level `grok_ledger.jsonl` (our own MultiImageClient `GrokImagine` generations via `XAIGrokAPI`, + source `log-backfill`) — separate provenance, not the web export graph. + +Lost / never captured: + +- `prod-grok-backend.json` (the raw export) has been deleted from the instance. +- Even when present, the GDPR export is **flat**: it does **not** contain `sourceImageId`, + `parentPostId`, mode/guide, or duration. The current `videos.csv`/`manifest.jsonl` reflect that. +- The video downloader fetched each post page but discarded everything except the mp4 URL; + the post HTML did **not** server-render parent/child data anyway (only the prompt via the + `` tag, the `og:image` poster, and the `og:video` mp4). + +**Conclusion:** the authoritative graph is not in any file we currently hold. It must be +re-harvested from grok's authenticated API, or approximated with heuristics. + +## 5. Sources of Truth for Relationships (ranked) + +### S1 — Grok internal REST API (PRIMARY, authoritative) — CONFIRMED + +Grok's own site loads media through an internal REST endpoint while logged in: + +``` +POST https://grok.com/rest/media/post/list +body: { "limit": 100, "filter": { "source": "MEDIA_POST_SOURCE_LIKED" }, "cursor": } +``` + +It is session-cookie authenticated (runs inside the logged-in browser; no API key). +`filter` is required; `MEDIA_POST_SOURCE_LIKED` is the one source that returns *your own* +posts (other source enums return a public/discover feed). Pagination is by the returned +`nextCursor` (a millisecond timestamp). Top-level results are your liked **images**; their +descendant videos are carried in nested `childPosts[]` (flattened to one level, each child +carrying its own `originalPostId`). + +Confirmed per-post fields (verified against the reference account, not just docs): + +- `id`, `userId`, `createTime`, `mediaType` (`MEDIA_POST_TYPE_IMAGE|VIDEO`) +- `prompt` / `originalPrompt`, `mediaUrl`, `hdMediaUrl`, `mimeType`, `resolution{width,height}` +- `childPosts[]` — descendants (videos, occasionally edited images) +- `originalPostId` — **immediate parent** (image for image→video; video for an extension) +- `originalRefType` — absent for direct image→video; `ORIGINAL_REF_TYPE_VIDEO_EXTENSION`; + `ORIGINAL_REF_TYPE_IMAGE_EDIT`; `ORIGINAL_REF_TYPE_MULTI_REF_IMAGE_EDIT` +- `mode` — the guide: `normal`, `custom`, `extremely-spicy-or-crazy`, … (the "spicy/fun" family) +- `videoDuration`, `videoExtensionStartTime` (offset into parent for extensions) +- `thumbnailImageUrl` (first frame), `lastFrameThumbnailImageUrl` (last frame) +- `modelName` (`imagine_x_1`, `imagine-video-gen`, …), `moderated`, `rRated`, + `inputMediaItems[]` (source image refs, tokenized URLs), `isRootUserUploaded` + +Source-image bytes are downloadable while logged in: top-level image `mediaUrl`s are mostly +on the public `imagine-public.x.ai` CDN (fetchable server-side, no auth); the rest are on +`assets.grok.com/users///content` (cookie-authenticated, no token needed). + +Implementation: `harvest_relay.py` + a browser-side paginating loop write `rest_posts.jsonl`; +`build_graph.py` turns it into `archive_graph.js`. Related community references: +`ironsniper1/Grok-Imagine-Bulk-Favorites-Downloader`, `uucz/grok-imagine-downloader`. + +Risks/constraints: requires an active logged-in session; subject to grok ToS and rate +limits (bulk authenticated bursts get throttled — keep concurrency modest, e.g. ≤6, with +retry); field names are unofficial and may drift; must run client-side in the user's +browser, relaying to a loopback receiver, not from this repo's servers. + +### S2 — Poster / first-frame images (SECONDARY, available now) + +Every video post exposes a poster frame at `https://grok.com/imagine/post//image` +(public; we saw `og:image` 832×1248). The first frame of an image-to-video clip is (close +to) the **source image**. This enables perceptual matching even without the API. + +### S3 — Perceptual / structural heuristics (FALLBACK, available now, offline) + +- **image → video**: perceptual-hash (pHash/dHash) the video's first frame against archived + images; high similarity + video.createTime ≥ image.createTime ⇒ candidate `derivedFrom`. +- **video → extension**: an extension's first frame ≈ parent's first frame (because output is + original + continuation), and the extension is longer in duration, later in time, and + usually shares the prompt ⇒ candidate `extends`. Duration ladders (e.g. 6s → 12s → 18s) + are a strong signal. +- **prompt grouping**: normalized-prompt equality (already implemented). +- **time + token Jaccard** scoring (already implemented in `build_archive_browser.py`). + +Heuristic edges must be stored with `confidence` and `evidence`, never presented as +authoritative. + +### S4 — Our own ledger (`grok_ledger.jsonl`) + +Authoritative for media **we** generated through MultiImageClient's Grok path, with exact +prompt/model/timestamp/local path. Useful to cross-link archive items to our own runs, but +covers only our generations, not the full web account. + +## 6. Target Restructured Data Model + +Produce a normalized graph the visualizer can load. Proposed shape (one generated +`archive_graph.js` / `.json`, archive-owned, not committed): + +```jsonc +{ + "generatedAt": "ISO", + "basePath": "", + "nodes": [ + { + "id": "uuid", + "type": "image | video | upload", + "createTime": "ISO", + "t": 0, // unix ms + "prompt": "", + "mode": "normal|fun|custom|spicy|null", + "templateType": "I2I|I2I2V|null", + "durationSec": 0, // videos + "file": "Videos/....mp4", // local relative, if downloaded + "posterFile": "Posters/....jpg", + "bytes": 0, + "status": "downloaded|missing|failed|suspect", + "provenance": "rest_api|export|heuristic|ledger" + } + ], + "edges": [ + { + "from": "childId", + "to": "parentId", + "kind": "derivedFrom|extends|referenceOf|editOf", + "confidence": 1.0, // 1.0 = authoritative API, <1 = heuristic + "evidence": ["rest:childPosts", "phash:0.94", "duration:6->12", "time", "prompt-eq"] + } + ], + "clusters": [ + { "rootId": "imageUuid", "memberIds": ["..."], "kind": "image-lineage" } + ], + "stats": { "...": 0 } +} +``` + +Rules: + +- Nodes are immutable identity by UUID; merge data from all sources, recording `provenance`. +- Edges are additive; the same parent/child can have both an authoritative and a heuristic + edge — keep the highest-confidence one for default navigation, expose the rest on demand. +- A **cluster** is a connected lineage rooted at the earliest source image (or upload). + Clusters drive the visualizer's "show this whole creative thread" view. + +## 7. Restoration Pipeline (tools) + +All tools live under `tools/grok-export/`, take explicit `--archive-root`, write +archive-owned outputs, and never hardcode personal paths (per project conventions). + +1. **`harvest_rest_api`** (S1, primary) — a userscript/CLI that, from a logged-in grok + session, pages `rest/media/post/list` (and per-post detail), and writes + `rest_posts.jsonl` with raw `childPosts`/`originalPost`/mode/duration. This is the + highest-value new capability. Must be idempotent and resumable by cursor. +2. **`fetch_posters`** (S2) — download `…/post//image` poster frames into `Posters/` + for every video (cheap, public, enables thumbnails + perceptual matching). +3. **`build_graph`** (S1+S3+S4) — merge `rest_posts.jsonl` (authoritative), `manifest.jsonl`, + `videos.csv`, download logs, `grok_ledger.jsonl`, and heuristic matchers (pHash on + posters/frames, duration ladders, prompt groups) into `archive_graph.js`. Supersedes the + ad-hoc `sourceCandidates` logic in `build_archive_browser.py`. +4. **(optional) `extract_frames`** — sample first/last frames of each mp4 (ffmpeg) to power + pHash-based `derivedFrom`/`extends` detection when the API is unavailable. + +Existing `extract_export.py`, `download_videos.py`, and `build_archive_browser.py` are kept; +`build_graph` extends rather than replaces them, and should reuse their parsing helpers. + +## 8. Visualizer Requirements + +The static browser (`archive_browser.html`) should navigate the graph, not just a flat list: + +- **Lineage cluster view**: select any node → show its whole cluster (root image, sibling + image variants, the video fan-out per image, and each video's extension chain) as a + navigable tree/graph. +- **Variant fan**: for a source image, show all child videos as a fan, labeled by mode/guide + and prompt; indicate count ("10 videos from this image"). +- **Extension timeline**: for a video, show its extension chain in order with durations + (e.g. 6s → 12s → 18s) and per-step prompts. +- **Edge confidence affordance**: authoritative edges render solid; heuristic edges render + dashed/with a confidence badge and their `evidence`. +- **Local-first media**: play `Videos/.mp4` and show `Posters/...`; remote URLs and + grok links are metadata only. Keep current behavior: hide suspect videos from normal + browsing, one active `