Skip to content

AI Image Generator — Kotlin 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 Kotlin 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: Kotlin (1.9, standard library only)
// Source:   CosmoDev polyglot showcase port, from src/lib/image-gen.ts.

import java.net.URLDecoder
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.time.Duration
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset
import kotlin.math.ceil

object ImageGen {
    const val MODEL = "@cf/black-forest-labs/flux-1-schnell"
    const val STEPS = 4 // flux-1-schnell's design point -> 57.6 neurons/image
    const val PROMPT_MAX_CHARS = 600 // UX clamp, below the model's 2048 limit
    const val IP_HOUR_CAP = 10
    const val DAILY_CAP = 100

    /** Daily cap wins; the retry hint is in minutes. */
    sealed interface GateVerdict {
        data object Ok : GateVerdict
        data object RateLimitedIp : GateVerdict // fixed 60-minute window
        data class RateLimitedDaily(val retryAfterMinutes: Int) : GateVerdict
    }

    /** ok ? prompt : null; reason is null when ok ("empty"|"tooLong"|"controlChars"). */
    data class PromptResult(val prompt: String?, val reason: String?)

    /** Normalize CRLF/CR to \n, trim, enforce 1..600 chars, and reject control
     *  chars except \n (C0 minus U+000A, DEL, C1). */
    fun validatePrompt(raw: String): PromptResult {
        val prompt = raw.replace("\r\n", "\n").replace('\r', '\n').trim()
        if (prompt.isEmpty()) return PromptResult(null, "empty")
        if (prompt.length > PROMPT_MAX_CHARS) return PromptResult(null, "tooLong")
        val hasCtrl = prompt.any { it < '\n' || (it > '\n' && it < ' ') || (it.code in 0x7F..0x9F) }
        if (hasCtrl) return PromptResult(null, "controlChars")
        return PromptResult(prompt, null)
    }

    /** Minutes (>= 1) until the next UTC midnight — the daily window reset. */
    private fun minutesToUtcMidnight(now: Instant): Int {
        val midnight = LocalDate.ofInstant(now, ZoneOffset.UTC).plusDays(1)
            .atStartOfDay().toInstant(ZoneOffset.UTC)
        return maxOf(1, ceil(Duration.between(now, midnight).toMillis() / 60_000.0).toInt())
    }

    /** Daily cap wins: the site-global budget resets at UTC midnight. */
    fun checkGate(ipCount: Int, dailyCount: Int, now: Instant = Instant.now()): GateVerdict =
        when {
            dailyCount >= DAILY_CAP -> GateVerdict.RateLimitedDaily(minutesToUtcMidnight(now))
            ipCount >= IP_HOUR_CAP -> GateVerdict.RateLimitedIp
            else -> GateVerdict.Ok
        }

    /** Encode the shareable state: "" when there is nothing to share.
     *  URLEncoder is form-encoded (+ for spaces) — swap in %20 to match the
     *  TS encodeURIComponent. */
    fun encodeUrlState(prompt: String): String =
        if (prompt.isEmpty()) ""
        else "?prompt=" + URLEncoder.encode(prompt, StandardCharsets.UTF_8).replace("+", "%20")

    /** Inverse of encodeUrlState — a missing param decodes to "". */
    fun decodeUrlState(search: String): String =
        search.removePrefix("?").split('&').firstOrNull { it.startsWith("prompt=") }
            ?.substring("prompt=".length)
            ?.let { URLDecoder.decode(it.replace("%20", "+"), StandardCharsets.UTF_8) }
            ?: ""
}

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 →