sizeFromAspect($aspect); $urls = []; for ($i = 1; $i <= $count; $i++) { $svg = $this->templateSvg($prompt, $width, $height, $i); if ($svg === null) { $messages = $this->messages($prompt, $width, $height, $aspect, $i, $count); $response = Http::timeout($timeout) ->withToken($apiKey) ->acceptJson() ->post("{$baseUrl}/chat/completions", [ 'model' => $model, 'messages' => $messages, 'max_tokens' => $maxTokens, 'temperature' => 0.8, ]); if (! $response->successful()) { $errorBody = trim((string) data_get($response->json(), 'error.message', $response->body())); throw new RuntimeException('SVG image request gagal: '.$response->status().' - '.$errorBody); } $content = (string) data_get($response->json(), 'choices.0.message.content', ''); $svg = $this->sanitizeSvg($content, $prompt, $width, $height, $i); } $filename = 'svg_'.now()->format('Ymd_His').'_'.Str::lower(Str::random(8)).'.svg'; $path = 'images/'.$filename; if (! Storage::disk('public')->put($path, $svg)) { throw new RuntimeException('Gagal menyimpan file SVG ke storage publik.'); } $urls[] = Storage::url($path); } return $urls; } private function messages(string $prompt, int $width, int $height, string $aspect, int $variant, int $count): array { return [ [ 'role' => 'system', 'content' => 'You create polished SVG artwork. Return only raw markup. No markdown, no code fences, no explanations, no scripts, no foreignObject, no external assets, no remote fonts. The image must be literal and recognizable first, stylish second. Show one clear main subject matching the prompt, centered and large. Avoid abstract blobs unless the prompt explicitly asks for abstract art.', ], [ 'role' => 'user', 'content' => "Create a premium AI-style SVG illustration inspired by this prompt: {$prompt}\n". "Aspect ratio: {$aspect}\n". "Canvas: {$width}x{$height}\n". "Variant {$variant} of {$count}\n". "Requirements:\n". "- valid standalone SVG\n". "- include viewBox=\"0 0 {$width} {$height}\"\n". "- no XML declaration\n". "- use only SVG shapes, gradients, patterns, filters, and text\n". "- keep it visually rich and suitable for a product mockup or concept poster\n". "- make the main subject obvious at first glance\n". "- if the prompt is an animal or object, draw the actual animal or object, not a symbolic orb or abstract icon\n". "- avoid text unless the prompt explicitly asks for typography\n". "- do not mention policies or limitations", ], ]; } private function sanitizeSvg(string $raw, string $prompt, int $width, int $height, int $variant): string { $raw = trim($raw); $raw = preg_replace('/^```(?:svg)?/i', '', $raw) ?? $raw; $raw = preg_replace('/```$/', '', $raw) ?? $raw; $start = stripos($raw, ''); if ($start === false || $end === false) { return $this->templateSvg($prompt, $width, $height, $variant) ?? $this->fallbackSvg($prompt, $width, $height, $variant); } $svg = substr($raw, $start, $end - $start + 6); $svg = preg_replace('/]*>.*?<\/script>/is', '', $svg) ?? $svg; $svg = preg_replace('/]*>.*?<\/foreignObject>/is', '', $svg) ?? $svg; if (! str_contains($svg, 'xmlns=')) { $svg = preg_replace('/elephantSvg($prompt, $width, $height, $variant); } return null; } private function fallbackSvg(string $prompt, int $width, int $height, int $variant): string { $label = htmlspecialchars(Str::limit($prompt, 42, ''), ENT_QUOTES, 'UTF-8'); $accentA = ['#43d9bd', '#f5b96b', '#ff7a59', '#8de2ff'][$variant % 4]; $accentB = ['#13273b', '#101a2a', '#1d1730', '#0f2233'][$variant % 4]; $panelWidth = max(320, $width - 160); $panelHeight = max(220, $height - 180); $cx = (int) floor($width * 0.24); $cy = (int) floor($height * 0.36); $radius = (int) floor(min($width, $height) * 0.12); $x = (int) floor($width * 0.36); $y = (int) floor($height * 0.25); $w = (int) floor($width * 0.42); $h = (int) floor($height * 0.18); $x2 = (int) floor($width * 0.36); $y2 = (int) floor($height * 0.48); $w2 = (int) floor($width * 0.32); $h2 = (int) floor($height * 0.12); $textY = max(120, $height - 120); $textY2 = max(156, $height - 84); return << {$label} SVG CONCEPT POSTER SVG; } private function elephantSvg(string $prompt, int $width, int $height, int $variant): string { $accentA = ['#43d9bd', '#f5b96b', '#ff7a59', '#8de2ff'][$variant % 4]; $accentB = ['#13273b', '#101a2a', '#1d1730', '#0f2233'][$variant % 4]; $label = htmlspecialchars(Str::limit($prompt, 34, ''), ENT_QUOTES, 'UTF-8'); $shadowY = (int) floor($height * 0.78); $bodyCx = (int) floor($width * 0.5); $bodyCy = (int) floor($height * 0.5); $bodyRx = (int) floor($width * 0.19); $bodyRy = (int) floor($height * 0.2); $headCx = (int) floor($width * 0.39); $headCy = (int) floor($height * 0.42); $headR = (int) floor(min($width, $height) * 0.12); $earLeftCx = (int) floor($width * 0.29); $earRightCx = (int) floor($width * 0.44); $earCy = (int) floor($height * 0.4); $earRx = (int) floor($width * 0.09); $earRy = (int) floor($height * 0.12); $legTop = (int) floor($height * 0.58); $legHeight = (int) floor($height * 0.18); $textY = max(90, $height - 90); $leg1X = $bodyCx - 150; $leg2X = $bodyCx - 90; $leg3X = $bodyCx + 22; $leg4X = $bodyCx + 82; $trunkX1 = $headCx + 28; $trunkY1 = $headCy + 18; $trunkX2 = $headCx + 44; $trunkY2 = $headCy + 66; $trunkX3 = $headCx + 34; $trunkY3 = $headCy + 116; $trunkX4 = $headCx + 24; $trunkY4 = $headCy + 162; $trunkX5 = $headCx + 70; $trunkY5 = $headCy + 172; $trunkX6 = $headCx + 86; $trunkY6 = $headCy + 128; $trunkHlX1 = $headCx + 16; $trunkHlY1 = $headCy + 42; $trunkHlX2 = $headCx + 44; $trunkHlY2 = $headCy + 72; $trunkHlX3 = $headCx + 58; $trunkHlY3 = $headCy + 120; $trunkHlX4 = $headCx + 48; $trunkHlY4 = $headCy + 154; $tusk1X1 = $headCx + 12; $tusk1Y1 = $headCy + 30; $tusk1X2 = $headCx + 30; $tusk1Y2 = $headCy + 42; $tusk1X3 = $headCx + 44; $tusk1Y3 = $headCy + 52; $tusk1X4 = $headCx + 56; $tusk1Y4 = $headCy + 54; $tusk2X1 = $headCx + 2; $tusk2Y1 = $headCy + 28; $tusk2X2 = $headCx + 18; $tusk2Y2 = $headCy + 42; $tusk2X3 = $headCx + 28; $tusk2Y3 = $headCy + 52; $tusk2X4 = $headCx + 34; $tusk2Y4 = $headCy + 58; $eyeLeftX = $headCx - 18; $eyeRightX = $headCx + 14; $eyeY = $headCy - 10; $eyeGlowLeftX = $headCx - 20; $eyeGlowRightX = $headCx + 12; $eyeGlowY = $headCy - 12; $tailX1 = $bodyCx + 150; $tailY1 = $bodyCy - 52; $tailX2 = $bodyCx + 184; $tailY2 = $bodyCy - 78; $tailX3 = $bodyCx + 198; $tailY3 = $bodyCy - 18; $tailX4 = $bodyCx + 174; $tailY4 = $bodyCy + 14; return << {$label} SVG; } private function sizeFromAspect(string $aspect): array { return match ($aspect) { 'portrait' => [1024, 1536], 'square' => [1024, 1024], default => [1536, 1024], }; } }