$input normalized creator input from AiBiographyInputBuilder * @param bool $strict true on retry — forces more conservative output * @param bool $sparse true for sparse-profile creators * @return array{messages: list, max_tokens: int, temperature: float, stream: bool, prompt_version: string} */ public function build(array $input, bool $strict = false, bool $sparse = false): array { if ($sparse && $strict) { $systemPrompt = self::SYSTEM_PROMPT_SPARSE_STRICT; $userPrompt = $this->buildSparseUserPrompt($input, strict: true); $maxTokens = 240; $temperature = 0.2; } elseif ($sparse) { $systemPrompt = self::SYSTEM_PROMPT_SPARSE; $userPrompt = $this->buildSparseUserPrompt($input, strict: false); $maxTokens = 320; $temperature = 0.3; } elseif ($strict) { $systemPrompt = self::SYSTEM_PROMPT_STRICT; $userPrompt = $this->buildUserPrompt($input, strict: true); $maxTokens = 450; $temperature = 0.25; } else { $systemPrompt = self::SYSTEM_PROMPT; $userPrompt = $this->buildUserPrompt($input, strict: false); $maxTokens = 600; $temperature = 0.45; } return [ 'messages' => [ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userPrompt], ], 'max_tokens' => $maxTokens, 'temperature' => $temperature, 'stream' => false, 'prompt_version' => self::PROMPT_VERSION, ]; } private function buildUserPrompt(array $input, bool $strict): string { $wordTarget = $strict ? '60 to 100' : '70 to 130'; $lines = [ "Write a creator biography in {$wordTarget} words using only the facts below. Output one paragraph only.", '', ]; $username = (string) ($input['username'] ?? ''); if ($username !== '') { $lines[] = "- Creator: {$username}"; } $memberYear = $input['member_since_year'] ?? null; $years = $input['years_on_skinbase'] ?? null; if ($memberYear !== null && (int) $memberYear > 0) { $label = ((int) ($years ?? 0) > 1) ? ", {$years} years on the platform" : ''; $lines[] = "- Member since: {$memberYear}{$label}"; } $uploads = $input['uploads_count'] ?? 0; if ((int) $uploads > 0) { $lines[] = "- Total public uploads: {$uploads}"; } $featured = $input['featured_count'] ?? 0; if ((int) $featured > 0) { $lines[] = "- Featured artworks: {$featured}"; } $downloads = $input['downloads_count'] ?? 0; if ((int) $downloads > 5000) { $lines[] = sprintf('- Total downloads: %s', number_format((int) $downloads)); } $categories = $input['top_categories'] ?? []; if ($categories !== []) { $lines[] = '- Top categories: ' . implode(', ', array_slice((array) $categories, 0, 2)); } // On strict retry, trim tags to keep prompt tight and reduce hallucination surface. if (! $strict) { $tags = $input['top_tags'] ?? []; if ($tags !== []) { $lines[] = '- Common themes: ' . implode(', ', array_slice((array) $tags, 0, 3)); } } $bestWork = $input['best_performing_work'] ?? null; if (is_array($bestWork) && isset($bestWork['title'], $bestWork['year'])) { $lines[] = "- Best-performing work: {$bestWork['title']} ({$bestWork['year']})"; } $productiveYear = $input['most_productive_year'] ?? null; if ($productiveYear !== null && (int) $productiveYear > 0) { $lines[] = "- Most productive year: {$productiveYear}"; } $status = $input['current_activity_status'] ?? null; if ($status !== null && $status !== '') { $statusLabels = [ 'active' => 'currently active', 'recently_active' => 'recently active', 'returning' => 'returning creator', 'legacy' => 'long-standing creator', 'inactive' => null, ]; $statusLabel = $statusLabels[$status] ?? null; if ($statusLabel !== null) { $lines[] = "- Activity: {$statusLabel}"; } } $milestones = $input['milestones'] ?? []; if (is_array($milestones)) { if (! empty($milestones['has_comeback'])) { $lines[] = '- Notable milestone: returned after a significant break'; } $streak = (int) ($milestones['best_upload_streak_months'] ?? 0); if ($streak >= 3 && ! $strict) { $lines[] = "- Upload streak: {$streak} consecutive months"; } } // Include eras and evolution only when not on strict retry. if (! $strict) { $eras = $input['eras'] ?? []; if (is_array($eras) && count($eras) >= 2) { $eraTitles = array_column($eras, 'title'); $lines[] = '- Creator eras: ' . implode(' → ', $eraTitles); } $evolutionCount = $input['evolution_count'] ?? 0; if ((int) $evolutionCount > 0) { $lines[] = "- Remastered/evolved works: {$evolutionCount}"; } } $lines[] = ''; $lines[] = 'Avoid hype. Do not open with a formulaic phrase. Do not list every stat. Output one paragraph only. No markdown.'; return implode("\n", $lines); } private function buildSparseUserPrompt(array $input, bool $strict = false): string { $wordTarget = $strict ? '35 to 50' : '35 to 60'; $lines = [ "Write a brief, modest creator introduction in {$wordTarget} words using only these facts. Output one paragraph only.", '', ]; $username = (string) ($input['username'] ?? ''); if ($username !== '') { $lines[] = "- Creator: {$username}"; } $memberYear = $input['member_since_year'] ?? null; $years = $input['years_on_skinbase'] ?? null; if ($memberYear !== null && (int) $memberYear > 0) { $yearsLabel = ((int) ($years ?? 0) > 1) ? ", {$years} years on the platform" : ''; $lines[] = "- Member since: {$memberYear}{$yearsLabel}"; } $uploads = $input['uploads_count'] ?? 0; if ((int) $uploads > 0) { $lines[] = "- Public uploads: {$uploads}"; } $categories = $input['top_categories'] ?? []; if ($categories !== []) { $lines[] = '- Categories: ' . implode(', ', array_slice((array) $categories, 0, $strict ? 1 : 2)); } $lines[] = ''; $lines[] = 'Keep it simple and factual. Write at least ' . self::MIN_WORDS . ' words. Prefer two short sentences if needed. No praise. No markdown.'; return implode("\n", $lines); } }