diff --git a/experiments/llm-tech-step-poc/.gitignore b/experiments/llm-tech-step-poc/.gitignore new file mode 100644 index 0000000..5a2a383 --- /dev/null +++ b/experiments/llm-tech-step-poc/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +models/ diff --git a/experiments/llm-tech-step-poc/README.md b/experiments/llm-tech-step-poc/README.md new file mode 100644 index 0000000..56a9d53 --- /dev/null +++ b/experiments/llm-tech-step-poc/README.md @@ -0,0 +1,105 @@ +# PoC — détection d'actions culinaires par mini LLM local + +Expérimentation autonome, **hors du monorepo pnpm** (`pnpm-workspace.yaml` ne +référence que `apps/*`/`packages/*`) : ce dossier a son propre +`package.json`/`tsconfig.json` et ne pollue ni les dépendances ni le build +Docker de `apps/api`. + +Objectif : comparer, sur la même tâche (structurer une étape de recette en +séquence ordonnée d'actions), le pipeline `node-nlp` déjà en place +(`apps/api/src/lib/recipe-matching/tech-step-matcher.ts` — +`TechStepClassifierService`) à un mini LLM instruct tournant 100 % en local +via [`node-llama-cpp`](https://node-llama-cpp.withcat.ai/), avec sortie JSON +strictement contrainte par un schéma (GBNF grammar), sur trois axes : +précision, robustesse multilingue FR/EN, latence. + +## Installation + +```bash +cd experiments/llm-tech-step-poc +pnpm install +``` + +`node-llama-cpp` télécharge/compile son binding natif llama.cpp à +l'installation (binaire prébuilt pour les plateformes courantes, sinon +compilation locale — nécessite alors un toolchain C++, voir sa doc +["Troubleshooting"](https://node-llama-cpp.withcat.ai/guide/troubleshooting) +en cas d'échec). + +## Modèle + +Le script télécharge automatiquement (une seule fois, mis en cache dans +`experiments/llm-tech-step-poc/models/`, jamais commité) le GGUF choisi via +`LLM_TECH_STEP_MODEL` : + +| Valeur (défaut en gras) | Modèle | Pourquoi | +|---|---|---| +| **`qwen2.5-1.5b`** | Qwen2.5-1.5B-Instruct, `Q4_K_M` | Meilleure robustesse multilingue FR/EN et meilleur suivi d'instructions de structuration JSON à taille comparable — recommandation par défaut vu que "robustesse FR/EN" est un critère explicite de ce PoC. | +| `llama-3.2-1b` | Llama-3.2-1B-Instruct, `Q4_K_M` | ~35 % de paramètres en moins (plus rapide/plus léger), FR officiellement supporté, mais structuration JSON moins fiable à 1B — utile en comparaison "latence d'abord". | + +```bash +LLM_TECH_STEP_MODEL=llama-3.2-1b pnpm bench +``` + +**Hors-ligne / CI** : `LLM_TECH_STEP_MODEL_PATH=/chemin/vers/un.gguf pnpm bench` +pointe directement vers un fichier déjà téléchargé, sans passer par la +résolution/téléchargement Hugging Face. + +## Lancer le benchmark + +```bash +pnpm bench +``` + +Charge le modèle, puis lance 3 répétitions sur chacune des 3 phrases de test +(2 FR + 1 EN, voir `TEST_SENTENCES` dans +[`src/llm-tech-step-poc.ts`](./src/llm-tech-step-poc.ts) — l'une d'elles est +délibérément le cas piège documenté dans le commentaire de +`tech-step-matcher.ts` lui-même : "jusqu'à ce que le beurre ait disparu dans +la poêle", aucun verbe de cuisson littéral, seul le sens implique `COOK`). +Imprime, par phrase : le JSON détaillé de chaque action détectée, puis un +tableau récapitulatif (latence moyenne/min/max, delta RSS moyen, nombre +d'actions détectées). + +## Méthodologie de comparaison avec le pipeline `node-nlp` + +Ce script reste volontairement autonome (aucune dépendance vers `apps/api`, +donc pas de connexion Postgres requise pour le faire tourner). Pour comparer +manuellement sur les mêmes phrases : + +```ts +// Dans apps/api, un script ponctuel (ou un REPL tsx) : +import { techStepClassifier } from "./src/lib/recipe-matching/tech-step-matcher.js"; + +console.log(await techStepClassifier.matchTechStepSpans( + "Émincez finement les oignons puis faites-les revenir 10 minutes à feu moyen dans une poêle avec un filet d'huile d'olive, puis réservez.", + "fr", +)); +``` + +(nécessite une base Postgres accessible et `TechStep` seedée — voir +`apps/api/prisma/seed.ts` — puisque `matchTechStepSpans` résout ses `uid` +vers de vrais `TechStep.id`). + +Les deux sorties ne sont pas directement isomorphes (`TechStepMatch` renvoie +un `techStepId` + des spans de caractères contre un `KitchenAction` +structuré avec ingrédients/durée/température/ustensiles) — la comparaison +porte sur : le nombre d'actions/techniques détectées par phrase, si la +catégorie/technique choisie est correcte, et le comportement sur la phrase +piège FR sans verbe littéral. + +## Limites de ce PoC + +- Pas de jeu d'évaluation étiqueté ni de métrique de précision automatisée + — les 3 phrases sont inspectées à l'œil, pas notées. +- La grammaire GBNF ne garantit qu'une syntaxe JSON conforme au schéma, + jamais la justesse sémantique du contenu (catégorie choisie, durée + correctement extraite...) — voir le commentaire sur + `KITCHEN_ACTION_JSON_SCHEMA` dans le script. +- Le delta de RSS process est une approximation de la RAM réellement utilisée + par l'inférence (le binding natif alloue dans le même process, donc le RSS + la capture, mais au bruit du GC/de l'allocateur près) — pas une mesure + isolée du seul processus llama.cpp. +- Latence mesurée en CPU pur (pas de configuration GPU dans ce PoC) — un + déploiement réel voudrait évaluer l'offload GPU (`gpuLayers` dans les + options `loadModel`) si la cible dispose d'un GPU. diff --git a/experiments/llm-tech-step-poc/package.json b/experiments/llm-tech-step-poc/package.json new file mode 100644 index 0000000..babc67e --- /dev/null +++ b/experiments/llm-tech-step-poc/package.json @@ -0,0 +1,25 @@ +{ + "name": "llm-tech-step-poc", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "PoC autonome : détection d'actions culinaires dans une étape de recette via un mini LLM local (node-llama-cpp, sortie JSON contrainte par schéma), à comparer au pipeline node-nlp de apps/api/src/lib/recipe-matching/tech-step-matcher.ts.", + "scripts": { + "bench": "tsx src/llm-tech-step-poc.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "node-llama-cpp": "^3.20.0" + }, + "devDependencies": { + "@types/node": "^22.9.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild", + "node-llama-cpp" + ] + } +} diff --git a/experiments/llm-tech-step-poc/pnpm-lock.yaml b/experiments/llm-tech-step-poc/pnpm-lock.yaml new file mode 100644 index 0000000..4febaec --- /dev/null +++ b/experiments/llm-tech-step-poc/pnpm-lock.yaml @@ -0,0 +1,1398 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + node-llama-cpp: + specifier: ^3.20.0 + version: 3.20.0(typescript@5.9.3) + devDependencies: + '@types/node': + specifier: ^22.9.0 + version: 22.20.1 + tsx: + specifier: ^4.19.2 + version: 4.23.12 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + +packages: + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==, tarball: https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==, tarball: https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==, tarball: https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==, tarball: https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==, tarball: https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==, tarball: https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==, tarball: https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==, tarball: https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==, tarball: https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==, tarball: https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==, tarball: https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==, tarball: https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==, tarball: https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==, tarball: https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==, tarball: https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==, tarball: https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==, tarball: https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==, tarball: https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==, tarball: https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==, tarball: https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==, tarball: https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==, tarball: https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==, tarball: https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==, tarball: https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==, tarball: https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==, tarball: https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@huggingface/jinja@0.5.9': + resolution: {integrity: sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==, tarball: https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz} + engines: {node: '>=18'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==, tarball: https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz} + engines: {node: '>=18.0.0'} + + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==, tarball: https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz} + + '@kwsites/promise-deferred@1.1.1': + resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==, tarball: https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz} + + '@node-llama-cpp/linux-arm64@3.20.0': + resolution: {integrity: sha512-WFAffebfOLqBaZMfNsORns1G5vLMRVthxw/moDzON7TGYH6PTQN97h5YkLfRoSmZne/rtpHXH2LdYg0vFNAgnQ==, tarball: https://registry.npmjs.org/@node-llama-cpp/linux-arm64/-/linux-arm64-3.20.0.tgz} + engines: {node: '>=20.0.0'} + cpu: [arm64, x64] + os: [linux] + + '@node-llama-cpp/linux-armv7l@3.20.0': + resolution: {integrity: sha512-VUWc9U8QzgfNVcAB2BoapxBJK3wQt8EnBkstRWITTxIb4PLQjARM8Lobuz0p8gMJja4W0yWJLCN0YzkLqn51Qw==, tarball: https://registry.npmjs.org/@node-llama-cpp/linux-armv7l/-/linux-armv7l-3.20.0.tgz} + engines: {node: '>=20.0.0'} + cpu: [arm, x64] + os: [linux] + + '@node-llama-cpp/linux-riscv64@3.20.0': + resolution: {integrity: sha512-U5CV75ECl+RV8WhxKeucSyO3sjtrAudDJ3l8cBMc1V8G5CUWPpHHyS/7bL4a2l8mY+Y+LvI08VSNSiRW5GlXjw==, tarball: https://registry.npmjs.org/@node-llama-cpp/linux-riscv64/-/linux-riscv64-3.20.0.tgz} + engines: {node: '>=20.0.0'} + cpu: [riscv64] + os: [linux] + + '@node-llama-cpp/linux-x64-cuda-ext@3.20.0': + resolution: {integrity: sha512-2XwxFr0K+bLnmhcmXHeR2xM+RZ3LCisrSTJN9LHFkkctCsuZ9mw292XLq5V5b4oJxa3d28UJm5G0WJjnvVd17Q==, tarball: https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda-ext/-/linux-x64-cuda-ext-3.20.0.tgz} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + + '@node-llama-cpp/linux-x64-cuda@3.20.0': + resolution: {integrity: sha512-XWGGj12nK82NWju5+H5r/b0AY5Fv/zFZHdBwqB+JEyLSyzeGEH7Hc8P6bXBhcxNbGdCWd6wEx/EdZ5PSJrn79Q==, tarball: https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda/-/linux-x64-cuda-3.20.0.tgz} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + + '@node-llama-cpp/linux-x64-vulkan@3.20.0': + resolution: {integrity: sha512-xTzv4cuTpsmmQgqvWWZvcvURHfPgUQdQzVXvYN1bwAEYq2DRVckEKrHPJ48824sFmdIRRJqDerniqEXiZlDPzA==, tarball: https://registry.npmjs.org/@node-llama-cpp/linux-x64-vulkan/-/linux-x64-vulkan-3.20.0.tgz} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + + '@node-llama-cpp/linux-x64@3.20.0': + resolution: {integrity: sha512-zCSTd5m4MDrLWzgUvOvuGGzHm9DiEONZt+srgHYhV4Ppu/T5TL3kENj7lHY1cmQccKNgepABiyb9KcigjZbvSQ==, tarball: https://registry.npmjs.org/@node-llama-cpp/linux-x64/-/linux-x64-3.20.0.tgz} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [linux] + + '@node-llama-cpp/mac-arm64-metal@3.20.0': + resolution: {integrity: sha512-QeFyyTZWicxKGzyoYwR1VtBGM8R1/oHjai9DC6KSg3T8WYpZd3mqATy24GPPVxgg20yEXUCbNl4xyKXZsLD0dQ==, tarball: https://registry.npmjs.org/@node-llama-cpp/mac-arm64-metal/-/mac-arm64-metal-3.20.0.tgz} + engines: {node: '>=20.0.0'} + cpu: [arm64, x64] + os: [darwin] + + '@node-llama-cpp/mac-x64@3.20.0': + resolution: {integrity: sha512-3/B1uT0dNkhGTVkjTpI6OlHdUsic9NWDeocO0GHeq134LmQan34rERpR7JJgyv50iXufah+mvumFD/VZvfUqqQ==, tarball: https://registry.npmjs.org/@node-llama-cpp/mac-x64/-/mac-x64-3.20.0.tgz} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [darwin] + + '@node-llama-cpp/win-arm64@3.20.0': + resolution: {integrity: sha512-UDx5NBXVRtcLaoQsF1gZiYlgXQYfxLbFVb4j4sa7Jgq/b6oq2lTAjeos7sYsMPRKa+S/BB/rDFis6FsRDJur7w==, tarball: https://registry.npmjs.org/@node-llama-cpp/win-arm64/-/win-arm64-3.20.0.tgz} + engines: {node: '>=20.0.0'} + cpu: [arm64, x64] + os: [win32] + + '@node-llama-cpp/win-x64-cuda-ext@3.20.0': + resolution: {integrity: sha512-BTnHmJ7xTzrvv8CWGYnG+2eygjZ4xAvQujtvkfC8N187bRR1fmTA+R7dbrqC2b3fXKtOvyPwOB9NNl+d2FuCmQ==, tarball: https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda-ext/-/win-x64-cuda-ext-3.20.0.tgz} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [win32] + + '@node-llama-cpp/win-x64-cuda@3.20.0': + resolution: {integrity: sha512-VPV0Ayw3TP9gGiuwlyWD5x97dQSNMKcbWFXsREJ0KLVRxJXihZdZTrjRaudQX1T1d0lJUeLhDmQUGpDh/8jf1w==, tarball: https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda/-/win-x64-cuda-3.20.0.tgz} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [win32] + + '@node-llama-cpp/win-x64-vulkan@3.20.0': + resolution: {integrity: sha512-7V2SjNejon668+xmtlZ36u2FmtIT2fOfQbGjT5zJ6ydW1Xec53VsX1VwQdikx+79Y9/gEp7L2GtBcX6bc0LsCQ==, tarball: https://registry.npmjs.org/@node-llama-cpp/win-x64-vulkan/-/win-x64-vulkan-3.20.0.tgz} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [win32] + + '@node-llama-cpp/win-x64@3.20.0': + resolution: {integrity: sha512-Mbh9n74DCB5zTw02cme7Kp9nVg9X5Wvf+SNMWkXx5o3rGLuiSijGqRIktPOO3aHQwshB/RXC4j6I34HCPsgdgg==, tarball: https://registry.npmjs.org/@node-llama-cpp/win-x64/-/win-x64-3.20.0.tgz} + engines: {node: '>=20.0.0'} + cpu: [x64] + os: [win32] + + '@reflink/reflink-darwin-arm64@0.1.19': + resolution: {integrity: sha512-ruy44Lpepdk1FqDz38vExBY/PVUsjxZA+chd9wozjUH9JjuDT/HEaQYA6wYN9mf041l0yLVar6BCZuWABJvHSA==, tarball: https://registry.npmjs.org/@reflink/reflink-darwin-arm64/-/reflink-darwin-arm64-0.1.19.tgz} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@reflink/reflink-darwin-x64@0.1.19': + resolution: {integrity: sha512-By85MSWrMZa+c26TcnAy8SDk0sTUkYlNnwknSchkhHpGXOtjNDUOxJE9oByBnGbeuIE1PiQsxDG3Ud+IVV9yuA==, tarball: https://registry.npmjs.org/@reflink/reflink-darwin-x64/-/reflink-darwin-x64-0.1.19.tgz} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@reflink/reflink-linux-arm64-gnu@0.1.19': + resolution: {integrity: sha512-7P+er8+rP9iNeN+bfmccM4hTAaLP6PQJPKWSA4iSk2bNvo6KU6RyPgYeHxXmzNKzPVRcypZQTpFgstHam6maVg==, tarball: https://registry.npmjs.org/@reflink/reflink-linux-arm64-gnu/-/reflink-linux-arm64-gnu-0.1.19.tgz} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@reflink/reflink-linux-arm64-musl@0.1.19': + resolution: {integrity: sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==, tarball: https://registry.npmjs.org/@reflink/reflink-linux-arm64-musl/-/reflink-linux-arm64-musl-0.1.19.tgz} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@reflink/reflink-linux-x64-gnu@0.1.19': + resolution: {integrity: sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==, tarball: https://registry.npmjs.org/@reflink/reflink-linux-x64-gnu/-/reflink-linux-x64-gnu-0.1.19.tgz} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@reflink/reflink-linux-x64-musl@0.1.19': + resolution: {integrity: sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==, tarball: https://registry.npmjs.org/@reflink/reflink-linux-x64-musl/-/reflink-linux-x64-musl-0.1.19.tgz} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@reflink/reflink-win32-arm64-msvc@0.1.19': + resolution: {integrity: sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==, tarball: https://registry.npmjs.org/@reflink/reflink-win32-arm64-msvc/-/reflink-win32-arm64-msvc-0.1.19.tgz} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@reflink/reflink-win32-x64-msvc@0.1.19': + resolution: {integrity: sha512-E//yT4ni2SyhwP8JRjVGWr3cbnhWDiPLgnQ66qqaanjjnMiu3O/2tjCPQXlcGc/DEYofpDc9fvhv6tALQsMV9w==, tarball: https://registry.npmjs.org/@reflink/reflink-win32-x64-msvc/-/reflink-win32-x64-msvc-0.1.19.tgz} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@reflink/reflink@0.1.19': + resolution: {integrity: sha512-DmCG8GzysnCZ15bres3N5AHCmwBwYgp0As6xjhQ47rAUTUXxJiK+lLUxaGsX3hd/30qUpVElh05PbGuxRPgJwA==, tarball: https://registry.npmjs.org/@reflink/reflink/-/reflink-0.1.19.tgz} + engines: {node: '>= 10'} + + '@simple-git/args-pathspec@1.0.3': + resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==, tarball: https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz} + + '@simple-git/argv-parser@1.1.1': + resolution: {integrity: sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==, tarball: https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz} + + '@tinyhttp/content-disposition@2.2.4': + resolution: {integrity: sha512-5Kc5CM2Ysn3vTTArBs2vESUt0AQiWZA86yc1TI3B+lxXmtEq133C1nxXNOgnzhrivdPZIh3zLj5gDnZjoLL5GA==, tarball: https://registry.npmjs.org/@tinyhttp/content-disposition/-/content-disposition-2.2.4.tgz} + engines: {node: '>=12.17.0'} + + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==, tarball: https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz} + + ansi-escapes@6.2.1: + resolution: {integrity: sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==, tarball: https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz} + engines: {node: '>=14.16'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, tarball: https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz} + engines: {node: '>=8'} + + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==, tarball: https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, tarball: https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==, tarball: https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz} + engines: {node: '>=12'} + + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==, tarball: https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==, tarball: https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz} + engines: {node: '>= 0.8'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==, tarball: https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chmodrp@1.0.2: + resolution: {integrity: sha512-TdngOlFV1FLTzU0o1w8MB6/BFywhtLC0SzRTGJU7T9lmdjlCWeMRt1iVo0Ki+ldwNk0BqNiKoc8xpLZEQ8mY1w==, tarball: https://registry.npmjs.org/chmodrp/-/chmodrp-1.0.2.tgz} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==, tarball: https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz} + engines: {node: '>=18'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==, tarball: https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz} + engines: {node: '>=8'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==, tarball: https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==, tarball: https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz} + engines: {node: '>=6'} + + cli-spinners@3.4.0: + resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==, tarball: https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz} + engines: {node: '>=18.20'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==, tarball: https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz} + engines: {node: '>=12'} + + cmake-js@8.0.0: + resolution: {integrity: sha512-YbUP88RDwCvoQkZhRtGURYm9RIpWdtvZuhT87fKNoLjk8kIFIFeARpKfuZQGdwfH99GZpUmqSfcDrK62X7lTgg==, tarball: https://registry.npmjs.org/cmake-js/-/cmake-js-8.0.0.tgz} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, tarball: https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, tarball: https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==, tarball: https://registry.npmjs.org/commander/-/commander-10.0.1.tgz} + engines: {node: '>=14'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, tarball: https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, tarball: https://registry.npmjs.org/debug/-/debug-4.4.3.tgz} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==, tarball: https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz} + engines: {node: '>=4.0.0'} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==, tarball: https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==, tarball: https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz} + + env-var@7.5.0: + resolution: {integrity: sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA==, tarball: https://registry.npmjs.org/env-var/-/env-var-7.5.0.tgz} + engines: {node: '>=10'} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==, tarball: https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, tarball: https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz} + engines: {node: '>=6'} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==, tarball: https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz} + + filename-reserved-regex@3.0.0: + resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==, tarball: https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + filenamify@6.0.0: + resolution: {integrity: sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==, tarball: https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz} + engines: {node: '>=16'} + + fs-extra@11.4.0: + resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==, tarball: https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, tarball: https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==, tarball: https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==, tarball: https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, tarball: https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz} + + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==, tarball: https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz} + engines: {node: '>= 4'} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==, tarball: https://registry.npmjs.org/ini/-/ini-1.3.8.tgz} + + ipull@3.9.5: + resolution: {integrity: sha512-5w/yZB5lXmTfsvNawmvkCjYo4SJNuKQz/av8TC1UiOyfOHyaM+DReqbpU2XpWYfmY+NIUbRRH8PUAWsxaS+IfA==, tarball: https://registry.npmjs.org/ipull/-/ipull-3.9.5.tgz} + engines: {node: '>=18.0.0'} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==, tarball: https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz} + engines: {node: '>=8'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==, tarball: https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz} + engines: {node: '>=18'} + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==, tarball: https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==, tarball: https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, tarball: https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz} + + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==, tarball: https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz} + engines: {node: '>=20'} + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==, tarball: https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz} + + lifecycle-utils@2.1.0: + resolution: {integrity: sha512-AnrXnE2/OF9PHCyFg0RSqsnQTzV991XaZA/buhFDoc58xU7rhSCDgCz/09Lqpsn4MpoPHt7TRAXV1kWZypFVsA==, tarball: https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-2.1.0.tgz} + + lifecycle-utils@4.3.1: + resolution: {integrity: sha512-sHZNkjBVcR0e/2indpmvpOj++4e1mUyniN94wqdbtQaXYoAJRlYxpOJ3TwBmp7PwgHqsoAkiTQu+aLa9bss7Jw==, tarball: https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-4.3.1.tgz} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==, tarball: https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz} + + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==, tarball: https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz} + engines: {node: '>=18'} + + lowdb@7.0.1: + resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==, tarball: https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz} + engines: {node: '>=18'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==, tarball: https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz} + engines: {node: '>=18'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, tarball: https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==, tarball: https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==, tarball: https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz} + engines: {node: '>= 18'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, tarball: https://registry.npmjs.org/ms/-/ms-2.1.3.tgz} + + nanoid@5.1.16: + resolution: {integrity: sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==, tarball: https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz} + engines: {node: ^18 || >=20} + hasBin: true + + node-addon-api@8.9.2: + resolution: {integrity: sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==, tarball: https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz} + engines: {node: ^18 || ^20 || >= 21} + + node-api-headers@1.9.0: + resolution: {integrity: sha512-2oNILP4jXwRB4ywnYKjVk1YyJ96n2D4EOVJO6S3oYZ5PtbJrw3Yt9TpAuX3nBLMuzn74rnfGQrv13pS9vC+YiA==, tarball: https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.9.0.tgz} + + node-llama-cpp@3.20.0: + resolution: {integrity: sha512-KnET3ttADYLCobjMnMTLkWkLt87rPRDhNzTZgGOCt8m8yTmdQW2sfLNmulGBA9laFvcyRIMOsgQST8lunPmMgw==, tarball: https://registry.npmjs.org/node-llama-cpp/-/node-llama-cpp-3.20.0.tgz} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==, tarball: https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz} + engines: {node: '>=18'} + + ora@9.4.1: + resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==, tarball: https://registry.npmjs.org/ora/-/ora-9.4.1.tgz} + engines: {node: '>=20'} + + parse-ms@3.0.0: + resolution: {integrity: sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==, tarball: https://registry.npmjs.org/parse-ms/-/parse-ms-3.0.0.tgz} + engines: {node: '>=12'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==, tarball: https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz} + engines: {node: '>=18'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, tarball: https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz} + engines: {node: '>=8'} + + pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==, tarball: https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz} + engines: {node: ^14.13.1 || >=16.0.0} + + pretty-ms@8.0.0: + resolution: {integrity: sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==, tarball: https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz} + engines: {node: '>=14.16'} + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==, tarball: https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz} + engines: {node: '>=18'} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==, tarball: https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==, tarball: https://registry.npmjs.org/rc/-/rc-1.2.8.tgz} + hasBin: true + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==, tarball: https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz} + engines: {node: '>=0.10.0'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==, tarball: https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz} + engines: {node: '>=18'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==, tarball: https://registry.npmjs.org/retry/-/retry-0.12.0.tgz} + engines: {node: '>= 4'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==, tarball: https://registry.npmjs.org/retry/-/retry-0.13.1.tgz} + engines: {node: '>= 4'} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==, tarball: https://registry.npmjs.org/semver/-/semver-7.8.5.tgz} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, tarball: https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, tarball: https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==, tarball: https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==, tarball: https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz} + engines: {node: '>=14'} + + simple-git@3.36.0: + resolution: {integrity: sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==, tarball: https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz} + + sleep-promise@9.1.0: + resolution: {integrity: sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA==, tarball: https://registry.npmjs.org/sleep-promise/-/sleep-promise-9.1.0.tgz} + + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==, tarball: https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz} + engines: {node: '>=18'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==, tarball: https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz} + engines: {node: '>=20'} + + stdin-discarder@0.3.2: + resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==, tarball: https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz} + engines: {node: '>=18'} + + stdout-update@4.0.1: + resolution: {integrity: sha512-wiS21Jthlvl1to+oorePvcyrIkiG/6M3D3VTmDUlJm7Cy6SbFhKkAvX+YBuHLxck/tO3mrdpC/cNesigQc3+UQ==, tarball: https://registry.npmjs.org/stdout-update/-/stdout-update-4.0.1.tgz} + engines: {node: '>=16.0.0'} + + steno@4.0.2: + resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==, tarball: https://registry.npmjs.org/steno/-/steno-4.0.2.tgz} + engines: {node: '>=18'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==, tarball: https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==, tarball: https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz} + engines: {node: '>=18'} + + string-width@8.2.2: + resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==, tarball: https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz} + engines: {node: '>=20'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, tarball: https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==, tarball: https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz} + engines: {node: '>=12'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==, tarball: https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz} + engines: {node: '>=0.10.0'} + + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==, tarball: https://registry.npmjs.org/tar/-/tar-7.5.22.tgz} + engines: {node: '>=18'} + + tsx@4.23.12: + resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==, tarball: https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, tarball: https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==, tarball: https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz} + engines: {node: '>= 10.0.0'} + + url-join@4.0.1: + resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==, tarball: https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz} + + validate-npm-package-name@7.0.2: + resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==, tarball: https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz} + engines: {node: ^20.17.0 || >=22.9.0} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, tarball: https://registry.npmjs.org/which/-/which-2.0.2.tgz} + engines: {node: '>= 8'} + hasBin: true + + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==, tarball: https://registry.npmjs.org/which/-/which-6.0.1.tgz} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==, tarball: https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz} + engines: {node: '>=10'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==, tarball: https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz} + engines: {node: '>=10'} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==, tarball: https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz} + engines: {node: '>=18'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==, tarball: https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==, tarball: https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz} + engines: {node: '>=12'} + + yoctocolors@2.2.0: + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==, tarball: https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz} + engines: {node: '>=18'} + +snapshots: + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@huggingface/jinja@0.5.9': {} + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@kwsites/promise-deferred@1.1.1': {} + + '@node-llama-cpp/linux-arm64@3.20.0': + optional: true + + '@node-llama-cpp/linux-armv7l@3.20.0': + optional: true + + '@node-llama-cpp/linux-riscv64@3.20.0': + optional: true + + '@node-llama-cpp/linux-x64-cuda-ext@3.20.0': + optional: true + + '@node-llama-cpp/linux-x64-cuda@3.20.0': + optional: true + + '@node-llama-cpp/linux-x64-vulkan@3.20.0': + optional: true + + '@node-llama-cpp/linux-x64@3.20.0': + optional: true + + '@node-llama-cpp/mac-arm64-metal@3.20.0': + optional: true + + '@node-llama-cpp/mac-x64@3.20.0': + optional: true + + '@node-llama-cpp/win-arm64@3.20.0': + optional: true + + '@node-llama-cpp/win-x64-cuda-ext@3.20.0': + optional: true + + '@node-llama-cpp/win-x64-cuda@3.20.0': + optional: true + + '@node-llama-cpp/win-x64-vulkan@3.20.0': + optional: true + + '@node-llama-cpp/win-x64@3.20.0': + optional: true + + '@reflink/reflink-darwin-arm64@0.1.19': + optional: true + + '@reflink/reflink-darwin-x64@0.1.19': + optional: true + + '@reflink/reflink-linux-arm64-gnu@0.1.19': + optional: true + + '@reflink/reflink-linux-arm64-musl@0.1.19': + optional: true + + '@reflink/reflink-linux-x64-gnu@0.1.19': + optional: true + + '@reflink/reflink-linux-x64-musl@0.1.19': + optional: true + + '@reflink/reflink-win32-arm64-msvc@0.1.19': + optional: true + + '@reflink/reflink-win32-x64-msvc@0.1.19': + optional: true + + '@reflink/reflink@0.1.19': + optionalDependencies: + '@reflink/reflink-darwin-arm64': 0.1.19 + '@reflink/reflink-darwin-x64': 0.1.19 + '@reflink/reflink-linux-arm64-gnu': 0.1.19 + '@reflink/reflink-linux-arm64-musl': 0.1.19 + '@reflink/reflink-linux-x64-gnu': 0.1.19 + '@reflink/reflink-linux-x64-musl': 0.1.19 + '@reflink/reflink-win32-arm64-msvc': 0.1.19 + '@reflink/reflink-win32-x64-msvc': 0.1.19 + optional: true + + '@simple-git/args-pathspec@1.0.3': {} + + '@simple-git/argv-parser@1.1.1': + dependencies: + '@simple-git/args-pathspec': 1.0.3 + + '@tinyhttp/content-disposition@2.2.4': {} + + '@types/node@22.20.1': + dependencies: + undici-types: 6.21.0 + + ansi-escapes@6.2.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.3.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + + bytes@3.1.2: {} + + chalk@5.6.2: {} + + chmodrp@1.0.2: {} + + chownr@3.0.0: {} + + ci-info@4.4.0: {} + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cli-spinners@3.4.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cmake-js@8.0.0: + dependencies: + debug: 4.4.3 + fs-extra: 11.4.0 + node-api-headers: 1.9.0 + rc: 1.2.8 + semver: 7.8.5 + tar: 7.5.22 + url-join: 4.0.1 + which: 6.0.1 + yargs: 17.7.3 + transitivePeerDependencies: + - supports-color + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@10.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-extend@0.6.0: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + env-var@7.5.0: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + eventemitter3@5.0.4: {} + + filename-reserved-regex@3.0.0: {} + + filenamify@6.0.0: + dependencies: + filename-reserved-regex: 3.0.0 + + fs-extra@11.4.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + + graceful-fs@4.2.11: {} + + ignore@7.0.6: {} + + ini@1.3.8: {} + + ipull@3.9.5: + dependencies: + '@tinyhttp/content-disposition': 2.2.4 + async-retry: 1.3.3 + chalk: 5.6.2 + ci-info: 4.4.0 + cli-spinners: 2.9.2 + commander: 10.0.1 + eventemitter3: 5.0.4 + filenamify: 6.0.0 + fs-extra: 11.4.0 + is-unicode-supported: 2.1.0 + lifecycle-utils: 2.1.0 + lodash.debounce: 4.0.8 + lowdb: 7.0.1 + pretty-bytes: 6.1.1 + pretty-ms: 8.0.0 + sleep-promise: 9.1.0 + slice-ansi: 7.1.2 + stdout-update: 4.0.1 + strip-ansi: 7.2.0 + optionalDependencies: + '@reflink/reflink': 0.1.19 + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.6.0 + + is-interactive@2.0.0: {} + + is-unicode-supported@2.1.0: {} + + isexe@2.0.0: {} + + isexe@4.0.0: {} + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + lifecycle-utils@2.1.0: {} + + lifecycle-utils@4.3.1: {} + + lodash.debounce@4.0.8: {} + + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.2.0 + + lowdb@7.0.1: + dependencies: + steno: 4.0.2 + + mimic-function@5.0.1: {} + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + + ms@2.1.3: {} + + nanoid@5.1.16: {} + + node-addon-api@8.9.2: {} + + node-api-headers@1.9.0: {} + + node-llama-cpp@3.20.0(typescript@5.9.3): + dependencies: + '@huggingface/jinja': 0.5.9 + async-retry: 1.3.3 + bytes: 3.1.2 + chalk: 5.6.2 + chmodrp: 1.0.2 + cmake-js: 8.0.0 + cross-spawn: 7.0.6 + env-var: 7.5.0 + filenamify: 6.0.0 + fs-extra: 11.4.0 + ignore: 7.0.6 + ipull: 3.9.5 + is-unicode-supported: 2.1.0 + lifecycle-utils: 4.3.1 + log-symbols: 7.0.1 + nanoid: 5.1.16 + node-addon-api: 8.9.2 + ora: 9.4.1 + pretty-ms: 9.3.0 + proper-lockfile: 4.1.2 + semver: 7.8.5 + simple-git: 3.36.0 + slice-ansi: 8.0.0 + stdout-update: 4.0.1 + strip-ansi: 7.2.0 + validate-npm-package-name: 7.0.2 + which: 6.0.1 + yargs: 17.7.3 + optionalDependencies: + '@node-llama-cpp/linux-arm64': 3.20.0 + '@node-llama-cpp/linux-armv7l': 3.20.0 + '@node-llama-cpp/linux-riscv64': 3.20.0 + '@node-llama-cpp/linux-x64': 3.20.0 + '@node-llama-cpp/linux-x64-cuda': 3.20.0 + '@node-llama-cpp/linux-x64-cuda-ext': 3.20.0 + '@node-llama-cpp/linux-x64-vulkan': 3.20.0 + '@node-llama-cpp/mac-arm64-metal': 3.20.0 + '@node-llama-cpp/mac-x64': 3.20.0 + '@node-llama-cpp/win-arm64': 3.20.0 + '@node-llama-cpp/win-x64': 3.20.0 + '@node-llama-cpp/win-x64-cuda': 3.20.0 + '@node-llama-cpp/win-x64-cuda-ext': 3.20.0 + '@node-llama-cpp/win-x64-vulkan': 3.20.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + ora@9.4.1: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 3.4.0 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 7.0.1 + stdin-discarder: 0.3.2 + string-width: 8.2.2 + + parse-ms@3.0.0: {} + + parse-ms@4.0.0: {} + + path-key@3.1.1: {} + + pretty-bytes@6.1.1: {} + + pretty-ms@8.0.0: + dependencies: + parse-ms: 3.0.0 + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + require-directory@2.1.1: {} + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + retry@0.12.0: {} + + retry@0.13.1: {} + + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-git@3.36.0: + dependencies: + '@kwsites/file-exists': 1.1.1 + '@kwsites/promise-deferred': 1.1.1 + '@simple-git/args-pathspec': 1.0.3 + '@simple-git/argv-parser': 1.1.1 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + sleep-promise@9.1.0: {} + + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + stdin-discarder@0.3.2: {} + + stdout-update@4.0.1: + dependencies: + ansi-escapes: 6.2.1 + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + steno@4.0.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string-width@8.2.2: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.3.0 + + strip-json-comments@2.0.1: {} + + tar@7.5.22: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + + tsx@4.23.12: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + universalify@2.0.1: {} + + url-join@4.0.1: {} + + validate-npm-package-name@7.0.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@6.0.1: + dependencies: + isexe: 4.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + y18n@5.0.8: {} + + yallist@5.0.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yoctocolors@2.2.0: {} diff --git a/experiments/llm-tech-step-poc/src/llm-tech-step-poc.ts b/experiments/llm-tech-step-poc/src/llm-tech-step-poc.ts new file mode 100644 index 0000000..f16d048 --- /dev/null +++ b/experiments/llm-tech-step-poc/src/llm-tech-step-poc.ts @@ -0,0 +1,567 @@ +/** + * PoC autonome — détection d'actions culinaires via un mini LLM local + * (`node-llama-cpp`), à comparer au pipeline `node-nlp` déjà en place dans + * `apps/api/src/lib/recipe-matching/tech-step-matcher.ts` + * (`TechStepClassifierService`). + * + * Objectif de la comparaison : ce pipeline `node-nlp` classe une étape en + * UNE technique par clause (NER pour repérer les candidats -> découpage en + * clauses -> classification d'intention par clause). Ce PoC teste une + * approche différente : demander à un petit LLM instruct local d'extraire + * en une seule passe la séquence ORDONNÉE de toutes les actions atomiques + * d'une étape, sous forme d'un JSON structuré — sans borne de vocabulaire + * fixée à l'avance (pas de liste de synonymes/utterances à maintenir), au + * prix d'une latence et d'une empreinte mémoire bien plus élevées (un + * modèle de ~1 à 2 Md de paramètres contre un classifieur bayésien/NER + * léger). Les deux pipelines tournent entièrement en local, sans appel + * réseau à l'inférence (le seul accès réseau de ce fichier est le + * téléchargement ponctuel du modèle GGUF, voir {@link resolveModelPath}). + * + * Portée volontairement limitée à un fichier autonome, hors du monorepo + * pnpm (`pnpm-workspace.yaml` ne référence que `apps/*`/`packages/*`) : + * c'est un script d'expérimentation jetable, pas un module destiné à être + * consommé par `apps/api` — même statut que les scripts one-off déjà + * exemptés de la convention "un `try`/`catch` par `await`" du repo + * (`prisma/seed.ts`, `apps/api/src/scripts/seed-runtime.ts`) : ici aussi, + * laisser une erreur se propager telle quelle jusqu'au point d'appel qui + * décide quoi en faire (le run complet du benchmark, ou `main()` en tout + * dernier ressort) est plus lisible qu'un `catch { throw err; }` répété + * sans rien y ajouter. + * + * Usage : voir `README.md` à côté de ce fichier (installation, modèle, + * variables d'environnement). En bref : + * + * ```bash + * cd experiments/llm-tech-step-poc + * pnpm install + * pnpm bench + * ``` + */ + +import path from "node:path"; +import { performance } from "node:perf_hooks"; +import { fileURLToPath } from "node:url"; +import { + getLlama, + LlamaChatSession, + type LlamaJsonSchemaGrammar, + resolveModelFile, +} from "node-llama-cpp"; + +// --------------------------------------------------------------------------- +// Types métier — RecipeStepAnalysis / KitchenAction +// --------------------------------------------------------------------------- + +/** + * Taxonomie fermée des actions culinaires que le LLM peut poser sur une + * action extraite. Volontairement large (`OTHER` en filet de sécurité) plutôt + * qu'exhaustive comme les ~25 `TechStep` de la base : ce PoC teste la + * *structuration* d'une étape en séquence d'actions typées, pas encore un + * remplacement à iso-vocabulaire du catalogue `TechStep` existant. + */ +export enum KitchenActionType { + /** Travail au couteau — émincer, couper en dés, hacher, éplucher, trancher. */ + CUT = "CUT", + /** Cuisson à proprement parler — faire revenir, mijoter, bouillir, cuire au four, griller, fondre. */ + COOK = "COOK", + /** Combiner/mélanger des ingrédients entre eux, sans cuisson — mélanger, fouetter, incorporer. */ + MIX = "MIX", + /** Laisser reposer/refroidir/mariner/lever, sans intervention active. */ + REST = "REST", + /** Assaisonner — sel, poivre, épices, herbes, condiments. */ + SEASON = "SEASON", + /** Préchauffage d'un four, d'une poêle ou d'un appareil avant utilisation. */ + PREHEAT = "PREHEAT", + /** Toute action ne rentrant dans aucune des catégories ci-dessus (dresser, égoutter, réserver, transférer...). */ + OTHER = "OTHER", +} + +/** + * Une action atomique extraite d'une étape de recette — l'équivalent, côté + * LLM, de ce qu'un `TechStepMatch` (`tech-step-matcher.ts`) représente côté + * pipeline `node-nlp`, mais enrichi des attributs qu'un LLM générativiste + * peut extraire en une seule passe (ingrédients, durée, température, + * ustensiles) là où le pipeline `node-nlp` ne renvoie qu'un id de technique + * plus ses spans de texte. + */ +export interface KitchenAction { + /** Catégorie de l'action, parmi {@link KitchenActionType}. */ + action: KitchenActionType; + /** Verbe littéral employé dans le texte (langue d'origine, non traduit) — ex. "émincez", "dice". */ + verb: string; + /** Ingrédients sur lesquels porte spécifiquement cette action ; tableau vide si aucun n'est nommé. */ + ingredients: string[]; + /** Durée en minutes si l'étape en mentionne une (heures/secondes converties) ; `null` sinon. */ + durationMinutes: number | null; + /** Mention littérale de température/intensité de feu (ex. "180°C", "feu doux", "medium heat") ; `null` sinon. */ + temperature: string | null; + /** Ustensiles/équipements nommés pour cette action ; tableau vide si aucun n'est nommé. */ + utensils: string[]; +} + +/** + * Résultat complet de l'analyse d'une étape — la séquence ORDONNÉE + * d'actions qu'elle décrit, alignée sur le texte source pour traçabilité + * dans les résultats du benchmark. + * + * `originalText` n'est volontairement PAS demandé au LLM (donc pas dans le + * schéma JSON imposé par la grammaire, voir {@link KITCHEN_ACTIONS_JSON_SCHEMA}) + * : le faire recopier le texte d'entrée gaspillerait des tokens de + * génération et risquerait une recopie légèrement différente de l'original + * (espaces, ponctuation) sans aucun bénéfice — ce champ est réattaché + * programmatiquement par {@link LocalLlmStepAnalyzer.analyzeStep} à partir + * de l'argument d'entrée, pas de la réponse du modèle. + */ +export interface RecipeStepAnalysis { + /** Texte source de l'étape, tel que passé à `analyzeStep`. */ + originalText: string; + /** Séquence ordonnée d'actions détectées ; vide si l'étape n'en décrit aucune. */ + actions: KitchenAction[]; +} + +// --------------------------------------------------------------------------- +// Schéma JSON — grammaire GBNF imposée à la génération +// --------------------------------------------------------------------------- + +/** + * Schéma JSON d'une {@link KitchenAction}, dans le sous-ensemble supporté + * par `LlamaChatSession`+`llama.createGrammarForJsonSchema` (object/array/ + * string/number/enum/oneOf — pas d'union `type: [...]` pour les champs + * nullable, node-llama-cpp veut `oneOf: [{type:"null"}, {type:"..."}]`, + * voir la doc "Using Grammar"). Champ à champ, en miroir strict de + * {@link KitchenAction} : la grammaire ne fait qu'imposer une SYNTAXE JSON + * valide conforme à ce schéma, elle ne garantit pas que le modèle choisisse + * la bonne catégorie/le bon champ — c'est le rôle du prompt système + * ({@link SYSTEM_PROMPT}) de guider la sémantique. + */ +const KITCHEN_ACTION_JSON_SCHEMA = { + type: "object", + properties: { + action: { enum: Object.values(KitchenActionType) }, + verb: { type: "string" }, + ingredients: { type: "array", items: { type: "string" } }, + durationMinutes: { oneOf: [{ type: "null" }, { type: "number" }] }, + temperature: { oneOf: [{ type: "null" }, { type: "string" }] }, + utensils: { type: "array", items: { type: "string" } }, + }, + required: ["action", "verb", "ingredients", "durationMinutes", "temperature", "utensils"], +} as const; + +/** + * Racine du schéma imposé au modèle — un objet `{ actions: [...] }` plutôt + * qu'un tableau nu en racine (node-llama-cpp exige un `type: "object"` en + * racine de la grammaire JSON). `originalText` n'y figure pas, voir le + * commentaire sur {@link RecipeStepAnalysis.originalText}. + */ +const KITCHEN_ACTIONS_JSON_SCHEMA = { + type: "object", + properties: { + actions: { type: "array", items: KITCHEN_ACTION_JSON_SCHEMA }, + }, + required: ["actions"], +} as const; + +/** Forme brute que renvoie `grammar.parse()` pour {@link KITCHEN_ACTIONS_JSON_SCHEMA} — reconverti en {@link RecipeStepAnalysis} par {@link LocalLlmStepAnalyzer.analyzeStep}. */ +interface KitchenActionsGrammarResult { + actions: KitchenAction[]; +} + +/** + * Instructions système — porte toute la sémantique que la grammaire GBNF ne + * peut pas imposer (elle ne contraint que la forme JSON, jamais le + * contenu) : la définition de chaque catégorie de {@link KitchenActionType}, + * et ce qu'extraire pour chaque champ. Explicitement bilingue dans son + * énoncé même (plutôt que deux prompts FR/EN séparés à maintenir) — le but + * du benchmark est justement de voir si un seul prompt, sur un modèle + * multilingue, tient la route en français ET en anglais sans bascule + * explicite de langue. + */ +const SYSTEM_PROMPT = `You are a culinary instruction parser. You receive ONE recipe step, written in either French or English. Break it down into the ordered sequence of atomic actions it describes, and respond with ONLY the JSON object required by the schema — no prose, no markdown code fences, no explanation. + +Action taxonomy (pick exactly one per action): +- CUT: knife work — chopping, dicing, mincing, slicing, peeling. +- COOK: applying heat to actually cook food — frying, sautéing, simmering, boiling, baking, grilling, melting. +- MIX: combining/stirring/whisking/folding ingredients together, with no heat involved. +- REST: letting something sit, cool, chill, marinate or rise without active handling. +- SEASON: adding salt, pepper, spices, herbs or condiments to flavor a dish. +- PREHEAT: bringing an oven, pan or appliance up to temperature before it is used. +- OTHER: anything not covered above (plating, straining, transferring, reserving...). + +For each action extract: +- verb: the literal action verb from the source text, in its original language. +- ingredients: the ingredients this specific action applies to (empty array if none named). +- durationMinutes: a single number in minutes if a duration is stated (convert hours/seconds), otherwise null. +- temperature: the literal temperature/heat-level mention (e.g. "180°C", "feu doux", "medium heat"), otherwise null. +- utensils: any cookware/tools named for this action (empty array if none named). + +Keep the actions in the order they happen in the text. A step can describe several sequential actions, even when no explicit verb names a technique (e.g. "until the butter has disappeared into the pan" means melting butter, i.e. COOK).`; + +// --------------------------------------------------------------------------- +// Modèles recommandés +// --------------------------------------------------------------------------- + +/** Une des deux familles de modèles GGUF évaluées par ce PoC — voir le comparatif dans `README.md`. */ +export type RecommendedModelKey = "qwen2.5-1.5b" | "llama-3.2-1b"; + +/** Un modèle GGUF candidat, référencé par son URI `hf:` (résolu/téléchargé par `resolveModelFile`, voir la doc "Downloading Models" de node-llama-cpp). */ +interface RecommendedModel { + /** URI `hf::` — node-llama-cpp résout et télécharge (une seule fois, mis en cache) le fichier GGUF correspondant depuis Hugging Face. */ + hfUri: string; + /** Pourquoi ce modèle, en une phrase — voir aussi le comparatif détaillé dans `README.md`. */ + rationale: string; +} + +/** + * Les deux modèles recommandés pour cette tâche, choisis parmi les + * instruct GGUF ~1-1.5 Md de paramètres (assez petits pour tourner en CPU + * pur avec une latence de l'ordre de la seconde, assez récents pour bien + * suivre des instructions de structuration JSON) : + * + * - **Qwen2.5-1.5B-Instruct** (recommandation par défaut) : corpus + * d'entraînement nettement plus multilingue que la famille Llama à + * taille comparable, et meilleur suivi d'instructions de structuration + * (extraction JSON, function calling) dans les benchmarks publiés par + * Qwen — le compromis précision/latence le plus favorable ici vu que le + * critère "robustesse FR/EN" est un objectif explicite du PoC. + * - **Llama-3.2-1B-Instruct** (alternative) : ~35 % de paramètres en + * moins, donc plus rapide et plus léger en RAM ; le FR fait partie de ses + * langues officiellement supportées, mais avec un suivi d'instructions de + * structuration plus fragile à cette taille dans la pratique — utile + * comme point de comparaison "latence d'abord" plutôt que comme premier + * choix. + * + * Les deux sont quantisés en `Q4_K_M` — le compromis taille/qualité standard + * pour de l'inférence CPU (~4.5 bits/poids, largement suffisant pour une + * tâche d'extraction structurée, contrairement à de la génération créative + * longue où une quantisation plus fine se voit davantage). + */ +const RECOMMENDED_MODELS: Record = { + "qwen2.5-1.5b": { + hfUri: "hf:Qwen/Qwen2.5-1.5B-Instruct-GGUF:Q4_K_M", + rationale: + "Meilleure robustesse multilingue FR/EN et meilleur suivi d'instructions de structuration JSON à taille comparable.", + }, + "llama-3.2-1b": { + hfUri: "hf:bartowski/Llama-3.2-1B-Instruct-GGUF:Q4_K_M", + rationale: + "Plus petit/plus rapide ; FR officiellement supporté mais structuration JSON moins fiable à 1B.", + }, +}; + +/** Répertoire où les modèles GGUF téléchargés sont mis en cache — à côté de ce fichier, jamais commité (voir `.gitignore` du dossier). */ +const MODELS_DIRECTORY = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "models"); + +/** + * Résout le chemin du fichier GGUF à charger : un chemin local explicite + * via `LLM_TECH_STEP_MODEL_PATH` prime toujours (utile hors-ligne, ou en CI + * où un téléchargement réseau à la volée n'est pas souhaitable) ; sinon, + * {@link RECOMMENDED_MODELS} est résolu par `resolveModelFile`, qui + * télécharge le fichier une seule fois dans {@link MODELS_DIRECTORY} puis le + * réutilise tel quel aux exécutions suivantes. + */ +async function resolveModelPath(modelKey: RecommendedModelKey): Promise { + const explicitPath = process.env.LLM_TECH_STEP_MODEL_PATH; + if (explicitPath !== undefined && explicitPath.length > 0) return explicitPath; + return await resolveModelFile(RECOMMENDED_MODELS[modelKey].hfUri, MODELS_DIRECTORY); +} + +// --------------------------------------------------------------------------- +// LocalLlmStepAnalyzer — enrobage node-llama-cpp +// --------------------------------------------------------------------------- + +/** + * Charge un modèle GGUF local et l'expose comme un service d'analyse + * d'étapes de recette — vraie `class` (pas un objet littéral), même + * convention que `TechStepClassifierService` : elle possède un état réel + * (modèle chargé, contexte, grammaire compilée) coûteux à reconstruire, + * jamais recréé par appel. + */ +export class LocalLlmStepAnalyzer { + /** Instance `node-llama-cpp` — porte d'entrée vers le binding natif llama.cpp. `undefined` avant `initialize()`. */ + private _llama: Awaited> | undefined; + /** Modèle GGUF chargé en mémoire. `undefined` avant `initialize()`. */ + private _model: + | Awaited>["loadModel"]>> + | undefined; + /** Contexte d'inférence (fenêtre de contexte + cache KV) dérivé de `_model`. `undefined` avant `initialize()`. */ + private _context: + | Awaited< + ReturnType< + Awaited>["loadModel"]>>["createContext"] + > + > + | undefined; + /** + * Grammaire GBNF compilée depuis {@link KITCHEN_ACTIONS_JSON_SCHEMA} — + * compilée une seule fois, réutilisée à chaque `analyzeStep`. Typée + * explicitement via le type générique `LlamaJsonSchemaGrammar` + * (plutôt qu'un `Awaited>` sur la méthode générique + * `createGrammarForJsonSchema`, qui perd le type précis du schéma faute + * d'argument concret à cet endroit) pour que `grammar.parse()` renvoie un + * type déjà aligné sur {@link KitchenActionsGrammarResult}. `undefined` + * avant `initialize()`. + */ + private _grammar: LlamaJsonSchemaGrammar | undefined; + + /** + * Charge le modèle (téléchargement au besoin, voir {@link resolveModelPath}), + * crée son contexte d'inférence et compile la grammaire JSON — la partie + * coûteuse (souvent plusieurs secondes, dominée par le chargement des + * poids depuis disque), à faire une seule fois avant tout `analyzeStep`. + */ + public async initialize(modelKey: RecommendedModelKey): Promise { + const modelPath = await resolveModelPath(modelKey); + this._llama = await getLlama(); + this._model = await this._llama.loadModel({ modelPath }); + this._context = await this._model.createContext({ contextSize: 4096 }); + this._grammar = await this._llama.createGrammarForJsonSchema(KITCHEN_ACTIONS_JSON_SCHEMA); + } + + /** + * Analyse une étape de recette et renvoie sa séquence ordonnée d'actions. + * + * Une séquence `LlamaContextSequence` dédiée est allouée pour CET appel + * puis libérée en sortie (`finally`), plutôt que de réutiliser une session + * de chat partagée : `LlamaChatSession` accumule l'historique de + * conversation à chaque `prompt()`, ce qui aurait fait grandir le contexte + * (et donc la latence mesurée) au fil des phrases du benchmark au lieu de + * mesurer chaque étape dans des conditions comparables. Le contexte par + * défaut n'autorise qu'une seule séquence active à la fois + * (`createContext()` sans `sequences` explicite) — d'où la libération + * immédiate, indispensable pour que l'appel suivant puisse en allouer une + * nouvelle. + */ + public async analyzeStep(stepText: string): Promise { + if (this._llama === undefined || this._context === undefined || this._grammar === undefined) { + throw new Error("LocalLlmStepAnalyzer.initialize() must be awaited before analyzeStep()."); + } + const context = this._context; + const grammar = this._grammar; + const sequence = context.getSequence(); + try { + const session = new LlamaChatSession({ + contextSequence: sequence, + systemPrompt: SYSTEM_PROMPT, + }); + const response = await session.prompt(stepText, { grammar }); + // La grammaire garantit un JSON syntaxiquement conforme au schéma — + // ce cast ne fait que réattacher le type nommé `KitchenActionsGrammarResult` + // (le schéma étant défini structurellement, pas de risque `any`). + const parsed = grammar.parse(response) as KitchenActionsGrammarResult; + return { originalText: stepText, actions: parsed.actions }; + } finally { + await sequence.dispose(); + } + } + + /** Libère le modèle et son contexte — à appeler une fois le benchmark terminé, la mémoire native n'étant pas gérée par le GC de V8. */ + public async dispose(): Promise { + await this._context?.dispose(); + await this._model?.dispose(); + } +} + +// --------------------------------------------------------------------------- +// Benchmark +// --------------------------------------------------------------------------- + +/** Une phrase de test du benchmark, avec sa langue et ce qui la rend "complexe" (documentation, non exploité par le code). */ +interface BenchmarkSentence { + id: string; + locale: "fr" | "en"; + text: string; + /** Ce qui rend cette phrase intéressante à tester — affiché dans les résultats pour donner du contexte à la comparaison manuelle avec le pipeline `node-nlp`. */ + note: string; +} + +/** + * Trois phrases complexes, FR et EN, choisies pour couvrir des difficultés + * différentes : + * + * 1. FR, plusieurs actions explicites enchaînées avec une durée et un + * ingrédient qui change de forme grammaticale ("les" reprend "oignons"). + * 2. EN, même complexité multi-actions, pour comparer directement au 1. sur + * une structure de phrase équivalente dans l'autre langue. + * 3. FR, la phrase-piège citée dans la doc de `tech-step-matcher.ts` elle-même + * ("jusqu'à ce que le beurre ait disparu dans la poêle") : aucune action + * n'est nommée par un verbe de technique littéral, seul le sens implique + * une cuisson (`COOK`) — exactement le cas que le pipeline `node-nlp` + * existant a dû être spécifiquement entraîné à reconnaître (voir le + * point 3 de sa doc). Comparer les deux pipelines sur cette phrase + * précise est le test le plus direct de "précision sémantique, pas + * seulement mot-clé" que ce PoC cherche à évaluer. + */ +const TEST_SENTENCES: readonly BenchmarkSentence[] = [ + { + id: "fr-multi-action", + locale: "fr", + text: "Émincez finement les oignons puis faites-les revenir 10 minutes à feu moyen dans une poêle avec un filet d'huile d'olive, puis réservez.", + note: "3 actions enchaînées (CUT, COOK, OTHER), durée + feu + ustensile explicites.", + }, + { + id: "en-multi-action", + locale: "en", + text: "Dice the tomatoes, season with salt and pepper, then simmer everything in a saucepan over low heat for about 15 minutes before letting it rest for 5 minutes.", + note: "4 actions enchaînées (CUT, SEASON, COOK, REST), deux durées distinctes à ne pas fusionner.", + }, + { + id: "fr-action-implicite", + locale: "fr", + text: "Dans une poêle chaude, faites chauffer une noix de beurre jusqu'à ce qu'il ait disparu, puis ajoutez les échalotes ciselées.", + note: "Cas piège documenté dans tech-step-matcher.ts : aucun verbe de cuisson littéral, seul le sens implique COOK (fonte du beurre).", + }, +]; + +/** Nombre de répétitions mesurées par phrase — atténue le bruit d'une mesure isolée sans allonger excessivement le run. */ +const REPETITIONS_PER_SENTENCE = 3; + +/** Une mesure individuelle (une répétition, une phrase) — la matière première des tableaux récapitulatifs imprimés en fin de run. */ +interface BenchmarkSample { + sentence: BenchmarkSentence; + latencyMs: number; + /** Delta de RSS du process Node entre juste avant et juste après cet appel — une approximation de la RAM réellement consommée par l'inférence : `process.memoryUsage()` ne voit que le tas V8, mais le binding natif llama.cpp alloue dans le même process, donc le RSS (mémoire résidente totale du process) le capture bien, au bruit du GC près. */ + rssDeltaBytes: number; + analysis: RecipeStepAnalysis; +} + +/** + * Exécute {@link REPETITIONS_PER_SENTENCE} analyses par phrase de + * {@link TEST_SENTENCES} et renvoie toutes les mesures individuelles. + * Une erreur sur une répétition est journalisée et n'interrompt pas les + * suivantes — un run de benchmark qui plante entièrement à la première + * réponse mal formée serait bien moins utile qu'un rapport partiel. + */ +async function runBenchmark(analyzer: LocalLlmStepAnalyzer): Promise { + const samples: BenchmarkSample[] = []; + for (const sentence of TEST_SENTENCES) { + for (let repetition = 1; repetition <= REPETITIONS_PER_SENTENCE; repetition++) { + const rssBefore = process.memoryUsage().rss; + const startedAt = performance.now(); + try { + const analysis = await analyzer.analyzeStep(sentence.text); + const latencyMs = performance.now() - startedAt; + const rssDeltaBytes = process.memoryUsage().rss - rssBefore; + samples.push({ sentence, latencyMs, rssDeltaBytes, analysis }); + } catch (err) { + console.error(`[poc] échec sur "${sentence.id}" (répétition ${repetition})`, err); + } + } + } + return samples; +} + +/** Une ligne de sortie détaillée, une par échantillon — sert de matière première à la comparaison manuelle avec le pipeline `node-nlp`. */ +function printDetailedResults(samples: readonly BenchmarkSample[]): void { + for (const sample of samples) { + console.info( + `\n[${sample.sentence.id}] (${sample.sentence.locale}) — ${sample.latencyMs.toFixed(0)} ms`, + ); + console.info(` texte : ${sample.sentence.text}`); + console.info(` attendu : ${sample.sentence.note}`); + console.table( + sample.analysis.actions.map((action) => ({ + action: action.action, + verbe: action.verb, + ingrédients: action.ingredients.join(", "), + "durée (min)": action.durationMinutes ?? "—", + température: action.temperature ?? "—", + ustensiles: action.utensils.join(", "), + })), + ); + } +} + +/** Une ligne du tableau récapitulatif final — moyennes/min/max de latence et RAM par phrase, agrégées sur {@link REPETITIONS_PER_SENTENCE} répétitions. */ +interface BenchmarkSummaryRow { + phrase: string; + langue: string; + "runs OK": number; + "latence moy. (ms)": string; + "latence min (ms)": string; + "latence max (ms)": string; + "RSS moy. (Mo)": string; + "actions détectées": number; +} + +/** Agrège {@link BenchmarkSample}s par phrase et imprime le tableau récapitulatif du benchmark. */ +function printSummaryTable(samples: readonly BenchmarkSample[]): void { + const rows: BenchmarkSummaryRow[] = TEST_SENTENCES.map((sentence) => { + const sentenceSamples = samples.filter((sample) => sample.sentence.id === sentence.id); + const latencies = sentenceSamples.map((sample) => sample.latencyMs); + const avgLatency = latencies.reduce((sum, value) => sum + value, 0) / (latencies.length || 1); + const avgRssMb = + sentenceSamples.reduce((sum, sample) => sum + sample.rssDeltaBytes, 0) / + (sentenceSamples.length || 1) / + (1024 * 1024); + const lastSample = sentenceSamples.at(-1); + return { + phrase: sentence.id, + langue: sentence.locale, + "runs OK": sentenceSamples.length, + "latence moy. (ms)": latencies.length > 0 ? avgLatency.toFixed(0) : "—", + "latence min (ms)": latencies.length > 0 ? Math.min(...latencies).toFixed(0) : "—", + "latence max (ms)": latencies.length > 0 ? Math.max(...latencies).toFixed(0) : "—", + "RSS moy. (Mo)": sentenceSamples.length > 0 ? avgRssMb.toFixed(1) : "—", + "actions détectées": lastSample?.analysis.actions.length ?? 0, + }; + }); + console.info("\n=== Récapitulatif ==="); + console.table(rows); +} + +// --------------------------------------------------------------------------- +// Entrée du script +// --------------------------------------------------------------------------- + +/** + * Point d'entrée : charge le modèle choisi via `LLM_TECH_STEP_MODEL` + * (`"qwen2.5-1.5b"` par défaut, voir {@link RECOMMENDED_MODELS}), lance le + * benchmark sur {@link TEST_SENTENCES}, imprime les résultats détaillés puis + * le récapitulatif, et libère le modèle avant de quitter. + */ +async function main(): Promise { + const modelKey: RecommendedModelKey = + process.env.LLM_TECH_STEP_MODEL === "llama-3.2-1b" ? "llama-3.2-1b" : "qwen2.5-1.5b"; + console.info( + `[poc] modèle sélectionné : ${modelKey} (${RECOMMENDED_MODELS[modelKey].rationale})`, + ); + + const analyzer = new LocalLlmStepAnalyzer(); + const rssBeforeLoad = process.memoryUsage().rss; + // `performance.now()` plutôt que `console.time`/`console.timeEnd` — la + // config Biome du repo n'autorise que error/warn/info/debug/table/assert + // sur `console` (voir `biome.json`, `suspicious.noConsole`), pas `time`. + const loadStartedAt = performance.now(); + try { + await analyzer.initialize(modelKey); + } catch (err) { + console.error( + "[poc] échec du chargement du modèle — vérifier LLM_TECH_STEP_MODEL_PATH / la connexion réseau pour le téléchargement initial", + err, + ); + process.exitCode = 1; + return; + } + const loadDurationMs = performance.now() - loadStartedAt; + const modelRssMb = (process.memoryUsage().rss - rssBeforeLoad) / (1024 * 1024); + console.info( + `[poc] modèle chargé en ${loadDurationMs.toFixed(0)} ms (+${modelRssMb.toFixed(1)} Mo RSS)`, + ); + + try { + const benchmarkStartedAt = performance.now(); + const samples = await runBenchmark(analyzer); + const benchmarkDurationMs = performance.now() - benchmarkStartedAt; + console.info(`[poc] benchmark complet en ${benchmarkDurationMs.toFixed(0)} ms`); + printDetailedResults(samples); + printSummaryTable(samples); + } finally { + try { + await analyzer.dispose(); + } catch (err) { + console.error("[poc] erreur lors de la libération du modèle", err); + } + } +} + +await main(); diff --git a/experiments/llm-tech-step-poc/tsconfig.json b/experiments/llm-tech-step-poc/tsconfig.json new file mode 100644 index 0000000..a73d580 --- /dev/null +++ b/experiments/llm-tech-step-poc/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src"] +}