Image generation: - Automatic model fallback: try gpt-image-1 first, fall back to dall-e-3 if it fails (e.g. org not verified on OpenAI) - Local filesystem fallback: if MinIO upload fails, write the image to backend/uploads/recipes/ and return a URL served by fastify-static - Unified handling of base64 vs URL responses from the Images API - DALL-E quality mapped automatically (low/medium/high -> standard) Local MinIO stack: - docker-compose.yml at repo root with minio + minio-init service that auto-creates the bucket and makes it publicly readable - Default credentials: freedge / freedge123 (configurable) - Console at :9001, API at :9000 - .env.example now points to the local stack by default Static file serving: - Register @fastify/static to serve ./uploads at /uploads/* - Enables local fallback to return usable URLs to the frontend - New PUBLIC_BASE_URL env var to build absolute URLs TypeScript errors (21 -> 0): - JWT typing via '@fastify/jwt' module augmentation (FastifyJWT interface with payload + user) fixes all request.user.id errors - Stripe constructor now passes required StripeConfig - fastify.createCustomer guards checked on the helper itself for proper TS narrowing (not on fastify.stripe) - Remove 'done' arg from async onClose hook - MinIO transport.agent + listFiles return type cast ignored README: - Add 'Stockage des fichiers' section explaining the two modes - Updated setup instructions to start with docker compose up Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
186 lines
7.8 KiB
Markdown
186 lines
7.8 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
# 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`)
|
|
- Bucket S3-compatible, accessible en lecture publique
|
|
- Console web : http://localhost:9001 (user `freedge`, pass `freedge123`)
|
|
- API S3 : http://localhost:9000
|
|
|
|
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
|