ordinarthur 460f7d334c feat: streaming recipe generation + user preferences + parallel image gen
Backend:
- Prisma: add user preferences (dietaryPreference, allergies, maxCookingTime,
  equipment, cuisinePreference, servingsDefault) + migration SQL.
  Also make stripeId nullable so signup works without Stripe.
- prompts.ts: buildUserPrompt now takes a BuildPromptOptions with preferences.
  Injects strong, explicit constraints in the user message (vegan rules,
  allergy warnings, time limits, equipment availability, cuisine hints).
- recipe-generator.ts: new streamRecipe() async generator. Streams OpenAI
  chat completion with json_schema strict mode, parses the growing buffer
  to detect 'titre' and 'description' early, yields typed events:
    { type: 'delta' | 'title' | 'description' | 'complete' }
  Final event includes the parsed StructuredRecipe + cost log.
- recipes.ts route: new POST /recipes/create-stream returning SSE:
    event: progress     { step }
    event: transcription{ text }
    event: title        { title }     <- triggers parallel image gen
    event: description  { description }
    event: recipe       { recipe }
    event: image        { url }
    event: saved        { id }
    event: done
  Heartbeat every 15s to prevent proxy timeouts. Image generation is
  kicked off the moment the title is extracted, running in parallel with
  the rest of the recipe stream. Legacy POST /recipes/create still works
  and now also passes user preferences.
- users.ts route: GET /profile now returns preferences (equipment
  deserialized from JSON). New PUT /users/preferences with validation
  (diet enum, time 5-600, servings 1-20, equipment array -> JSON).
- ai.ts plugin: generateRecipe signature extended to accept preferences.

Frontend:
- api/recipe.ts: createRecipeStream() async generator that consumes SSE
  via fetch + ReadableStream + TextDecoder (EventSource can't do POST).
  Parses 'event:' and 'data:' lines, yields typed StreamEvent union.
- api/auth.ts: User interface extended with preferences; new
  UserPreferences type exported.
- api/user.ts: updatePreferences() method.
- RecipeForm.tsx: handleSubmit now consumes the stream. Live UI displays:
    1. Initial cooking loader with step label
    2. Transcription appears as soon as it's ready
    3. Title fades in the moment it's extracted (before the rest of the
       recipe finishes generating)
    4. Description appears right after
    5. Image replaces the loader when ready
    6. Small delay before navigating to the saved recipe detail page
- Profile.tsx: new 'Cuisine' tab with form for diet, allergies, max time,
  servings, cuisine preference, and equipment checkboxes (8 options).

UX improvement: perceived latency is dramatically reduced. Instead of
waiting 40s staring at a spinner, the user sees the title ~3-4s in and
can start reading, while the image finishes generating in parallel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:53:55 +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

# 1. Services (MinIO) — depuis la racine du projet
docker compose up -d
# → MinIO sur :9000 (API) + :9001 (console web, user freedge / pass freedge123)
# → le bucket 'freedge' est créé automatiquement et rendu lisible publiquement

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

# 3. Variables d'environnement backend (.env dans backend/)
cp .env.example .env   # puis éditer OPENAI_API_KEY et JWT_SECRET
# Les défauts MinIO du .env.example pointent sur la stack docker-compose

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

# 5. 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 (requiert org vérifiée sur OpenAI)
OPENAI_IMAGE_FALLBACK_MODEL Défaut dall-e-3, utilisé si le principal échoue
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 ./uploads sinon
PUBLIC_BASE_URL URL publique du backend pour les fichiers locaux (défaut http://localhost:3000)
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>

Stockage des fichiers (images & audio)

Deux modes, avec fallback automatique :

  1. MinIO (recommandé, lancé via docker compose up -d)

  2. Local (fallback si MinIO est indisponible)

    • Les fichiers sont écrits dans backend/uploads/{audio,recipes}/
    • Servis par Fastify sur GET /uploads/*
    • L'URL publique retournée au frontend est ${PUBLIC_BASE_URL}/uploads/recipes/xxx.png

Le frontend n'a pas à se soucier du mode : il reçoit une URL publique dans tous les cas.

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%