ordinarthur 9dbd7e0ba9 feat(ai): quality-first recipe pipeline with structured outputs
Overhaul of the backend AI module to produce better recipes, better
images, more reliably, and cheaper.

New src/ai/ module:
- prompts.ts: long 'Chef Antoine' system prompt (~1500 tokens) with
  explicit originality rules, technical precision requirements, vocal
  transcription handling, and 3 few-shot style examples. Long enough
  to benefit from OpenAI's automatic prompt caching (-50% on cached
  portion from the 2nd call onward).
- recipe-generator.ts: uses Structured Outputs (json_schema strict).
  Rich schema: titre, description, origine_inspiration, ingredients
  with quantity/notes/complement flag, numbered etapes with per-step
  duration, conseils array, accord_boisson. No more JSON.parse crashes.
- image-generator.ts: switched from dall-e-3 to gpt-image-1 (medium
  quality by default). Much better photographic realism. Dedicated
  magazine-style prompt (editorial food photography, 45-deg overhead,
  natural light, stoneware). Slugify preserves extended Latin chars
  (cote-de-boeuf not c-te-de-b-uf).
- transcriber.ts: migrated from whisper-1 to gpt-4o-mini-transcribe
  (50% cheaper, better on French). Includes a context prompt to bias
  toward culinary vocabulary.
- cost.ts: centralized pricing table + helpers. Every OpenAI call now
  emits a structured log with model, durationMs, costUsd, usage, and
  cacheHit flag.

