Skip to content

AI Image Generator — C# source

Turn a text prompt into a 1024×1024 image with FLUX.1 [schnell] on Cloudflare Workers AI. No account, no API key — rate-limited for fair use. Your prompt goes to the model through our Worker; we store no prompts and no images.

This is the C# implementation — the same logic the interactive tool runs, in a shareable, citable form.

// AI Image Generator — prompt validation, usage-gate verdicts, and ?prompt=
// URL state for FLUX.1-schnell image generation.
//
// Language: C# (C# 12 / .NET 8, standard library only)
// Source:   CosmoDev polyglot showcase port, from src/lib/image-gen.ts.

namespace ImageGen;

public enum PromptInvalid { Empty, TooLong, ControlChars }

/// <summary>Daily cap wins; the retry hint is in minutes.</summary>
/// <param name="Error">"RATE_LIMITED_IP" | "RATE_LIMITED_DAILY", null when ok.</param>
public sealed record GateVerdict(bool Ok, string? Error = null, int RetryAfterMinutes = 0);

public static class ImageGen
{
    public const string Model = "@cf/black-forest-labs/flux-1-schnell";
    public const int Steps = 4; // flux-1-schnell's design point -> 57.6 neurons/image
    public const int PromptMaxChars = 600; // UX clamp, below the model's 2048 limit
    public const int IpHourCap = 10;
    public const int DailyCap = 100;

    /// <summary>Normalize CRLF/CR to \n, trim, enforce 1..600 chars, and
    /// reject control chars except \n (C0 minus U+000A, DEL, C1). Returns
    /// (ok, prompt, reason).</summary>
    public static (bool Ok, string Prompt, PromptInvalid Reason) ValidatePrompt(string raw)
    {
        var prompt = raw.Replace("\r\n", "\n").Replace('\r', '\n').Trim();
        if (prompt.Length == 0) return (false, prompt, PromptInvalid.Empty);
        if (prompt.Length > PromptMaxChars) return (false, prompt, PromptInvalid.TooLong);
        foreach (var c in prompt)
        {
            var isCtrl = c < '\n' || (c > '\n' && c < ' ') || (c >= (char) 0x7F && c <= (char) 0x9F);
            if (isCtrl) return (false, prompt, PromptInvalid.ControlChars);
        }
        return (true, prompt, default);
    }

    /// <summary>Minutes (>= 1) until the next UTC midnight — the daily reset.</summary>
    private static int MinutesToUtcMidnight(DateTimeOffset now)
    {
        var utc = now.UtcDateTime;
        return Math.Max(1, (int) Math.Ceiling((utc.Date.AddDays(1) - utc).TotalMinutes));
    }

    /// <summary>Daily cap wins: the site-global budget resets at UTC midnight.</summary>
    public static GateVerdict CheckGate(int ipCount, int dailyCount, DateTimeOffset? now = null)
    {
        if (dailyCount >= DailyCap)
            return new(false, "RATE_LIMITED_DAILY", MinutesToUtcMidnight(now ?? DateTimeOffset.UtcNow));
        if (ipCount >= IpHourCap)
            return new(false, "RATE_LIMITED_IP", 60);
        return new(true);
    }

    /// <summary>Encode the shareable state: "" when there is nothing to share.</summary>
    public static string EncodeUrlState(string prompt) =>
        prompt.Length == 0 ? "" : $"?prompt={Uri.EscapeDataString(prompt)}";

    /// <summary>Inverse of EncodeUrlState — a missing param decodes to "".</summary>
    public static string DecodeUrlState(string search)
    {
        foreach (var pair in (search.StartsWith('?') ? search[1..] : search).Split('&'))
        {
            if (pair.StartsWith("prompt=", StringComparison.Ordinal))
                return Uri.UnescapeDataString(pair["prompt=".Length..]);
        }
        return "";
    }
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →