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 →