prepare('SELECT credits FROM users WHERE id = :id'); $creditsStmt->execute(['id' => $_SESSION['user_id']]); $userCredits = (int) $creditsStmt->fetchColumn(); $pdo->exec('CREATE TABLE IF NOT EXISTS critters ( id INTEGER PRIMARY KEY AUTOINCREMENT, noun_id INTEGER NOT NULL REFERENCES nouns(id), adjective_id INTEGER NOT NULL REFERENCES adjectives(id), verb_id INTEGER NOT NULL REFERENCES verbs(id), location_id INTEGER NOT NULL REFERENCES locations(id), author_id INTEGER NOT NULL REFERENCES users(id), image TEXT, isFavorite INTEGER NOT NULL DEFAULT 0, isArchived INTEGER NOT NULL DEFAULT 0, datetime TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP )'); $critterColumns = $pdo->query('PRAGMA table_info(critters)')->fetchAll(PDO::FETCH_COLUMN, 1); if (!in_array('author_id', $critterColumns, true)) { $pdo->exec('ALTER TABLE critters ADD COLUMN author_id INTEGER REFERENCES users(id)'); } if (!in_array('image', $critterColumns, true)) { $pdo->exec('ALTER TABLE critters ADD COLUMN image TEXT'); } if (!in_array('isFavorite', $critterColumns, true)) { $pdo->exec('ALTER TABLE critters ADD COLUMN isFavorite INTEGER NOT NULL DEFAULT 0'); } if (!in_array('isArchived', $critterColumns, true)) { $pdo->exec('ALTER TABLE critters ADD COLUMN isArchived INTEGER NOT NULL DEFAULT 0'); } if (!in_array('datetime', $critterColumns, true)) { $pdo->exec('ALTER TABLE critters ADD COLUMN datetime TEXT'); $pdo->exec("UPDATE critters SET datetime = CURRENT_TIMESTAMP WHERE datetime IS NULL"); } $pdo->exec('CREATE INDEX IF NOT EXISTS idx_critters_noun_id ON critters (noun_id)'); $pdo->exec('CREATE INDEX IF NOT EXISTS idx_critters_adjective_id ON critters (adjective_id)'); $pdo->exec('CREATE INDEX IF NOT EXISTS idx_critters_verb_id ON critters (verb_id)'); $pdo->exec('CREATE INDEX IF NOT EXISTS idx_critters_location_id ON critters (location_id)'); $pdo->exec('CREATE INDEX IF NOT EXISTS idx_critters_author_id ON critters (author_id)'); function countCrittersCreatedToday(PDO $pdo): int { return (int) $pdo->query("SELECT COUNT(*) FROM critters WHERE date(datetime) = date('now')")->fetchColumn(); } function secondsSinceLastCritterByAuthor(PDO $pdo, int $authorId): ?int { $stmt = $pdo->prepare("SELECT CAST(strftime('%s', 'now') - strftime('%s', datetime) AS INTEGER) FROM critters WHERE author_id = :author_id ORDER BY datetime DESC LIMIT 1"); $stmt->execute(['author_id' => $authorId]); $seconds = $stmt->fetchColumn(); return $seconds === false ? null : (int) $seconds; } $dailyLimitReached = countCrittersCreatedToday($pdo) >= DAILY_CRITTER_LIMIT; $cooldownRemaining = 0; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $secondsSinceLast = secondsSinceLastCritterByAuthor($pdo, (int) $_SESSION['user_id']); if ($secondsSinceLast !== null && $secondsSinceLast < CRITTER_COOLDOWN_SECONDS) { $cooldownRemaining = CRITTER_COOLDOWN_SECONDS - $secondsSinceLast; } } function isAllowedReplicateUrl(string $url): bool { $parts = parse_url($url); if ($parts === false || ($parts['scheme'] ?? '') !== 'https' || empty($parts['host'])) { return false; } $host = strtolower($parts['host']); return $host === 'api.replicate.com' || $host === 'replicate.delivery' || substr($host, -strlen('.replicate.delivery')) === '.replicate.delivery'; } function getValueById(PDO $pdo, string $table, $id): string { $stmt = $pdo->prepare("SELECT value FROM {$table} WHERE id = :id"); $stmt->execute(['id' => $id]); return (string) $stmt->fetchColumn(); } $adjectives = $pdo->query('SELECT id, value FROM adjectives ORDER BY value')->fetchAll(PDO::FETCH_ASSOC); $nouns = $pdo->query('SELECT id, value FROM nouns ORDER BY value')->fetchAll(PDO::FETCH_ASSOC); $verbs = $pdo->query('SELECT id, value FROM verbs ORDER BY value')->fetchAll(PDO::FETCH_ASSOC); $locations = $pdo->query('SELECT id, value FROM locations ORDER BY value')->fetchAll(PDO::FETCH_ASSOC); $fields = [ 'adjective_id' => ['table' => 'adjectives', 'label' => 'adjective'], 'noun_id' => ['table' => 'nouns', 'label' => 'noun'], 'verb_id' => ['table' => 'verbs', 'label' => 'verb'], 'location_id' => ['table' => 'locations', 'label' => 'location'], ]; $errors = []; $selected = [ 'adjective_id' => '', 'noun_id' => '', 'verb_id' => '', 'location_id' => '', ]; if ($_SERVER['REQUEST_METHOD'] !== 'POST') { foreach ($fields as $field => $info) { $value = $_GET[$field] ?? ''; if ($value !== '' && ctype_digit((string) $value) && rowExistsById($pdo, $info['table'], $value)) { $selected[$field] = (int) $value; } } $hasRemixParams = $selected['adjective_id'] !== '' || $selected['noun_id'] !== '' || $selected['verb_id'] !== '' || $selected['location_id'] !== ''; if (!$hasRemixParams) { $optionLists = [ 'adjective_id' => $adjectives, 'noun_id' => $nouns, 'verb_id' => $verbs, 'location_id' => $locations, ]; foreach ($optionLists as $field => $rows) { if (!empty($rows)) { $selected[$field] = (int) $rows[array_rand($rows)]['id']; } } } } if ($_SERVER['REQUEST_METHOD'] === 'POST' && $userCredits > 0 && !$dailyLimitReached) { foreach ($fields as $field => $info) { $value = $_POST[$field] ?? ''; $selected[$field] = $value; if ($value === '' || !ctype_digit((string) $value) || !rowExistsById($pdo, $info['table'], $value)) { $errors[$field] = 'Please choose a valid ' . $info['label'] . '.'; } } if (empty($errors) && countCrittersCreatedToday($pdo) >= DAILY_CRITTER_LIMIT) { $dailyLimitReached = true; } if (empty($errors) && $cooldownRemaining > 0) { $errors['cooldown'] = "Please wait {$cooldownRemaining} more second(s) before creating another critter."; } $creditReserved = false; if (empty($errors) && !$dailyLimitReached) { // Atomically reserve one credit in the same statement that checks for it, so // concurrent requests can't all read "1 credit" before any of them writes the // deduction. SQLite serializes writes, so only as many of these UPDATEs can // match "credits > 0" as there are credits actually available; everyone else // gets 0 rows affected and is turned away before the expensive Replicate call. $reserveStmt = $pdo->prepare('UPDATE users SET credits = credits - 1 WHERE id = :id AND credits > 0'); $reserveStmt->execute(['id' => $_SESSION['user_id']]); $creditReserved = $reserveStmt->rowCount() === 1; if (!$creditReserved) { $userCredits = 0; $errors['credits'] = 'You do not have enough credits to create a critter.'; } } if (empty($errors) && !$dailyLimitReached && $creditReserved) { $adjectiveValue = getValueById($pdo, 'adjectives', $selected['adjective_id']); $nounValue = getValueById($pdo, 'nouns', $selected['noun_id']); $verbValue = getValueById($pdo, 'verbs', $selected['verb_id']); $locationValue = getValueById($pdo, 'locations', $selected['location_id']); $prompt = "A {$adjectiveValue} {$nounValue} {$verbValue} {$locationValue} in a high quality 3d animation art style. The image should be vibrant and colorful, capturing the essence of the action and setting."; $ch = curl_init('https://api.replicate.com/v1/models/black-forest-labs/flux-schnell/predictions'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_CAINFO, '/etc/ssl/certs/ca-certificates.crt'); curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer ' . getenv('REPLICATE_API_TOKEN'), 'Content-Type: application/json', 'Prefer: wait', ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'input' => [ 'prompt' => $prompt, ], ])); $replicateResponse = curl_exec($ch); $imageUrl = null; if ($replicateResponse !== false) { $prediction = json_decode($replicateResponse, true); $pollAttempt = 0; while ( isset($prediction['status']) && !in_array($prediction['status'], ['succeeded', 'failed', 'canceled'], true) && $pollAttempt < 30 ) { $pollUrl = $prediction['urls']['get'] ?? ''; if (!is_string($pollUrl) || !isAllowedReplicateUrl($pollUrl)) { $prediction['status'] = 'failed'; break; } sleep(2); $pollCh = curl_init($pollUrl); curl_setopt($pollCh, CURLOPT_RETURNTRANSFER, true); curl_setopt($pollCh, CURLOPT_CAINFO, '/etc/ssl/certs/ca-certificates.crt'); curl_setopt($pollCh, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS); curl_setopt($pollCh, CURLOPT_TIMEOUT, 15); curl_setopt($pollCh, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer ' . getenv('REPLICATE_API_TOKEN'), ]); $pollResponse = curl_exec($pollCh); if ($pollResponse !== false) { $prediction = json_decode($pollResponse, true); } $pollAttempt++; } if (($prediction['status'] ?? null) === 'succeeded') { $output = $prediction['output'] ?? null; $imageUrl = is_array($output) ? ($output[0] ?? null) : $output; } } else { // The Replicate request never went through, so give the reserved credit back. $refundStmt = $pdo->prepare('UPDATE users SET credits = credits + 1 WHERE id = :id'); $refundStmt->execute(['id' => $_SESSION['user_id']]); } $imageFilename = null; if (is_string($imageUrl) && isAllowedReplicateUrl($imageUrl)) { $imageCh = curl_init($imageUrl); curl_setopt($imageCh, CURLOPT_RETURNTRANSFER, true); curl_setopt($imageCh, CURLOPT_CAINFO, '/etc/ssl/certs/ca-certificates.crt'); curl_setopt($imageCh, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS); curl_setopt($imageCh, CURLOPT_TIMEOUT, 30); curl_setopt($imageCh, CURLOPT_MAXFILESIZE, 15 * 1024 * 1024); $imageData = curl_exec($imageCh); if ($imageData !== false && strlen($imageData) > 0 && strlen($imageData) <= 15 * 1024 * 1024) { $imageInfo = @getimagesizefromstring($imageData); $allowedImageTypes = ['image/webp', 'image/png', 'image/jpeg']; if ($imageInfo !== false && in_array($imageInfo['mime'] ?? '', $allowedImageTypes, true)) { $imagesDir = __DIR__ . '/critter-images'; if (!is_dir($imagesDir)) { mkdir($imagesDir, 0755, true); } $imageFilename = bin2hex(random_bytes(16)) . '.webp'; file_put_contents($imagesDir . '/' . $imageFilename, $imageData); } } } $stmt = $pdo->prepare('INSERT INTO critters (noun_id, adjective_id, verb_id, location_id, author_id, image, datetime) VALUES (:noun_id, :adjective_id, :verb_id, :location_id, :author_id, :image, :datetime)'); $stmt->execute([ 'noun_id' => $selected['noun_id'], 'adjective_id' => $selected['adjective_id'], 'verb_id' => $selected['verb_id'], 'location_id' => $selected['location_id'], 'author_id' => $_SESSION['user_id'], 'image' => $imageFilename, 'datetime' => gmdate('Y-m-d H:i:s'), ]); header('Location: /'); exit; } } function renderOptions(array $rows, $selectedId): void { foreach ($rows as $row) { $isSelected = (string) $selectedId === (string) $row['id']; echo ''; } } $pageTitle = 'Create Critter'; ?>

Critter limit reached for the day

Critter limit reached for the day, please come back tomorrow.

Out of credits

You have 0 credits remaining, so you can't create a new critter right now.

Buy Credits

Imagine · Mix · Create

Create Your New Critter

Pick a recipe and we'll bring it to life.