Plugin refactor:
- plugins/ai.ts now delegates to src/ai/* and only keeps the Fastify
  decoration glue + storage fallback for audio.
- OpenAI client configured with maxRetries=3, timeout=60s.
- Image generation runs in parallel with the recipe flatten/serialize
  step (minor speedup, ~0.5s).
- flattenRecipe() converts the rich structured recipe into the legacy
  flat RecipeData shape (for Prisma columns) while preserving the
  structured form in recipeData.structured.

Routes:
- recipes.ts stores the structured JSON in generatedRecipe (instead
  of the aplatissement lossy), enabling future frontends to render
  rich recipes with per-ingredient notes and step timers.

Env vars:
- OPENAI_TRANSCRIBE_MODEL, OPENAI_IMAGE_MODEL, OPENAI_IMAGE_QUALITY,
  OPENAI_IMAGE_SIZE, OPENAI_MAX_RETRIES, OPENAI_TIMEOUT_MS

Cost per recipe (estimated):
- Before: ~$0.044 (whisper $0.003 + 4o-mini $0.0004 + dall-e-3 $0.04)
- After : ~$0.018 (4o-mini-transcribe $0.0015 + 4o-mini $0.0004
  + gpt-image-1 medium $0.0165), ~-59%.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 08:46:18 +02:00
2025-03-10 00:24:26 +01:00

Freedge

Freedge génère des recettes personnalisées à partir des ingrédients dictés à l'oral. Le son est transcrit (Whisper), une recette est générée (GPT-4o-mini) et une image optionnelle est produite (DALL-E 3).

Stack

  • Frontend : React 19 + Vite + TailwindCSS + ShadCN/UI + React Router
  • Backend : Fastify 4 + TypeScript + Prisma 5 + SQLite
  • IA : OpenAI (gpt-4o-mini-transcribe, gpt-4o-mini avec Structured Outputs, gpt-image-1)
  • Stockage : MinIO (S3-compatible) avec fallback local
  • Paiement : Stripe (client créé à l'inscription — intégration abonnement à finaliser)
  • Auth : JWT + Google OAuth

Structure

freedge/
├── backend/                 # TypeScript
│   ├── prisma/              # Schéma + migrations SQLite
│   ├── tsconfig.json
│   └── src/
│       ├── ai/              # prompts, recipe-generator, image-generator,
│       │                    #   transcriber, cost (logging tokens/USD)
│       ├── plugins/         # auth, ai, stripe, google-auth
│       ├── routes/          # auth, recipes, users
│       ├── types/           # fastify.d.ts (augmentation décorateurs)
│       ├── utils/           # env, storage (MinIO), email
│       └── server.ts
└── frontend/
    └── src/
        ├── api/             # Clients HTTP (auth, user, recipe)
        ├── components/      # UI shadcn + composants métier
        ├── hooks/           # useAuth, useMobile, useAudioRecorder
        ├── layouts/         # MainLayout
        └── pages/           # Home, Auth/*, Recipes/*, Profile, ResetPassword

Prérequis

  • Node.js ≥ 18
  • pnpm (recommandé) ou npm
  • Une clé API OpenAI
  • (Optionnel) Un serveur MinIO pour le stockage images/audio
  • (Optionnel) Un compte Resend pour les emails

Démarrage

# Installation
cd backend && npm install
cd ../frontend && pnpm install

# Variables d'environnement backend (.env dans backend/)
cp .env.example .env   # puis éditer
# Requis : DATABASE_URL, JWT_SECRET, OPENAI_API_KEY

# Base de données
cd backend
npx prisma migrate dev
npx prisma generate

# Lancement
npm run dev                                # backend (tsx watch) sur :3000
cd ../frontend && pnpm dev                 # frontend sur :5173

# Build production backend
cd backend && npm run build && npm start   # compile vers dist/ puis lance node dist/server.js
npm run typecheck                          # vérification TS sans build

Variables d'environnement backend

Variable Requis Description
DATABASE_URL URL Prisma (ex: file:./prisma/dev.db)
JWT_SECRET Clé JWT (≥ 32 caractères recommandé)
OPENAI_API_KEY Clé API OpenAI
PORT Port du serveur (défaut 3000)
CORS_ORIGINS Origines autorisées, séparées par virgule
FRONTEND_URL URL frontend pour les emails de reset
OPENAI_TEXT_MODEL Défaut gpt-4o-mini (recette)
OPENAI_TRANSCRIBE_MODEL Défaut gpt-4o-mini-transcribe
OPENAI_IMAGE_MODEL Défaut gpt-image-1
OPENAI_IMAGE_QUALITY low / medium / high (défaut medium)
OPENAI_IMAGE_SIZE Défaut 1024x1024
ENABLE_IMAGE_GENERATION false pour désactiver totalement la génération d'image
OPENAI_MAX_RETRIES Retries SDK OpenAI (défaut 3)
OPENAI_TIMEOUT_MS Timeout par requête (défaut 60000)
STRIPE_SECRET_KEY Clé Stripe (pour créer les customers)
MINIO_ENDPOINT / MINIO_PORT / MINIO_USE_SSL / MINIO_ACCESS_KEY / MINIO_SECRET_KEY / MINIO_BUCKET Config MinIO ; fallback local sinon
MINIO_ALLOW_SELF_SIGNED true pour autoriser TLS auto-signé (DEV uniquement)
RESEND_API_KEY Clé Resend pour les emails

Routes API (préfixe /)

Auth (/auth)

  • POST /auth/register — Inscription email + mot de passe
  • POST /auth/login — Connexion
  • POST /auth/google-auth — Connexion/inscription via Google OAuth

Utilisateurs (/users)

  • GET /users/profile — Profil courant (🔒)
  • PUT /users/profile — Mise à jour du nom (🔒)
  • PUT /users/change-password — Changement de mot de passe (🔒)
  • PUT /users/change-email — Changement d'email (🔒)
  • POST /users/forgot-password — Demande de réinitialisation
  • POST /users/reset-password — Réinitialisation avec token
  • DELETE /users/account — Suppression du compte (🔒)

Recettes (/recipes) — toutes 🔒

  • POST /recipes/create — Upload audio + transcription + génération
  • GET /recipes/list — Liste les recettes de l'utilisateur
  • GET /recipes/:id — Détail d'une recette
  • DELETE /recipes/:id — Supprime une recette

Divers

  • GET /health — Healthcheck

🔒 = nécessite un JWT Authorization: Bearer <token>

Pipeline IA

Le module src/ai/ est isolé du reste du backend pour faciliter les itérations.

audio (multipart)
  └─→ saveAudioFile (MinIO ou local)
        └─→ transcribeAudio (gpt-4o-mini-transcribe, ~3s)
              └─→ generateRecipe (gpt-4o-mini, json_schema strict, ~3-5s)
                    ├─→ Recette structurée riche (titre, description,
                    │    inspiration, ingrédients avec notes, étapes
                    │    chronométrées, accord boisson, conseils)
                    └─→ generateRecipeImage (gpt-image-1 medium, ~6s)
                         (parallélisée avec la sérialisation JSON)

Caractéristiques :

  • Structured Outputs strict : la réponse JSON est validée côté OpenAI via json_schema. Plus jamais de JSON.parse qui casse.
  • Prompt système long et stable (~1500 tokens, persona "chef Antoine" avec règles d'originalité, de précision technique, et exemples few-shot). → Bénéficie du prompt caching automatique d'OpenAI (-50% sur la portion cachée à partir du 2ᵉ appel).
  • Retries + timeout au niveau du SDK OpenAI (configurables via env).
  • Logs de coût : chaque appel OpenAI émet un log structuré avec model, durationMs, costUsd, usage et cacheHit. Permet de suivre les coûts par utilisateur et de mesurer l'effet du cache.
  • Image best-effort : un échec de génération d'image ne casse pas la création de recette.

Sécurité

  • Helmet + rate-limit (100 req/min) activés
  • CORS whitelisté via CORS_ORIGINS
  • JWT signé, expiration 7 jours
  • Bcrypt (10 rounds) pour les mots de passe
  • Validation des variables d'environnement au démarrage

Licence

MIT

Description
No description provided
Readme 2.3 MiB
Languages
TypeScript 96.6%
CSS 1.7%
Shell 0.6%
Dockerfile 0.6%
JavaScript 0.3%
Other 0.2%