feat: ai analyze
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class MealImageAnalysisRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'image' => ['required', 'image', 'max:4096'],
|
||||
];
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'image.required' => 'Ajoute une image à analyser.',
|
||||
'image.image' => 'Le fichier doit être une image.',
|
||||
'image.max' => "L'image ne doit pas dépasser 4 Mo.",
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user