generateMealDraft(); $imagePath = $this->generateMealImage($draft); $ingredients = $draft['ingredients']; unset($draft['ingredients'], $draft['image_prompt']); return DB::transaction(function () use ($draft, $imagePath, $ingredients): MealPosts { $mealPost = MealPosts::create([ ...$draft, 'user_id' => $this->aiUser()->getKey(), 'image_url' => $imagePath, 'visibility' => MealPostVisibility::Public, 'ai_generated' => true, 'eaten_at' => now(), ]); if ($ingredients !== []) { $mealPost->ingredients()->createMany($ingredients); } return $mealPost->loadMissing('user:id,name,avatar_url', 'ingredients'); }); } private function generateMealDraft(): array { $response = MealMaker::make()->prompt($this->mealPrompt(), timeout: $this->textTimeout()); return $this->normalizeDraft( $response instanceof Arrayable ? $response->toArray() : (json_decode($response->text ?? '', true) ?: []) ); } private function generateMealImage(array $draft): string { $path = Image::of($this->imagePrompt($draft)) ->landscape() ->quality($this->imageQuality()) ->timeout($this->imageTimeout()) ->generate() ->storePublicly($this->imageStoragePath(), 'public'); if (! is_string($path)) { throw new RuntimeException("L'image du repas IA n'a pas pu etre stockee."); } return $path; } private function aiUser(): User { $email = $this->configString('user_email', 'ai@daily-meal.local'); return User::query()->firstOrCreate( ['email' => $email], [ 'name' => $this->configString('user_name', 'Daily Meal AI'), 'password' => Hash::make(Str::random(64)), 'bio' => 'Compte utilise pour publier des repas generes automatiquement.', ], ); } private function mealPrompt(): string { $season = now()->locale('fr')->translatedFormat('F'); return <<cleanString($draft['image_prompt'] ?? '', 1500); if ($prompt === '') { $prompt = "Photorealistic food photography of {$draft['title']}, single serving, natural light, restaurant plating."; } return $prompt.' No text, no watermark, no people, no hands.'; } private function normalizeDraft(array $draft): array { $type = MealPostType::tryFrom($this->cleanString($draft['type'] ?? '', 32)) ?? MealPostType::OTHER; return [ 'title' => $this->cleanString($draft['title'] ?? 'Repas IA', 255) ?: 'Repas IA', 'caption' => $this->cleanString($draft['caption'] ?? '', 1000), 'type' => $type, 'calories' => $this->positiveInteger($draft['calories'] ?? 0), 'proteins' => $this->positiveNumber($draft['proteins'] ?? 0), 'carbs' => $this->positiveNumber($draft['carbs'] ?? 0), 'fats' => $this->positiveNumber($draft['fats'] ?? 0), 'ingredients' => $this->normalizeIngredients($draft['ingredients'] ?? []), 'image_prompt' => $this->cleanString($draft['image_prompt'] ?? '', 1500), ]; } private function normalizeIngredients(mixed $ingredients): array { if (! is_array($ingredients)) { return []; } $normalized = []; foreach (array_slice($ingredients, 0, 12) as $ingredient) { if (! is_array($ingredient)) { continue; } $name = $this->cleanString($ingredient['ingredient'] ?? '', 255); if ($name === '') { continue; } $unit = IngredientUnit::tryFrom($this->cleanString($ingredient['unit'] ?? '', 16)) ?? IngredientUnit::GRAM; $normalized[] = [ 'position' => count($normalized) + 1, 'ingredient' => $name, 'quantity' => max(1, $this->positiveInteger($ingredient['quantity'] ?? 1)), 'unit' => $unit, ]; } return $normalized; } private function cleanString(mixed $value, int $limit): string { if (! is_string($value) && ! is_numeric($value)) { return ''; } $cleaned = trim((string) preg_replace('/\s+/', ' ', (string) $value)); return Str::limit($cleaned, $limit, ''); } private function positiveInteger(mixed $value): int { if (! is_numeric($value)) { return 0; } return max(0, (int) round((float) $value)); } private function positiveNumber(mixed $value): float { if (! is_numeric($value)) { return 0; } return round(max(0, (float) $value), 1); } private function imageQuality(): string { $quality = $this->configString('image_quality', 'medium'); return in_array($quality, ['low', 'medium', 'high'], true) ? $quality : 'medium'; } private function imageStoragePath(): string { return $this->configString('image_path', 'meal-posts/ai-generated'); } private function textTimeout(): int { return max(1, (int) config('meal_posts.ai_generation.text_timeout', 90)); } private function imageTimeout(): int { return max(1, (int) config('meal_posts.ai_generation.image_timeout', 120)); } private function configString(string $key, string $fallback): string { $value = trim((string) config("meal_posts.ai_generation.{$key}", $fallback)); return $value !== '' ? $value : $fallback; } }