Tofu/scripts/import-recipes/README.md
2026-09-18 01:41:02 +02:00

124 lines
5.3 KiB
Markdown

# import-recipes — amorçage de la bibliothèque de recettes
Outil **jetable**, lancé une fois à la main : il lit un dossier de fichiers `.odt`
et en sort un JSON par recette, plus l'index que l'appli consomme.
C'est le seul endroit du projet où Node et npm sont autorisés. **L'appli n'en
dépend jamais** : elle lit les fichiers produits, pas ce script. Une fois la
bibliothèque importée, ce dossier peut rester à dormir.
## Prérequis
- Node 22 ou plus récent, et `npm install` **dans ce dossier** (deux dépendances :
`@anthropic-ai/sdk` pour l'extraction, `fflate` pour lire le zip d'un `.odt`).
- Une clé d'API : `ANTHROPIC_API_KEY`, ou un profil créé une fois avec
`ant auth login`. Sans clé, le script s'arrête tout de suite avec un message
clair — il ne lit aucun fichier pour rien.
- `--dry-run` ne demande aucune clé.
## Usage
```sh
npm install
node importRecipes.js --source ~/Documents/Recettes --dry-run # lecture seule, gratuit
node importRecipes.js --source ~/Documents/Recettes --limit 2 # deux recettes, pour voir
node importRecipes.js --source ~/Documents/Recettes # tout
```
| Option | Effet |
|---|---|
| `--source <dossier>` | dossier plat contenant les `.odt` (obligatoire) |
| `--out <dossier>` | où écrire (défaut : `out`) |
| `--dry-run` | lit les `.odt` et s'arrête là : aucun appel au modèle, rien d'écrit |
| `--limit N` | n'importe que les N premiers fichiers |
Une ligne par fichier s'affiche pendant le passage, puis un récapitulatif et la
liste des échecs. **Un fichier illisible n'interrompt jamais le passage** : il est
consigné et on continue. Le code de sortie vaut `1` si au moins un fichier a
échoué, `0` sinon.
Le script écrit au fur et à mesure : un passage interrompu garde ce qui a déjà
été payé.
## Ce que ça produit
```
out/
recipes/<slug>.json une recette, vocabulaire schema.org
index.json { generatedAt, recipes: [{ id, name, ingredientNames }] }
```
`out/index.json` a **exactement** la forme que l'appli attend : il se dépose tel
quel dans le dossier `Tofu/` du Nextcloud, à côté de `out/recipes/`. Sans
Nextcloud, son contenu peut aussi remplacer celui de
`app/const/RECIPE_INDEX_FIXTURE.js`.
## La règle qui compte
`recipeIngredient[i]` est **la ligne telle qu'elle est écrite dans l'ODT** : ni
reformulée, ni corrigée, ni convertie. C'est elle qui sera affichée et imprimée,
donc elle garde les mots du foyer.
Le découpage structuré vit **à côté**, dans `tofu:parsedIngredients`, et n'existe
que pour cumuler une liste de courses et pour la recherche.
`parsedIngredients[i].raw` est identique à `recipeIngredient[i]` — même ordre,
même longueur, toujours (`alignParsedIngredients` le garantit après coup, quoi
que le modèle ait répondu).
Quand la ligne est imprécise, **le découpage reste imprécis** : on garde l'unité
telle qu'écrite (`poignée`, `filet`, `pincée`) ou on met `quantity: null`, avec
`imprecise: true`. Jamais de conversion en grammes, jamais de nombre inventé — un
nombre inventé se retrouverait additionné dans une liste de courses comme si
quelqu'un l'avait mesuré. La règle est écrite noir sur blanc dans le prompt
(`extraction/EXTRACTION_RULES.js`), avec des exemples, et un test vérifie qu'elle
y est toujours.
Si le modèle rend un découpage décalé, réécrit ou incomplet, la ligne brute gagne
et l'entrée fautive est remplacée par une entrée ouvertement imprécise — le
récapitulatif signale alors `[n ingredient lines realigned]`.
## Pourquoi des clés `tofu:`
`recipeIngredient`, `recipeInstructions`, `recipeYield` viennent du vocabulaire
schema.org. Le découpage et la trace d'import n'en font pas partie : ils sont
préfixés `tofu:parsedIngredients` et `tofu:source`. Le fichier dit ainsi
lui-même ce qui est standard et ce qui est à nous.
## Ce que ça coûte
Modèle `claude-opus-5`, tarif public 5 $ par million de tokens en entrée, 25 $ en
sortie. Une recette d'une page, c'est de l'ordre de 2 000 tokens en entrée
(les règles d'extraction comptent pour l'essentiel) et 1 000 en sortie, réflexion
comprise : **grossièrement 3 à 6 centimes par recette**, soit quelques euros pour
une bibliothèque entière. Le passage affiche les tokens consommés à la fin.
Commencer par `--dry-run`, puis `--limit 2` : de quoi vérifier la mise en forme
avant de payer le reste.
## Tests
```sh
npm test # node --test, aucune dépendance de test
```
Chaque fonction pure a son test. `extractOdtText` est testée sur un **vrai**
fichier `.odt` (`tests/sample-recipe.odt`), pas sur une chaîne inventée ;
`tests/buildSampleOdt.py` le régénère si le document d'exemple doit changer.
## En option : récupérer les `.odt` depuis le Nextcloud
```sh
NEXTCLOUD_APP_PASSWORD=... node fetchOdtFromNextcloud.js \
--server https://cloud.exemple.fr --user marie --folder Recettes --out odt
```
Télécharge les `.odt` d'un dossier (PROPFIND puis GET) pour que
`importRecipes.js --source odt` les lise ensuite. Copier les fichiers à la main
fait aussi bien l'affaire. Le mot de passe attendu est un **mot de passe
d'application**, jamais celui du compte.
## Notes
- Les messages de la console sont en anglais, comme le reste du code ; seuls les
textes des recettes — ceux qui s'affichent et s'impriment — sont en français.
- `node_modules/` et `out/` sont ignorés par git.