api/app/Http/Controllers/MealImageAnalysisController...

133 lines
3.9 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Ai\Agents\MealImageAnalyzer;
use App\Enums\IngredientUnit;
use App\Enums\MealPostType;
use App\Http\Requests\MealImageAnalysisRequest;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Str;
use Throwable;
class MealImageAnalysisController extends Controller
{
public function store(MealImageAnalysisRequest $request): JsonResponse
{
try {
$response = MealImageAnalyzer::make()->prompt(
$this->prompt(),
[$request->file('image')],
timeout: 90,
);
} catch (Throwable $exception) {
report($exception);
return response()->json([
'message' => "Impossible d'analyser l'image pour le moment.",
], 502);
}
return response()->json([
'data' => $this->normalizeDraft(
$response instanceof Arrayable
? $response->toArray()
: (json_decode($response->text, true) ?: [])
),
]);
}
private function prompt(): string
{
return <<<'PROMPT'
Analyse l'image du repas et prépare un brouillon éditable pour l'utilisateur.
Contraintes:
- Ne renseigne que ce qui est visible ou très probable dans l'image.
- Estime les quantités pour une portion consommable.
- Retourne des calories et macros cohérentes entre elles.
- Si l'image ne montre pas clairement un repas, retourne un titre générique et des valeurs à 0 avec une liste d'ingrédients vide.
PROMPT;
}
private function normalizeDraft(array $draft): array
{
$type = MealPostType::tryFrom($this->cleanString($draft['type'] ?? '', 32))
?? MealPostType::OTHER;
return [
'title' => $this->cleanString($draft['title'] ?? 'Repas', 255) ?: 'Repas',
'caption' => $this->cleanString($draft['caption'] ?? '', 1000),
'type' => $type->value,
'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'] ?? []),
];
}
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->value,
];
}
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);
}
}