OwnPath commited on
Commit
0cb5613
·
verified ·
1 Parent(s): d81c4fa

Kokoro-82M TTS + README/diagram update + dead-code/Ollama cleanup

Browse files
.env.example CHANGED
@@ -59,7 +59,4 @@ AGENT_FANOUT=1
59
  # the decisions; this small assistant summarises each agent's evidence. The 0.5B is tiny, so it
60
  # often falls back to the main model — use openbmb/MiniCPM4-8B-GGUF for genuine summaries.
61
  AGENT_SUMMARIZER_PROVIDER=llamacpp
62
- OLLAMA_MODEL=
63
- OLLAMA_HOST=http://127.0.0.1:11434
64
- OLLAMA_TIMEOUT_MS=60000
65
  AGENT_PORT=8787
 
59
  # the decisions; this small assistant summarises each agent's evidence. The 0.5B is tiny, so it
60
  # often falls back to the main model — use openbmb/MiniCPM4-8B-GGUF for genuine summaries.
61
  AGENT_SUMMARIZER_PROVIDER=llamacpp
 
 
 
62
  AGENT_PORT=8787
README.md CHANGED
@@ -23,8 +23,10 @@ tags:
23
  - achievement:sharing
24
  - achievement:fieldnotes
25
  models:
26
- - Qwen/Qwen3-Coder-30B-A3B-Instruct
27
  - nvidia/Nemotron-3-Nano-Omni-30B-A3B-Reasoning-BF16
 
 
 
28
  ---
29
 
30
  # 6ixPulse
@@ -56,7 +58,8 @@ The app then ranks Toronto neighborhoods, animates the map to the areas it is re
56
  - **Fans out a researcher per City Agent.** Affordability, Commute, Safety, Lifestyle and Growth agents each reason over only their own evidence; a **Recommendation agent runs last** with every agent's finding *and* all of their sources.
57
  - **Fails closed.** The UI withholds a rent, commute, safety, growth claim, or score unless the backend can tie it to source evidence — sources are named, never "S1".
58
  - **Shows a real Mapbox GL JS 3D map** with a research tour that zooms into each area while it is being researched, then flies out-and-in to the next.
59
- - **Two-tier brains, all < 32B.** NVIDIA Nemotron Nano Omni Reasoning is the main model (with `enable_thinking`); the small OpenBMB model on llama.cpp assists with summarisation. Falls back to HF when a provider is unavailable.
 
60
 
61
  ## Architecture
62
 
@@ -106,14 +109,16 @@ flowchart TB
106
  Score --- Transit
107
  Score --- CMHC
108
 
109
- subgraph Models["Two-tier brains · all under 32B"]
110
  Main["Main: NVIDIA Nemotron Nano Omni Reasoning<br/>temp 0.6 · enable_thinking · reasoning_budget<br/>↳ fallback: HF Qwen3-Coder"]
111
  Assist["Assistant: OpenBMB MiniCPM via llama.cpp<br/>summarises each agent's evidence"]
 
112
  end
113
  Discover --- Main
114
  Fanout --- Assist
115
  Fanout --- Main
116
  Recommend --- Main
 
117
  ```
118
 
119
  ## Tech Stack
@@ -128,6 +133,7 @@ flowchart TB
128
  - Models (all < 32B):
129
  - Main agentic brain: `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning` (build.nvidia.com)
130
  - Summarisation assistant: OpenBMB MiniCPM via llama.cpp (`openbmb/MiniCPM4-*-GGUF`)
 
131
  - Fallback: `Qwen/Qwen3-Coder-30B-A3B-Instruct` on the HF Router
132
 
133
  ## Build Small Hackathon Readiness
@@ -214,6 +220,8 @@ HF_MODEL=Qwen/Qwen3-Coder-30B-A3B-Instruct
214
 
215
  The reasoning model uses the Nemotron recipe (matching `ChatNVIDIA`): `temperature=0.6`, `top_p=0.95`, `chat_template_kwargs={"enable_thinking": true}`, and a `reasoning_budget`; its `<think>` reasoning is captured separately from the JSON answer. Thinking is enabled only for the decisions (discovery, recommendation, synthesis), and the per-agent fan-out runs in parallel on hosted APIs, so a full run stays within the response budget.
216
 
 
 
217
  ## Gradio Space
218
 
219
  This repo is prepared for a Docker-backed Gradio Space using `gradio.Server`, which is designed for custom frontends like React while still giving the project Gradio's API engine, queuing, MCP support, and Hugging Face Spaces hosting.
@@ -320,6 +328,7 @@ Dockerfile Docker Space image with Node + Python
320
  src/App.tsx app shell, agent panels, scores, listings
321
  src/components/MapCanvas.tsx Mapbox/deck.gl map + research tour
322
  src/lib/agentApi.ts frontend API client types
 
323
  server/index.mjs agent API server; orchestrates the pipeline
324
  server/discover.mjs model-driven neighbourhood discovery (+ coordinates)
325
  server/score-tools.mjs 8-dimension scoring from named no-key sources
 
23
  - achievement:sharing
24
  - achievement:fieldnotes
25
  models:
 
26
  - nvidia/Nemotron-3-Nano-Omni-30B-A3B-Reasoning-BF16
27
+ - Qwen/Qwen3-Coder-30B-A3B-Instruct
28
+ - openbmb/MiniCPM4-0.5B
29
+ - hexgrad/Kokoro-82M
30
  ---
31
 
32
  # 6ixPulse
 
58
  - **Fans out a researcher per City Agent.** Affordability, Commute, Safety, Lifestyle and Growth agents each reason over only their own evidence; a **Recommendation agent runs last** with every agent's finding *and* all of their sources.
59
  - **Fails closed.** The UI withholds a rent, commute, safety, growth claim, or score unless the backend can tie it to source evidence — sources are named, never "S1".
60
  - **Shows a real Mapbox GL JS 3D map** with a research tour that zooms into each area while it is being researched, then flies out-and-in to the next.
61
+ - **Reads the findings aloud.** A Play button narrates a short spoken summary using **Kokoro-82M** neural TTS running entirely in the browser no key, no backend.
62
+ - **Models, all < 32B.** NVIDIA Nemotron Nano Omni Reasoning is the main brain (with `enable_thinking`); the small OpenBMB model on llama.cpp assists with summarisation; Kokoro-82M handles speech. Falls back to HF when a provider is unavailable.
63
 
64
  ## Architecture
65
 
 
109
  Score --- Transit
110
  Score --- CMHC
111
 
112
+ subgraph Models["Models · all under 32B"]
113
  Main["Main: NVIDIA Nemotron Nano Omni Reasoning<br/>temp 0.6 · enable_thinking · reasoning_budget<br/>↳ fallback: HF Qwen3-Coder"]
114
  Assist["Assistant: OpenBMB MiniCPM via llama.cpp<br/>summarises each agent's evidence"]
115
+ Speech["Voice: Kokoro-82M neural TTS<br/>in-browser, narrates the summary"]
116
  end
117
  Discover --- Main
118
  Fanout --- Assist
119
  Fanout --- Main
120
  Recommend --- Main
121
+ Panels -. Play .-> Speech
122
  ```
123
 
124
  ## Tech Stack
 
133
  - Models (all < 32B):
134
  - Main agentic brain: `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning` (build.nvidia.com)
135
  - Summarisation assistant: OpenBMB MiniCPM via llama.cpp (`openbmb/MiniCPM4-*-GGUF`)
136
+ - Speech: `hexgrad/Kokoro-82M` neural TTS in the browser (transformers.js)
137
  - Fallback: `Qwen/Qwen3-Coder-30B-A3B-Instruct` on the HF Router
138
 
139
  ## Build Small Hackathon Readiness
 
220
 
221
  The reasoning model uses the Nemotron recipe (matching `ChatNVIDIA`): `temperature=0.6`, `top_p=0.95`, `chat_template_kwargs={"enable_thinking": true}`, and a `reasoning_budget`; its `<think>` reasoning is captured separately from the JSON answer. Thinking is enabled only for the decisions (discovery, recommendation, synthesis), and the per-agent fan-out runs in parallel on hosted APIs, so a full run stays within the response budget.
222
 
223
+ For speech, the Play button uses **`hexgrad/Kokoro-82M`** neural TTS via `transformers.js`, running fully in the browser (ONNX/WASM). It is lazy-loaded — the ~80 MB model only downloads on the first Play, then is cached — and falls back to the browser's built-in speech while it loads or where it can't run. No key, no backend, 82M parameters.
224
+
225
  ## Gradio Space
226
 
227
  This repo is prepared for a Docker-backed Gradio Space using `gradio.Server`, which is designed for custom frontends like React while still giving the project Gradio's API engine, queuing, MCP support, and Hugging Face Spaces hosting.
 
328
  src/App.tsx app shell, agent panels, scores, listings
329
  src/components/MapCanvas.tsx Mapbox/deck.gl map + research tour
330
  src/lib/agentApi.ts frontend API client types
331
+ src/lib/tts.ts Kokoro-82M in-browser narration
332
  server/index.mjs agent API server; orchestrates the pipeline
333
  server/discover.mjs model-driven neighbourhood discovery (+ coordinates)
334
  server/score-tools.mjs 8-dimension scoring from named no-key sources
package-lock.json CHANGED
@@ -11,6 +11,7 @@
11
  "@deck.gl/core": "^9.0.0",
12
  "@deck.gl/layers": "^9.0.0",
13
  "@deck.gl/mapbox": "^9.0.0",
 
14
  "lucide-react": "^0.468.0",
15
  "mapbox-gl": "^3.11.1",
16
  "react": "^19.0.0",
@@ -100,22 +101,519 @@
100
  "version": "1.10.0",
101
  "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
102
  "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
 
 
 
 
 
 
 
 
 
 
103
  "dev": true,
104
  "license": "MIT",
105
  "optional": true,
106
- "dependencies": {
107
- "tslib": "^2.4.0"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  }
109
  },
110
- "node_modules/@emnapi/wasi-threads": {
111
- "version": "1.2.1",
112
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
113
- "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
114
- "dev": true,
115
- "license": "MIT",
 
 
116
  "optional": true,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  "dependencies": {
118
- "tslib": "^2.4.0"
 
 
 
119
  }
120
  },
121
  "node_modules/@loaders.gl/core": {
@@ -367,6 +865,63 @@
367
  "integrity": "sha512-4VpAyMHOqydSvPlEyHwXaE+AkIdR03nX+Qhlxsk2D/IW4OVmDZgIsvJB1cDzyEEtcfKcnaEbfXeiPgejBceT6g==",
368
  "license": "MIT"
369
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  "node_modules/@rolldown/binding-android-arm64": {
371
  "version": "1.0.3",
372
  "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
@@ -798,6 +1353,13 @@
798
  "node": ">=12.17"
799
  }
800
  },
 
 
 
 
 
 
 
801
  "node_modules/chalk": {
802
  "version": "4.1.2",
803
  "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -835,6 +1397,15 @@
835
  "integrity": "sha512-pmcgaGHAxfYtKr4BtSaM/+/3vvQJJLkvQibEdO2eb5eiHrRgEAd43M2tpBGi4eIX32kXyvPLQVT23qNnuFaa8w==",
836
  "license": "ISC"
837
  },
 
 
 
 
 
 
 
 
 
838
  "node_modules/color-convert": {
839
  "version": "2.0.1",
840
  "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -904,22 +1475,97 @@
904
  "dev": true,
905
  "license": "MIT"
906
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  "node_modules/detect-libc": {
908
  "version": "2.1.2",
909
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
910
  "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
911
- "dev": true,
912
  "license": "Apache-2.0",
913
  "engines": {
914
  "node": ">=8"
915
  }
916
  },
 
 
 
 
 
 
917
  "node_modules/earcut": {
918
  "version": "2.2.4",
919
  "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
920
  "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
921
  "license": "ISC"
922
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
923
  "node_modules/fdir": {
924
  "version": "6.5.0",
925
  "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -988,6 +1634,57 @@
988
  "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
989
  "license": "MIT"
990
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
991
  "node_modules/has-flag": {
992
  "version": "4.0.0",
993
  "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -997,6 +1694,18 @@
997
  "node": ">=8"
998
  }
999
  },
 
 
 
 
 
 
 
 
 
 
 
 
1000
  "node_modules/json-bignum": {
1001
  "version": "0.0.3",
1002
  "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz",
@@ -1005,12 +1714,28 @@
1005
  "node": ">=0.8"
1006
  }
1007
  },
 
 
 
 
 
 
1008
  "node_modules/kdbush": {
1009
  "version": "4.1.0",
1010
  "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz",
1011
  "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
1012
  "license": "ISC"
1013
  },
 
 
 
 
 
 
 
 
 
 
1014
  "node_modules/lightningcss": {
1015
  "version": "1.32.0",
1016
  "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@@ -1278,6 +2003,12 @@
1278
  "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
1279
  "license": "MIT"
1280
  },
 
 
 
 
 
 
1281
  "node_modules/lucide-react": {
1282
  "version": "0.468.0",
1283
  "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz",
@@ -1341,6 +2072,39 @@
1341
  "tinyqueue": "3.0.0"
1342
  }
1343
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1344
  "node_modules/mjolnir.js": {
1345
  "version": "3.0.0",
1346
  "resolved": "https://registry.npmjs.org/mjolnir.js/-/mjolnir.js-3.0.0.tgz",
@@ -1372,6 +2136,58 @@
1372
  "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1373
  }
1374
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1375
  "node_modules/pbf": {
1376
  "version": "4.0.2",
1377
  "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.2.tgz",
@@ -1384,6 +2200,12 @@
1384
  "pbf": "bin/pbf"
1385
  }
1386
  },
 
 
 
 
 
 
1387
  "node_modules/picocolors": {
1388
  "version": "1.1.1",
1389
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1404,6 +2226,12 @@
1404
  "url": "https://github.com/sponsors/jonschlinkert"
1405
  }
1406
  },
 
 
 
 
 
 
1407
  "node_modules/postcss": {
1408
  "version": "8.5.15",
1409
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
@@ -1439,6 +2267,29 @@
1439
  "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
1440
  "license": "ISC"
1441
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1442
  "node_modules/protocol-buffers-schema": {
1443
  "version": "3.6.1",
1444
  "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz",
@@ -1481,6 +2332,23 @@
1481
  "protocol-buffers-schema": "^3.3.1"
1482
  }
1483
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1484
  "node_modules/robust-predicates": {
1485
  "version": "2.0.4",
1486
  "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-2.0.4.tgz",
@@ -1527,6 +2395,83 @@
1527
  "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
1528
  "license": "MIT"
1529
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1530
  "node_modules/source-map-js": {
1531
  "version": "1.2.1",
1532
  "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1543,6 +2488,12 @@
1543
  "integrity": "sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ==",
1544
  "license": "MIT"
1545
  },
 
 
 
 
 
 
1546
  "node_modules/supercluster": {
1547
  "version": "8.0.1",
1548
  "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
@@ -1577,6 +2528,22 @@
1577
  "node": ">=12.17"
1578
  }
1579
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1580
  "node_modules/tinyglobby": {
1581
  "version": "0.2.17",
1582
  "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -1606,6 +2573,18 @@
1606
  "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
1607
  "license": "0BSD"
1608
  },
 
 
 
 
 
 
 
 
 
 
 
 
1609
  "node_modules/typescript": {
1610
  "version": "5.9.3",
1611
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -1721,6 +2700,15 @@
1721
  "engines": {
1722
  "node": ">=12.17"
1723
  }
 
 
 
 
 
 
 
 
 
1724
  }
1725
  }
1726
  }
 
11
  "@deck.gl/core": "^9.0.0",
12
  "@deck.gl/layers": "^9.0.0",
13
  "@deck.gl/mapbox": "^9.0.0",
14
+ "kokoro-js": "^1.2.1",
15
  "lucide-react": "^0.468.0",
16
  "mapbox-gl": "^3.11.1",
17
  "react": "^19.0.0",
 
101
  "version": "1.10.0",
102
  "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
103
  "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
104
+ "license": "MIT",
105
+ "optional": true,
106
+ "dependencies": {
107
+ "tslib": "^2.4.0"
108
+ }
109
+ },
110
+ "node_modules/@emnapi/wasi-threads": {
111
+ "version": "1.2.1",
112
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
113
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
114
  "dev": true,
115
  "license": "MIT",
116
  "optional": true,
117
+ "dependencies": {
118
+ "tslib": "^2.4.0"
119
+ }
120
+ },
121
+ "node_modules/@huggingface/jinja": {
122
+ "version": "0.5.9",
123
+ "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz",
124
+ "integrity": "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==",
125
+ "license": "MIT",
126
+ "engines": {
127
+ "node": ">=18"
128
+ }
129
+ },
130
+ "node_modules/@huggingface/transformers": {
131
+ "version": "3.8.1",
132
+ "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.8.1.tgz",
133
+ "integrity": "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==",
134
+ "license": "Apache-2.0",
135
+ "dependencies": {
136
+ "@huggingface/jinja": "^0.5.3",
137
+ "onnxruntime-node": "1.21.0",
138
+ "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4",
139
+ "sharp": "^0.34.1"
140
+ }
141
+ },
142
+ "node_modules/@img/colour": {
143
+ "version": "1.1.0",
144
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
145
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
146
+ "license": "MIT",
147
+ "engines": {
148
+ "node": ">=18"
149
+ }
150
+ },
151
+ "node_modules/@img/sharp-darwin-arm64": {
152
+ "version": "0.34.5",
153
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
154
+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
155
+ "cpu": [
156
+ "arm64"
157
+ ],
158
+ "license": "Apache-2.0",
159
+ "optional": true,
160
+ "os": [
161
+ "darwin"
162
+ ],
163
+ "engines": {
164
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
165
+ },
166
+ "funding": {
167
+ "url": "https://opencollective.com/libvips"
168
+ },
169
+ "optionalDependencies": {
170
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
171
+ }
172
+ },
173
+ "node_modules/@img/sharp-darwin-x64": {
174
+ "version": "0.34.5",
175
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
176
+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
177
+ "cpu": [
178
+ "x64"
179
+ ],
180
+ "license": "Apache-2.0",
181
+ "optional": true,
182
+ "os": [
183
+ "darwin"
184
+ ],
185
+ "engines": {
186
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
187
+ },
188
+ "funding": {
189
+ "url": "https://opencollective.com/libvips"
190
+ },
191
+ "optionalDependencies": {
192
+ "@img/sharp-libvips-darwin-x64": "1.2.4"
193
+ }
194
+ },
195
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
196
+ "version": "1.2.4",
197
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
198
+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
199
+ "cpu": [
200
+ "arm64"
201
+ ],
202
+ "license": "LGPL-3.0-or-later",
203
+ "optional": true,
204
+ "os": [
205
+ "darwin"
206
+ ],
207
+ "funding": {
208
+ "url": "https://opencollective.com/libvips"
209
+ }
210
+ },
211
+ "node_modules/@img/sharp-libvips-darwin-x64": {
212
+ "version": "1.2.4",
213
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
214
+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
215
+ "cpu": [
216
+ "x64"
217
+ ],
218
+ "license": "LGPL-3.0-or-later",
219
+ "optional": true,
220
+ "os": [
221
+ "darwin"
222
+ ],
223
+ "funding": {
224
+ "url": "https://opencollective.com/libvips"
225
+ }
226
+ },
227
+ "node_modules/@img/sharp-libvips-linux-arm": {
228
+ "version": "1.2.4",
229
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
230
+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
231
+ "cpu": [
232
+ "arm"
233
+ ],
234
+ "license": "LGPL-3.0-or-later",
235
+ "optional": true,
236
+ "os": [
237
+ "linux"
238
+ ],
239
+ "funding": {
240
+ "url": "https://opencollective.com/libvips"
241
+ }
242
+ },
243
+ "node_modules/@img/sharp-libvips-linux-arm64": {
244
+ "version": "1.2.4",
245
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
246
+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
247
+ "cpu": [
248
+ "arm64"
249
+ ],
250
+ "license": "LGPL-3.0-or-later",
251
+ "optional": true,
252
+ "os": [
253
+ "linux"
254
+ ],
255
+ "funding": {
256
+ "url": "https://opencollective.com/libvips"
257
+ }
258
+ },
259
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
260
+ "version": "1.2.4",
261
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
262
+ "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
263
+ "cpu": [
264
+ "ppc64"
265
+ ],
266
+ "license": "LGPL-3.0-or-later",
267
+ "optional": true,
268
+ "os": [
269
+ "linux"
270
+ ],
271
+ "funding": {
272
+ "url": "https://opencollective.com/libvips"
273
+ }
274
+ },
275
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
276
+ "version": "1.2.4",
277
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
278
+ "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
279
+ "cpu": [
280
+ "riscv64"
281
+ ],
282
+ "license": "LGPL-3.0-or-later",
283
+ "optional": true,
284
+ "os": [
285
+ "linux"
286
+ ],
287
+ "funding": {
288
+ "url": "https://opencollective.com/libvips"
289
+ }
290
+ },
291
+ "node_modules/@img/sharp-libvips-linux-s390x": {
292
+ "version": "1.2.4",
293
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
294
+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
295
+ "cpu": [
296
+ "s390x"
297
+ ],
298
+ "license": "LGPL-3.0-or-later",
299
+ "optional": true,
300
+ "os": [
301
+ "linux"
302
+ ],
303
+ "funding": {
304
+ "url": "https://opencollective.com/libvips"
305
+ }
306
+ },
307
+ "node_modules/@img/sharp-libvips-linux-x64": {
308
+ "version": "1.2.4",
309
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
310
+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
311
+ "cpu": [
312
+ "x64"
313
+ ],
314
+ "license": "LGPL-3.0-or-later",
315
+ "optional": true,
316
+ "os": [
317
+ "linux"
318
+ ],
319
+ "funding": {
320
+ "url": "https://opencollective.com/libvips"
321
+ }
322
+ },
323
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
324
+ "version": "1.2.4",
325
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
326
+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
327
+ "cpu": [
328
+ "arm64"
329
+ ],
330
+ "license": "LGPL-3.0-or-later",
331
+ "optional": true,
332
+ "os": [
333
+ "linux"
334
+ ],
335
+ "funding": {
336
+ "url": "https://opencollective.com/libvips"
337
+ }
338
+ },
339
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
340
+ "version": "1.2.4",
341
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
342
+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
343
+ "cpu": [
344
+ "x64"
345
+ ],
346
+ "license": "LGPL-3.0-or-later",
347
+ "optional": true,
348
+ "os": [
349
+ "linux"
350
+ ],
351
+ "funding": {
352
+ "url": "https://opencollective.com/libvips"
353
+ }
354
+ },
355
+ "node_modules/@img/sharp-linux-arm": {
356
+ "version": "0.34.5",
357
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
358
+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
359
+ "cpu": [
360
+ "arm"
361
+ ],
362
+ "license": "Apache-2.0",
363
+ "optional": true,
364
+ "os": [
365
+ "linux"
366
+ ],
367
+ "engines": {
368
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
369
+ },
370
+ "funding": {
371
+ "url": "https://opencollective.com/libvips"
372
+ },
373
+ "optionalDependencies": {
374
+ "@img/sharp-libvips-linux-arm": "1.2.4"
375
+ }
376
+ },
377
+ "node_modules/@img/sharp-linux-arm64": {
378
+ "version": "0.34.5",
379
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
380
+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
381
+ "cpu": [
382
+ "arm64"
383
+ ],
384
+ "license": "Apache-2.0",
385
+ "optional": true,
386
+ "os": [
387
+ "linux"
388
+ ],
389
+ "engines": {
390
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
391
+ },
392
+ "funding": {
393
+ "url": "https://opencollective.com/libvips"
394
+ },
395
+ "optionalDependencies": {
396
+ "@img/sharp-libvips-linux-arm64": "1.2.4"
397
+ }
398
+ },
399
+ "node_modules/@img/sharp-linux-ppc64": {
400
+ "version": "0.34.5",
401
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
402
+ "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
403
+ "cpu": [
404
+ "ppc64"
405
+ ],
406
+ "license": "Apache-2.0",
407
+ "optional": true,
408
+ "os": [
409
+ "linux"
410
+ ],
411
+ "engines": {
412
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
413
+ },
414
+ "funding": {
415
+ "url": "https://opencollective.com/libvips"
416
+ },
417
+ "optionalDependencies": {
418
+ "@img/sharp-libvips-linux-ppc64": "1.2.4"
419
+ }
420
+ },
421
+ "node_modules/@img/sharp-linux-riscv64": {
422
+ "version": "0.34.5",
423
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
424
+ "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
425
+ "cpu": [
426
+ "riscv64"
427
+ ],
428
+ "license": "Apache-2.0",
429
+ "optional": true,
430
+ "os": [
431
+ "linux"
432
+ ],
433
+ "engines": {
434
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
435
+ },
436
+ "funding": {
437
+ "url": "https://opencollective.com/libvips"
438
+ },
439
+ "optionalDependencies": {
440
+ "@img/sharp-libvips-linux-riscv64": "1.2.4"
441
+ }
442
+ },
443
+ "node_modules/@img/sharp-linux-s390x": {
444
+ "version": "0.34.5",
445
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
446
+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
447
+ "cpu": [
448
+ "s390x"
449
+ ],
450
+ "license": "Apache-2.0",
451
+ "optional": true,
452
+ "os": [
453
+ "linux"
454
+ ],
455
+ "engines": {
456
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
457
+ },
458
+ "funding": {
459
+ "url": "https://opencollective.com/libvips"
460
+ },
461
+ "optionalDependencies": {
462
+ "@img/sharp-libvips-linux-s390x": "1.2.4"
463
+ }
464
+ },
465
+ "node_modules/@img/sharp-linux-x64": {
466
+ "version": "0.34.5",
467
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
468
+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
469
+ "cpu": [
470
+ "x64"
471
+ ],
472
+ "license": "Apache-2.0",
473
+ "optional": true,
474
+ "os": [
475
+ "linux"
476
+ ],
477
+ "engines": {
478
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
479
+ },
480
+ "funding": {
481
+ "url": "https://opencollective.com/libvips"
482
+ },
483
+ "optionalDependencies": {
484
+ "@img/sharp-libvips-linux-x64": "1.2.4"
485
+ }
486
+ },
487
+ "node_modules/@img/sharp-linuxmusl-arm64": {
488
+ "version": "0.34.5",
489
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
490
+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
491
+ "cpu": [
492
+ "arm64"
493
+ ],
494
+ "license": "Apache-2.0",
495
+ "optional": true,
496
+ "os": [
497
+ "linux"
498
+ ],
499
+ "engines": {
500
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
501
+ },
502
+ "funding": {
503
+ "url": "https://opencollective.com/libvips"
504
+ },
505
+ "optionalDependencies": {
506
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
507
+ }
508
+ },
509
+ "node_modules/@img/sharp-linuxmusl-x64": {
510
+ "version": "0.34.5",
511
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
512
+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
513
+ "cpu": [
514
+ "x64"
515
+ ],
516
+ "license": "Apache-2.0",
517
+ "optional": true,
518
+ "os": [
519
+ "linux"
520
+ ],
521
+ "engines": {
522
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
523
+ },
524
+ "funding": {
525
+ "url": "https://opencollective.com/libvips"
526
+ },
527
+ "optionalDependencies": {
528
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
529
+ }
530
+ },
531
+ "node_modules/@img/sharp-wasm32": {
532
+ "version": "0.34.5",
533
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
534
+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
535
+ "cpu": [
536
+ "wasm32"
537
+ ],
538
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
539
+ "optional": true,
540
+ "dependencies": {
541
+ "@emnapi/runtime": "^1.7.0"
542
+ },
543
+ "engines": {
544
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
545
+ },
546
+ "funding": {
547
+ "url": "https://opencollective.com/libvips"
548
+ }
549
+ },
550
+ "node_modules/@img/sharp-win32-arm64": {
551
+ "version": "0.34.5",
552
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
553
+ "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
554
+ "cpu": [
555
+ "arm64"
556
+ ],
557
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
558
+ "optional": true,
559
+ "os": [
560
+ "win32"
561
+ ],
562
+ "engines": {
563
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
564
+ },
565
+ "funding": {
566
+ "url": "https://opencollective.com/libvips"
567
  }
568
  },
569
+ "node_modules/@img/sharp-win32-ia32": {
570
+ "version": "0.34.5",
571
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
572
+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
573
+ "cpu": [
574
+ "ia32"
575
+ ],
576
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
577
  "optional": true,
578
+ "os": [
579
+ "win32"
580
+ ],
581
+ "engines": {
582
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
583
+ },
584
+ "funding": {
585
+ "url": "https://opencollective.com/libvips"
586
+ }
587
+ },
588
+ "node_modules/@img/sharp-win32-x64": {
589
+ "version": "0.34.5",
590
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
591
+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
592
+ "cpu": [
593
+ "x64"
594
+ ],
595
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
596
+ "optional": true,
597
+ "os": [
598
+ "win32"
599
+ ],
600
+ "engines": {
601
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
602
+ },
603
+ "funding": {
604
+ "url": "https://opencollective.com/libvips"
605
+ }
606
+ },
607
+ "node_modules/@isaacs/fs-minipass": {
608
+ "version": "4.0.1",
609
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
610
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
611
+ "license": "ISC",
612
  "dependencies": {
613
+ "minipass": "^7.0.4"
614
+ },
615
+ "engines": {
616
+ "node": ">=18.0.0"
617
  }
618
  },
619
  "node_modules/@loaders.gl/core": {
 
865
  "integrity": "sha512-4VpAyMHOqydSvPlEyHwXaE+AkIdR03nX+Qhlxsk2D/IW4OVmDZgIsvJB1cDzyEEtcfKcnaEbfXeiPgejBceT6g==",
866
  "license": "MIT"
867
  },
868
+ "node_modules/@protobufjs/aspromise": {
869
+ "version": "1.1.2",
870
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
871
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
872
+ "license": "BSD-3-Clause"
873
+ },
874
+ "node_modules/@protobufjs/base64": {
875
+ "version": "1.1.2",
876
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
877
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
878
+ "license": "BSD-3-Clause"
879
+ },
880
+ "node_modules/@protobufjs/codegen": {
881
+ "version": "2.0.5",
882
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
883
+ "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
884
+ "license": "BSD-3-Clause"
885
+ },
886
+ "node_modules/@protobufjs/eventemitter": {
887
+ "version": "1.1.1",
888
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
889
+ "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
890
+ "license": "BSD-3-Clause"
891
+ },
892
+ "node_modules/@protobufjs/fetch": {
893
+ "version": "1.1.1",
894
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
895
+ "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
896
+ "license": "BSD-3-Clause",
897
+ "dependencies": {
898
+ "@protobufjs/aspromise": "^1.1.1"
899
+ }
900
+ },
901
+ "node_modules/@protobufjs/float": {
902
+ "version": "1.0.2",
903
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
904
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
905
+ "license": "BSD-3-Clause"
906
+ },
907
+ "node_modules/@protobufjs/path": {
908
+ "version": "1.1.2",
909
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
910
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
911
+ "license": "BSD-3-Clause"
912
+ },
913
+ "node_modules/@protobufjs/pool": {
914
+ "version": "1.1.0",
915
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
916
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
917
+ "license": "BSD-3-Clause"
918
+ },
919
+ "node_modules/@protobufjs/utf8": {
920
+ "version": "1.1.1",
921
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz",
922
+ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
923
+ "license": "BSD-3-Clause"
924
+ },
925
  "node_modules/@rolldown/binding-android-arm64": {
926
  "version": "1.0.3",
927
  "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
 
1353
  "node": ">=12.17"
1354
  }
1355
  },
1356
+ "node_modules/boolean": {
1357
+ "version": "3.2.0",
1358
+ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
1359
+ "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
1360
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
1361
+ "license": "MIT"
1362
+ },
1363
  "node_modules/chalk": {
1364
  "version": "4.1.2",
1365
  "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
 
1397
  "integrity": "sha512-pmcgaGHAxfYtKr4BtSaM/+/3vvQJJLkvQibEdO2eb5eiHrRgEAd43M2tpBGi4eIX32kXyvPLQVT23qNnuFaa8w==",
1398
  "license": "ISC"
1399
  },
1400
+ "node_modules/chownr": {
1401
+ "version": "3.0.0",
1402
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
1403
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
1404
+ "license": "BlueOak-1.0.0",
1405
+ "engines": {
1406
+ "node": ">=18"
1407
+ }
1408
+ },
1409
  "node_modules/color-convert": {
1410
  "version": "2.0.1",
1411
  "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
 
1475
  "dev": true,
1476
  "license": "MIT"
1477
  },
1478
+ "node_modules/define-data-property": {
1479
+ "version": "1.1.4",
1480
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
1481
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
1482
+ "license": "MIT",
1483
+ "dependencies": {
1484
+ "es-define-property": "^1.0.0",
1485
+ "es-errors": "^1.3.0",
1486
+ "gopd": "^1.0.1"
1487
+ },
1488
+ "engines": {
1489
+ "node": ">= 0.4"
1490
+ },
1491
+ "funding": {
1492
+ "url": "https://github.com/sponsors/ljharb"
1493
+ }
1494
+ },
1495
+ "node_modules/define-properties": {
1496
+ "version": "1.2.1",
1497
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
1498
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
1499
+ "license": "MIT",
1500
+ "dependencies": {
1501
+ "define-data-property": "^1.0.1",
1502
+ "has-property-descriptors": "^1.0.0",
1503
+ "object-keys": "^1.1.1"
1504
+ },
1505
+ "engines": {
1506
+ "node": ">= 0.4"
1507
+ },
1508
+ "funding": {
1509
+ "url": "https://github.com/sponsors/ljharb"
1510
+ }
1511
+ },
1512
  "node_modules/detect-libc": {
1513
  "version": "2.1.2",
1514
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1515
  "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
 
1516
  "license": "Apache-2.0",
1517
  "engines": {
1518
  "node": ">=8"
1519
  }
1520
  },
1521
+ "node_modules/detect-node": {
1522
+ "version": "2.1.0",
1523
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
1524
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
1525
+ "license": "MIT"
1526
+ },
1527
  "node_modules/earcut": {
1528
  "version": "2.2.4",
1529
  "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
1530
  "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
1531
  "license": "ISC"
1532
  },
1533
+ "node_modules/es-define-property": {
1534
+ "version": "1.0.1",
1535
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
1536
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
1537
+ "license": "MIT",
1538
+ "engines": {
1539
+ "node": ">= 0.4"
1540
+ }
1541
+ },
1542
+ "node_modules/es-errors": {
1543
+ "version": "1.3.0",
1544
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
1545
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
1546
+ "license": "MIT",
1547
+ "engines": {
1548
+ "node": ">= 0.4"
1549
+ }
1550
+ },
1551
+ "node_modules/es6-error": {
1552
+ "version": "4.1.1",
1553
+ "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
1554
+ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
1555
+ "license": "MIT"
1556
+ },
1557
+ "node_modules/escape-string-regexp": {
1558
+ "version": "4.0.0",
1559
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
1560
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
1561
+ "license": "MIT",
1562
+ "engines": {
1563
+ "node": ">=10"
1564
+ },
1565
+ "funding": {
1566
+ "url": "https://github.com/sponsors/sindresorhus"
1567
+ }
1568
+ },
1569
  "node_modules/fdir": {
1570
  "version": "6.5.0",
1571
  "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
 
1634
  "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
1635
  "license": "MIT"
1636
  },
1637
+ "node_modules/global-agent": {
1638
+ "version": "3.0.0",
1639
+ "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
1640
+ "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
1641
+ "license": "BSD-3-Clause",
1642
+ "dependencies": {
1643
+ "boolean": "^3.0.1",
1644
+ "es6-error": "^4.1.1",
1645
+ "matcher": "^3.0.0",
1646
+ "roarr": "^2.15.3",
1647
+ "semver": "^7.3.2",
1648
+ "serialize-error": "^7.0.1"
1649
+ },
1650
+ "engines": {
1651
+ "node": ">=10.0"
1652
+ }
1653
+ },
1654
+ "node_modules/globalthis": {
1655
+ "version": "1.0.4",
1656
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
1657
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
1658
+ "license": "MIT",
1659
+ "dependencies": {
1660
+ "define-properties": "^1.2.1",
1661
+ "gopd": "^1.0.1"
1662
+ },
1663
+ "engines": {
1664
+ "node": ">= 0.4"
1665
+ },
1666
+ "funding": {
1667
+ "url": "https://github.com/sponsors/ljharb"
1668
+ }
1669
+ },
1670
+ "node_modules/gopd": {
1671
+ "version": "1.2.0",
1672
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1673
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1674
+ "license": "MIT",
1675
+ "engines": {
1676
+ "node": ">= 0.4"
1677
+ },
1678
+ "funding": {
1679
+ "url": "https://github.com/sponsors/ljharb"
1680
+ }
1681
+ },
1682
+ "node_modules/guid-typescript": {
1683
+ "version": "1.0.9",
1684
+ "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
1685
+ "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
1686
+ "license": "ISC"
1687
+ },
1688
  "node_modules/has-flag": {
1689
  "version": "4.0.0",
1690
  "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
 
1694
  "node": ">=8"
1695
  }
1696
  },
1697
+ "node_modules/has-property-descriptors": {
1698
+ "version": "1.0.2",
1699
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
1700
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
1701
+ "license": "MIT",
1702
+ "dependencies": {
1703
+ "es-define-property": "^1.0.0"
1704
+ },
1705
+ "funding": {
1706
+ "url": "https://github.com/sponsors/ljharb"
1707
+ }
1708
+ },
1709
  "node_modules/json-bignum": {
1710
  "version": "0.0.3",
1711
  "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz",
 
1714
  "node": ">=0.8"
1715
  }
1716
  },
1717
+ "node_modules/json-stringify-safe": {
1718
+ "version": "5.0.1",
1719
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
1720
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
1721
+ "license": "ISC"
1722
+ },
1723
  "node_modules/kdbush": {
1724
  "version": "4.1.0",
1725
  "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz",
1726
  "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
1727
  "license": "ISC"
1728
  },
1729
+ "node_modules/kokoro-js": {
1730
+ "version": "1.2.1",
1731
+ "resolved": "https://registry.npmjs.org/kokoro-js/-/kokoro-js-1.2.1.tgz",
1732
+ "integrity": "sha512-oq0HZJWis3t8lERkMJh84WLU86dpYD0EuBPtqYnLlQzyFP1OkyBRDcweAqCfhNOpltyN9j/azp1H6uuC47gShw==",
1733
+ "license": "Apache-2.0",
1734
+ "dependencies": {
1735
+ "@huggingface/transformers": "^3.5.1",
1736
+ "phonemizer": "^1.2.1"
1737
+ }
1738
+ },
1739
  "node_modules/lightningcss": {
1740
  "version": "1.32.0",
1741
  "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
 
2003
  "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
2004
  "license": "MIT"
2005
  },
2006
+ "node_modules/long": {
2007
+ "version": "5.3.2",
2008
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
2009
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
2010
+ "license": "Apache-2.0"
2011
+ },
2012
  "node_modules/lucide-react": {
2013
  "version": "0.468.0",
2014
  "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz",
 
2072
  "tinyqueue": "3.0.0"
2073
  }
2074
  },
2075
+ "node_modules/matcher": {
2076
+ "version": "3.0.0",
2077
+ "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
2078
+ "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
2079
+ "license": "MIT",
2080
+ "dependencies": {
2081
+ "escape-string-regexp": "^4.0.0"
2082
+ },
2083
+ "engines": {
2084
+ "node": ">=10"
2085
+ }
2086
+ },
2087
+ "node_modules/minipass": {
2088
+ "version": "7.1.3",
2089
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
2090
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
2091
+ "license": "BlueOak-1.0.0",
2092
+ "engines": {
2093
+ "node": ">=16 || 14 >=14.17"
2094
+ }
2095
+ },
2096
+ "node_modules/minizlib": {
2097
+ "version": "3.1.0",
2098
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
2099
+ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
2100
+ "license": "MIT",
2101
+ "dependencies": {
2102
+ "minipass": "^7.1.2"
2103
+ },
2104
+ "engines": {
2105
+ "node": ">= 18"
2106
+ }
2107
+ },
2108
  "node_modules/mjolnir.js": {
2109
  "version": "3.0.0",
2110
  "resolved": "https://registry.npmjs.org/mjolnir.js/-/mjolnir.js-3.0.0.tgz",
 
2136
  "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
2137
  }
2138
  },
2139
+ "node_modules/object-keys": {
2140
+ "version": "1.1.1",
2141
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
2142
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
2143
+ "license": "MIT",
2144
+ "engines": {
2145
+ "node": ">= 0.4"
2146
+ }
2147
+ },
2148
+ "node_modules/onnxruntime-common": {
2149
+ "version": "1.21.0",
2150
+ "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz",
2151
+ "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==",
2152
+ "license": "MIT"
2153
+ },
2154
+ "node_modules/onnxruntime-node": {
2155
+ "version": "1.21.0",
2156
+ "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz",
2157
+ "integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==",
2158
+ "hasInstallScript": true,
2159
+ "license": "MIT",
2160
+ "os": [
2161
+ "win32",
2162
+ "darwin",
2163
+ "linux"
2164
+ ],
2165
+ "dependencies": {
2166
+ "global-agent": "^3.0.0",
2167
+ "onnxruntime-common": "1.21.0",
2168
+ "tar": "^7.0.1"
2169
+ }
2170
+ },
2171
+ "node_modules/onnxruntime-web": {
2172
+ "version": "1.22.0-dev.20250409-89f8206ba4",
2173
+ "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.22.0-dev.20250409-89f8206ba4.tgz",
2174
+ "integrity": "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==",
2175
+ "license": "MIT",
2176
+ "dependencies": {
2177
+ "flatbuffers": "^25.1.24",
2178
+ "guid-typescript": "^1.0.9",
2179
+ "long": "^5.2.3",
2180
+ "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4",
2181
+ "platform": "^1.3.6",
2182
+ "protobufjs": "^7.2.4"
2183
+ }
2184
+ },
2185
+ "node_modules/onnxruntime-web/node_modules/onnxruntime-common": {
2186
+ "version": "1.22.0-dev.20250409-89f8206ba4",
2187
+ "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.22.0-dev.20250409-89f8206ba4.tgz",
2188
+ "integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==",
2189
+ "license": "MIT"
2190
+ },
2191
  "node_modules/pbf": {
2192
  "version": "4.0.2",
2193
  "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.2.tgz",
 
2200
  "pbf": "bin/pbf"
2201
  }
2202
  },
2203
+ "node_modules/phonemizer": {
2204
+ "version": "1.2.1",
2205
+ "resolved": "https://registry.npmjs.org/phonemizer/-/phonemizer-1.2.1.tgz",
2206
+ "integrity": "sha512-v0KJ4mi2T4Q7eJQ0W15Xd4G9k4kICSXE8bpDeJ8jisL4RyJhNWsweKTOi88QXFc4r4LZlz5jVL5lCHhkpdT71A==",
2207
+ "license": "Apache-2.0"
2208
+ },
2209
  "node_modules/picocolors": {
2210
  "version": "1.1.1",
2211
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
 
2226
  "url": "https://github.com/sponsors/jonschlinkert"
2227
  }
2228
  },
2229
+ "node_modules/platform": {
2230
+ "version": "1.3.6",
2231
+ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
2232
+ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
2233
+ "license": "MIT"
2234
+ },
2235
  "node_modules/postcss": {
2236
  "version": "8.5.15",
2237
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
 
2267
  "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
2268
  "license": "ISC"
2269
  },
2270
+ "node_modules/protobufjs": {
2271
+ "version": "7.6.4",
2272
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz",
2273
+ "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==",
2274
+ "hasInstallScript": true,
2275
+ "license": "BSD-3-Clause",
2276
+ "dependencies": {
2277
+ "@protobufjs/aspromise": "^1.1.2",
2278
+ "@protobufjs/base64": "^1.1.2",
2279
+ "@protobufjs/codegen": "^2.0.5",
2280
+ "@protobufjs/eventemitter": "^1.1.1",
2281
+ "@protobufjs/fetch": "^1.1.1",
2282
+ "@protobufjs/float": "^1.0.2",
2283
+ "@protobufjs/path": "^1.1.2",
2284
+ "@protobufjs/pool": "^1.1.0",
2285
+ "@protobufjs/utf8": "^1.1.1",
2286
+ "@types/node": ">=13.7.0",
2287
+ "long": "^5.3.2"
2288
+ },
2289
+ "engines": {
2290
+ "node": ">=12.0.0"
2291
+ }
2292
+ },
2293
  "node_modules/protocol-buffers-schema": {
2294
  "version": "3.6.1",
2295
  "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz",
 
2332
  "protocol-buffers-schema": "^3.3.1"
2333
  }
2334
  },
2335
+ "node_modules/roarr": {
2336
+ "version": "2.15.4",
2337
+ "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
2338
+ "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
2339
+ "license": "BSD-3-Clause",
2340
+ "dependencies": {
2341
+ "boolean": "^3.0.1",
2342
+ "detect-node": "^2.0.4",
2343
+ "globalthis": "^1.0.1",
2344
+ "json-stringify-safe": "^5.0.1",
2345
+ "semver-compare": "^1.0.0",
2346
+ "sprintf-js": "^1.1.2"
2347
+ },
2348
+ "engines": {
2349
+ "node": ">=8.0"
2350
+ }
2351
+ },
2352
  "node_modules/robust-predicates": {
2353
  "version": "2.0.4",
2354
  "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-2.0.4.tgz",
 
2395
  "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
2396
  "license": "MIT"
2397
  },
2398
+ "node_modules/semver": {
2399
+ "version": "7.8.4",
2400
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
2401
+ "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
2402
+ "license": "ISC",
2403
+ "bin": {
2404
+ "semver": "bin/semver.js"
2405
+ },
2406
+ "engines": {
2407
+ "node": ">=10"
2408
+ }
2409
+ },
2410
+ "node_modules/semver-compare": {
2411
+ "version": "1.0.0",
2412
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
2413
+ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
2414
+ "license": "MIT"
2415
+ },
2416
+ "node_modules/serialize-error": {
2417
+ "version": "7.0.1",
2418
+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
2419
+ "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
2420
+ "license": "MIT",
2421
+ "dependencies": {
2422
+ "type-fest": "^0.13.1"
2423
+ },
2424
+ "engines": {
2425
+ "node": ">=10"
2426
+ },
2427
+ "funding": {
2428
+ "url": "https://github.com/sponsors/sindresorhus"
2429
+ }
2430
+ },
2431
+ "node_modules/sharp": {
2432
+ "version": "0.34.5",
2433
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
2434
+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
2435
+ "hasInstallScript": true,
2436
+ "license": "Apache-2.0",
2437
+ "dependencies": {
2438
+ "@img/colour": "^1.0.0",
2439
+ "detect-libc": "^2.1.2",
2440
+ "semver": "^7.7.3"
2441
+ },
2442
+ "engines": {
2443
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
2444
+ },
2445
+ "funding": {
2446
+ "url": "https://opencollective.com/libvips"
2447
+ },
2448
+ "optionalDependencies": {
2449
+ "@img/sharp-darwin-arm64": "0.34.5",
2450
+ "@img/sharp-darwin-x64": "0.34.5",
2451
+ "@img/sharp-libvips-darwin-arm64": "1.2.4",
2452
+ "@img/sharp-libvips-darwin-x64": "1.2.4",
2453
+ "@img/sharp-libvips-linux-arm": "1.2.4",
2454
+ "@img/sharp-libvips-linux-arm64": "1.2.4",
2455
+ "@img/sharp-libvips-linux-ppc64": "1.2.4",
2456
+ "@img/sharp-libvips-linux-riscv64": "1.2.4",
2457
+ "@img/sharp-libvips-linux-s390x": "1.2.4",
2458
+ "@img/sharp-libvips-linux-x64": "1.2.4",
2459
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
2460
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
2461
+ "@img/sharp-linux-arm": "0.34.5",
2462
+ "@img/sharp-linux-arm64": "0.34.5",
2463
+ "@img/sharp-linux-ppc64": "0.34.5",
2464
+ "@img/sharp-linux-riscv64": "0.34.5",
2465
+ "@img/sharp-linux-s390x": "0.34.5",
2466
+ "@img/sharp-linux-x64": "0.34.5",
2467
+ "@img/sharp-linuxmusl-arm64": "0.34.5",
2468
+ "@img/sharp-linuxmusl-x64": "0.34.5",
2469
+ "@img/sharp-wasm32": "0.34.5",
2470
+ "@img/sharp-win32-arm64": "0.34.5",
2471
+ "@img/sharp-win32-ia32": "0.34.5",
2472
+ "@img/sharp-win32-x64": "0.34.5"
2473
+ }
2474
+ },
2475
  "node_modules/source-map-js": {
2476
  "version": "1.2.1",
2477
  "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
 
2488
  "integrity": "sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ==",
2489
  "license": "MIT"
2490
  },
2491
+ "node_modules/sprintf-js": {
2492
+ "version": "1.1.3",
2493
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
2494
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
2495
+ "license": "BSD-3-Clause"
2496
+ },
2497
  "node_modules/supercluster": {
2498
  "version": "8.0.1",
2499
  "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
 
2528
  "node": ">=12.17"
2529
  }
2530
  },
2531
+ "node_modules/tar": {
2532
+ "version": "7.5.16",
2533
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
2534
+ "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==",
2535
+ "license": "BlueOak-1.0.0",
2536
+ "dependencies": {
2537
+ "@isaacs/fs-minipass": "^4.0.0",
2538
+ "chownr": "^3.0.0",
2539
+ "minipass": "^7.1.2",
2540
+ "minizlib": "^3.1.0",
2541
+ "yallist": "^5.0.0"
2542
+ },
2543
+ "engines": {
2544
+ "node": ">=18"
2545
+ }
2546
+ },
2547
  "node_modules/tinyglobby": {
2548
  "version": "0.2.17",
2549
  "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
 
2573
  "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2574
  "license": "0BSD"
2575
  },
2576
+ "node_modules/type-fest": {
2577
+ "version": "0.13.1",
2578
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
2579
+ "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
2580
+ "license": "(MIT OR CC0-1.0)",
2581
+ "engines": {
2582
+ "node": ">=10"
2583
+ },
2584
+ "funding": {
2585
+ "url": "https://github.com/sponsors/sindresorhus"
2586
+ }
2587
+ },
2588
  "node_modules/typescript": {
2589
  "version": "5.9.3",
2590
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
 
2700
  "engines": {
2701
  "node": ">=12.17"
2702
  }
2703
+ },
2704
+ "node_modules/yallist": {
2705
+ "version": "5.0.0",
2706
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
2707
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
2708
+ "license": "BlueOak-1.0.0",
2709
+ "engines": {
2710
+ "node": ">=18"
2711
+ }
2712
  }
2713
  }
2714
  }
package.json CHANGED
@@ -17,6 +17,7 @@
17
  "@deck.gl/core": "^9.0.0",
18
  "@deck.gl/layers": "^9.0.0",
19
  "@deck.gl/mapbox": "^9.0.0",
 
20
  "lucide-react": "^0.468.0",
21
  "mapbox-gl": "^3.11.1",
22
  "react": "^19.0.0",
 
17
  "@deck.gl/core": "^9.0.0",
18
  "@deck.gl/layers": "^9.0.0",
19
  "@deck.gl/mapbox": "^9.0.0",
20
+ "kokoro-js": "^1.2.1",
21
  "lucide-react": "^0.468.0",
22
  "mapbox-gl": "^3.11.1",
23
  "react": "^19.0.0",
server/agent-core.mjs CHANGED
@@ -416,7 +416,7 @@ function buildEvidenceRecommendation(run, selected, webResearch, evidenceNotes)
416
  }
417
 
418
  function sanitizeTrace(trace = []) {
419
- const visibleTools = new Set(["discover_neighborhoods", "plan_research", "parse_renter_intent", "housing_web_research", "score_neighborhoods", "hf_reasoning", "nvidia_reasoning", "ollama_reasoning", "llamacpp_reasoning"]);
420
  return trace
421
  .filter((step) => visibleTools.has(step.tool) || step.tool.startsWith("agent_"))
422
  .map((step) => {
 
416
  }
417
 
418
  function sanitizeTrace(trace = []) {
419
+ const visibleTools = new Set(["discover_neighborhoods", "plan_research", "parse_renter_intent", "housing_web_research", "score_neighborhoods", "hf_reasoning", "nvidia_reasoning", "llamacpp_reasoning"]);
420
  return trace
421
  .filter((step) => visibleTools.has(step.tool) || step.tool.startsWith("agent_"))
422
  .map((step) => {
server/index.mjs CHANGED
@@ -4,7 +4,6 @@ import { resolve } from "node:path";
4
  import { applyEvidencePolicy, buildLocalAgentRun, mergeModelRecommendation } from "./agent-core.mjs";
5
  import { configuredModel, runHfAgent } from "./hf-client.mjs";
6
  import { configuredNvidiaModel, runNvidiaAgent } from "./nvidia-client.mjs";
7
- import { configuredOllamaModel, runOllamaAgent } from "./ollama-client.mjs";
8
  import { configuredLlamaCppModel, llamaCppEnabled, runLlamaCppAgent } from "./llamacpp-client.mjs";
9
  import { runAgentFanOut } from "./agent-fanout.mjs";
10
  import { discoverNeighborhoods, buildDiscoveredRows } from "./discover.mjs";
@@ -45,7 +44,6 @@ const server = createServer(async (request, response) => {
45
  ),
46
  nvidiaConfigured: Boolean(process.env.NVIDIA_API_KEY || process.env.NGC_API_KEY),
47
  llamacppConfigured: llamaCppEnabled(),
48
- ollamaConfigured: Boolean(configuredOllamaModel()),
49
  searchProvider: configuredSearchProvider(),
50
  search: searchProviderStatus(),
51
  researchEnabled: process.env.RESEARCH_ENABLED !== "0",
@@ -302,9 +300,6 @@ async function runConfiguredModel(localRun) {
302
  if (provider === "llamacpp") {
303
  return { provider, ...(await runLlamaCppAgent(localRun)) };
304
  }
305
- if (provider === "ollama") {
306
- return { provider, ...(await runOllamaAgent(localRun)) };
307
- }
308
  if (provider === "nvidia") {
309
  return { provider, ...(await runNvidiaAgent(localRun)) };
310
  }
@@ -313,20 +308,18 @@ async function runConfiguredModel(localRun) {
313
  }
314
 
315
  // auto: Nemotron (NVIDIA) first when keyed, then a local llama.cpp/OpenBMB GGUF,
316
- // then Ollama, then the HF model — so the agent always lands on a working brain.
317
  const nvidia = await runNvidiaAgent(localRun);
318
  if (nvidia.status === "done") return { provider: "nvidia", ...nvidia };
319
  const llamacpp = await runLlamaCppAgent(localRun);
320
  if (llamacpp.status === "done") return { provider: "llamacpp", ...llamacpp };
321
- const ollama = await runOllamaAgent(localRun);
322
- if (ollama.status === "done") return { provider: "ollama", ...ollama };
323
  const hf = await runHfAgent(localRun);
324
  if (hf.status === "done") return { provider: "hf", ...hf };
325
 
326
  return {
327
  provider: "auto",
328
  status: "error",
329
- reason: `NVIDIA: ${nvidia.reason}; llama.cpp: ${llamacpp.reason}; Ollama: ${ollama.reason}; Hugging Face: ${hf.reason}`,
330
  model: configuredAgentModel(),
331
  result: null,
332
  };
@@ -334,7 +327,7 @@ async function runConfiguredModel(localRun) {
334
 
335
  function configuredProvider() {
336
  const raw = (process.env.AGENT_MODEL_PROVIDER || process.env.AGENT_PROVIDER || "hf").toLowerCase();
337
- if (["nvidia", "llamacpp", "ollama", "hf", "auto"].includes(raw)) return raw;
338
  return "hf";
339
  }
340
 
@@ -342,9 +335,8 @@ function configuredAgentModel() {
342
  const provider = configuredProvider();
343
  if (provider === "nvidia") return configuredNvidiaModel();
344
  if (provider === "llamacpp") return configuredLlamaCppModel();
345
- if (provider === "ollama") return configuredOllamaModel();
346
  if (provider === "auto") {
347
- return configuredNvidiaModel() || configuredLlamaCppModel() || configuredOllamaModel() || configuredModel();
348
  }
349
  return configuredModel();
350
  }
 
4
  import { applyEvidencePolicy, buildLocalAgentRun, mergeModelRecommendation } from "./agent-core.mjs";
5
  import { configuredModel, runHfAgent } from "./hf-client.mjs";
6
  import { configuredNvidiaModel, runNvidiaAgent } from "./nvidia-client.mjs";
 
7
  import { configuredLlamaCppModel, llamaCppEnabled, runLlamaCppAgent } from "./llamacpp-client.mjs";
8
  import { runAgentFanOut } from "./agent-fanout.mjs";
9
  import { discoverNeighborhoods, buildDiscoveredRows } from "./discover.mjs";
 
44
  ),
45
  nvidiaConfigured: Boolean(process.env.NVIDIA_API_KEY || process.env.NGC_API_KEY),
46
  llamacppConfigured: llamaCppEnabled(),
 
47
  searchProvider: configuredSearchProvider(),
48
  search: searchProviderStatus(),
49
  researchEnabled: process.env.RESEARCH_ENABLED !== "0",
 
300
  if (provider === "llamacpp") {
301
  return { provider, ...(await runLlamaCppAgent(localRun)) };
302
  }
 
 
 
303
  if (provider === "nvidia") {
304
  return { provider, ...(await runNvidiaAgent(localRun)) };
305
  }
 
308
  }
309
 
310
  // auto: Nemotron (NVIDIA) first when keyed, then a local llama.cpp/OpenBMB GGUF,
311
+ // then the HF model — so the agent always lands on a working brain.
312
  const nvidia = await runNvidiaAgent(localRun);
313
  if (nvidia.status === "done") return { provider: "nvidia", ...nvidia };
314
  const llamacpp = await runLlamaCppAgent(localRun);
315
  if (llamacpp.status === "done") return { provider: "llamacpp", ...llamacpp };
 
 
316
  const hf = await runHfAgent(localRun);
317
  if (hf.status === "done") return { provider: "hf", ...hf };
318
 
319
  return {
320
  provider: "auto",
321
  status: "error",
322
+ reason: `NVIDIA: ${nvidia.reason}; llama.cpp: ${llamacpp.reason}; Hugging Face: ${hf.reason}`,
323
  model: configuredAgentModel(),
324
  result: null,
325
  };
 
327
 
328
  function configuredProvider() {
329
  const raw = (process.env.AGENT_MODEL_PROVIDER || process.env.AGENT_PROVIDER || "hf").toLowerCase();
330
+ if (["nvidia", "llamacpp", "hf", "auto"].includes(raw)) return raw;
331
  return "hf";
332
  }
333
 
 
335
  const provider = configuredProvider();
336
  if (provider === "nvidia") return configuredNvidiaModel();
337
  if (provider === "llamacpp") return configuredLlamaCppModel();
 
338
  if (provider === "auto") {
339
+ return configuredNvidiaModel() || configuredLlamaCppModel() || configuredModel();
340
  }
341
  return configuredModel();
342
  }
server/model-chat.mjs CHANGED
@@ -1,5 +1,5 @@
1
  // Shared "talk to the configured agentic model" helper. Resolves whichever brain is
2
- // active (Nemotron / llama.cpp+OpenBMB / Ollama / HF) and sends an OpenAI-compatible chat
3
  // request, so the per-agent fan-out reasons with the SAME agentic model as the synthesiser.
4
  import { stripReasoning } from "./model-prompt.mjs";
5
 
@@ -18,7 +18,6 @@ export function resolveActiveProvider(env = process.env) {
18
  const has = {
19
  nvidia: Boolean(env.NVIDIA_API_KEY || env.NGC_API_KEY),
20
  llamacpp: env.LLAMACPP_ENABLED === "1",
21
- ollama: Boolean(env.OLLAMA_MODEL || env.AGENT_OLLAMA_MODEL),
22
  hf: Boolean(env.HF_TOKEN || env.HUGGINGFACEHUB_API_TOKEN || env.HUGGING_FACE_HUB_TOKEN),
23
  };
24
  if (raw !== "auto" && raw !== "") return has[raw] ? raw : firstAvailable(has);
@@ -28,7 +27,6 @@ export function resolveActiveProvider(env = process.env) {
28
  function firstAvailable(has) {
29
  if (has.nvidia) return "nvidia";
30
  if (has.llamacpp) return "llamacpp";
31
- if (has.ollama) return "ollama";
32
  if (has.hf) return "hf";
33
  return null;
34
  }
@@ -37,7 +35,6 @@ function availability(env) {
37
  return {
38
  nvidia: Boolean(env.NVIDIA_API_KEY || env.NGC_API_KEY),
39
  llamacpp: env.LLAMACPP_ENABLED === "1",
40
- ollama: Boolean(env.OLLAMA_MODEL || env.AGENT_OLLAMA_MODEL),
41
  hf: Boolean(env.HF_TOKEN || env.HUGGINGFACEHUB_API_TOKEN || env.HUGGING_FACE_HUB_TOKEN),
42
  };
43
  }
@@ -51,7 +48,6 @@ export function resolveMainProvider(env = process.env) {
51
  if (raw !== "auto" && raw !== "" && has[raw]) return raw;
52
  if (has.nvidia) return "nvidia";
53
  if (has.hf) return "hf";
54
- if (has.ollama) return "ollama";
55
  if (has.llamacpp) return "llamacpp";
56
  return null;
57
  }
@@ -85,14 +81,6 @@ function endpointFor(provider, env) {
85
  grammar: true,
86
  };
87
  }
88
- if (provider === "ollama") {
89
- const host = env.OLLAMA_HOST || "http://127.0.0.1:11434";
90
- return {
91
- url: env.OLLAMA_CHAT_COMPLETIONS_URL || `${host.replace(/\/$/, "")}/v1/chat/completions`,
92
- model: env.OLLAMA_MODEL || env.AGENT_OLLAMA_MODEL,
93
- auth: "ollama",
94
- };
95
- }
96
  return {
97
  url: env.HF_CHAT_COMPLETIONS_URL || "https://router.huggingface.co/v1/chat/completions",
98
  model: env.HF_MODEL || "Qwen/Qwen3-Coder-30B-A3B-Instruct",
 
1
  // Shared "talk to the configured agentic model" helper. Resolves whichever brain is
2
+ // active (Nemotron / llama.cpp+OpenBMB / HF) and sends an OpenAI-compatible chat
3
  // request, so the per-agent fan-out reasons with the SAME agentic model as the synthesiser.
4
  import { stripReasoning } from "./model-prompt.mjs";
5
 
 
18
  const has = {
19
  nvidia: Boolean(env.NVIDIA_API_KEY || env.NGC_API_KEY),
20
  llamacpp: env.LLAMACPP_ENABLED === "1",
 
21
  hf: Boolean(env.HF_TOKEN || env.HUGGINGFACEHUB_API_TOKEN || env.HUGGING_FACE_HUB_TOKEN),
22
  };
23
  if (raw !== "auto" && raw !== "") return has[raw] ? raw : firstAvailable(has);
 
27
  function firstAvailable(has) {
28
  if (has.nvidia) return "nvidia";
29
  if (has.llamacpp) return "llamacpp";
 
30
  if (has.hf) return "hf";
31
  return null;
32
  }
 
35
  return {
36
  nvidia: Boolean(env.NVIDIA_API_KEY || env.NGC_API_KEY),
37
  llamacpp: env.LLAMACPP_ENABLED === "1",
 
38
  hf: Boolean(env.HF_TOKEN || env.HUGGINGFACEHUB_API_TOKEN || env.HUGGING_FACE_HUB_TOKEN),
39
  };
40
  }
 
48
  if (raw !== "auto" && raw !== "" && has[raw]) return raw;
49
  if (has.nvidia) return "nvidia";
50
  if (has.hf) return "hf";
 
51
  if (has.llamacpp) return "llamacpp";
52
  return null;
53
  }
 
81
  grammar: true,
82
  };
83
  }
 
 
 
 
 
 
 
 
84
  return {
85
  url: env.HF_CHAT_COMPLETIONS_URL || "https://router.huggingface.co/v1/chat/completions",
86
  model: env.HF_MODEL || "Qwen/Qwen3-Coder-30B-A3B-Instruct",
server/ollama-client.mjs DELETED
@@ -1,146 +0,0 @@
1
- import { buildModelContext } from "./agent-core.mjs";
2
- import { SYSTEM_PROMPT, parseModelJson } from "./model-prompt.mjs";
3
-
4
- const DEFAULT_OLLAMA_URL = "http://127.0.0.1:11434/v1/chat/completions";
5
-
6
- export async function runOllamaAgent(localRun, env = process.env) {
7
- if (env.AGENT_OFFLINE === "1") {
8
- return {
9
- status: "skipped",
10
- reason: "AGENT_OFFLINE is enabled",
11
- model: configuredOllamaModel(env),
12
- result: null,
13
- };
14
- }
15
-
16
- const model = configuredOllamaModel(env);
17
- if (!model) {
18
- return {
19
- status: "skipped",
20
- reason: "OLLAMA_MODEL is not configured",
21
- model: null,
22
- result: null,
23
- };
24
- }
25
-
26
- const context = buildModelContext(localRun);
27
- const basePayload = {
28
- model,
29
- messages: [
30
- { role: "system", content: SYSTEM_PROMPT },
31
- { role: "user", content: JSON.stringify(context) },
32
- ],
33
- temperature: Number(env.OLLAMA_TEMPERATURE ?? 0.2),
34
- max_tokens: Number(env.OLLAMA_MAX_TOKENS ?? 900),
35
- stream: false,
36
- };
37
-
38
- const richPayload = {
39
- ...basePayload,
40
- response_format: { type: "json_object" },
41
- reasoning_effort: env.OLLAMA_REASONING_EFFORT || "medium",
42
- };
43
-
44
- try {
45
- const body = await postChat(richPayload, env);
46
- return {
47
- status: "done",
48
- reason: null,
49
- model,
50
- result: parseModelJson(body, "Ollama"),
51
- };
52
- } catch (error) {
53
- if (isRetryableProviderError(error)) {
54
- try {
55
- const body = await postChat(basePayload, env);
56
- return {
57
- status: "done",
58
- reason: "Retried without JSON/reasoning parameters",
59
- model,
60
- result: parseModelJson(body, "Ollama"),
61
- };
62
- } catch (retryError) {
63
- return errorResult(retryError, model);
64
- }
65
- }
66
-
67
- return errorResult(error, model);
68
- }
69
- }
70
-
71
- export function configuredOllamaModel(env = process.env) {
72
- return env.OLLAMA_MODEL || env.AGENT_OLLAMA_MODEL || "";
73
- }
74
-
75
- export function ollamaChatCompletionsUrl(env = process.env) {
76
- if (env.OLLAMA_CHAT_COMPLETIONS_URL) return env.OLLAMA_CHAT_COMPLETIONS_URL;
77
- const host = env.OLLAMA_HOST || "http://127.0.0.1:11434";
78
- return `${host.replace(/\/$/, "")}/v1/chat/completions`;
79
- }
80
-
81
- async function postChat(payload, env) {
82
- const timeoutMs = Number(env.OLLAMA_TIMEOUT_MS ?? 60000);
83
- const controller = new AbortController();
84
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
85
-
86
- try {
87
- const response = await fetch(ollamaChatCompletionsUrl(env), {
88
- method: "POST",
89
- headers: {
90
- Authorization: "Bearer ollama",
91
- "Content-Type": "application/json",
92
- },
93
- body: JSON.stringify(payload),
94
- signal: controller.signal,
95
- });
96
-
97
- const text = await response.text();
98
- if (!response.ok) {
99
- throw new OllamaRequestError(response.status, conciseError(text));
100
- }
101
-
102
- try {
103
- return JSON.parse(text);
104
- } catch {
105
- throw new Error("Ollama returned a non-JSON chat response");
106
- }
107
- } catch (error) {
108
- if (error?.name === "AbortError") {
109
- throw new Error(`Ollama request timed out after ${timeoutMs}ms`);
110
- }
111
- throw error;
112
- } finally {
113
- clearTimeout(timeout);
114
- }
115
- }
116
-
117
- function isRetryableProviderError(error) {
118
- return error instanceof OllamaRequestError && (error.status === 400 || error.status === 422);
119
- }
120
-
121
- function errorResult(error, model) {
122
- return {
123
- status: "error",
124
- reason: error instanceof Error ? error.message : "Unknown Ollama error",
125
- model,
126
- result: null,
127
- };
128
- }
129
-
130
- function conciseError(text) {
131
- if (!text) return "empty error body";
132
- try {
133
- const parsed = JSON.parse(text);
134
- return parsed.error?.message || parsed.error || parsed.message || JSON.stringify(parsed).slice(0, 500);
135
- } catch {
136
- return text.slice(0, 500);
137
- }
138
- }
139
-
140
- class OllamaRequestError extends Error {
141
- constructor(status, message) {
142
- super(`Ollama request failed with HTTP ${status}: ${message}`);
143
- this.name = "OllamaRequestError";
144
- this.status = status;
145
- }
146
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
server/research-tools.mjs CHANGED
@@ -114,34 +114,6 @@ const TORONTO_DATASETS = {
114
  },
115
  };
116
 
117
- const SOURCE_CATEGORIES = [
118
- {
119
- category: "listings",
120
- sourceType: "market_listing",
121
- domains: ["realtor.ca", "zillow.com", "rentals.ca", "condos.ca", "zumper.com", "padmapper.com"],
122
- },
123
- {
124
- category: "community",
125
- sourceType: "resident_discussion",
126
- domains: ["reddit.com/r/askTO", "reddit.com/r/TorontoRenting", "reddit.com/r/toronto"],
127
- },
128
- {
129
- category: "reviews",
130
- sourceType: "local_reviews",
131
- domains: ["google.com/maps", "yelp.ca", "tripadvisor.ca"],
132
- },
133
- {
134
- category: "official",
135
- sourceType: "official_context",
136
- domains: ["toronto.ca", "ttc.ca", "torontopolice.on.ca", "metrolinx.com"],
137
- },
138
- {
139
- category: "market",
140
- sourceType: "market_context",
141
- domains: ["zolo.ca", "housesigma.com", "strata.ca", "rentals.ca/blog", "wowa.ca"],
142
- },
143
- ];
144
-
145
  const officialDataCache = new Map();
146
 
147
  export async function runHousingResearch(localRun, env = process.env) {
 
114
  },
115
  };
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  const officialDataCache = new Map();
118
 
119
  export async function runHousingResearch(localRun, env = process.env) {
src/App.tsx CHANGED
@@ -26,13 +26,13 @@ import {
26
  import MapCanvas from "./components/MapCanvas";
27
  import { DIMENSIONS, type DimensionKey, type LayerKey } from "./data/neighborhoods";
28
  import { runAgentBackend, type AgentBackendRun } from "./lib/agentApi";
 
29
  import {
30
  agentDimension,
31
  cityAgents,
32
  colorForScore,
33
  consensus,
34
  defaultPrompt,
35
- formatRentRange,
36
  getAgentFinding,
37
  getAgentThinking,
38
  initialAgentStates,
@@ -684,31 +684,57 @@ function DetailPanel({
684
  recommendation: AgentBackendRun["recommendation"] | null;
685
  }) {
686
  const fit = consensus(selected.overall);
687
- const [playing, setPlaying] = useState(false);
 
 
688
 
689
  const stopSpeech = useCallback(() => {
 
 
 
 
 
 
690
  if (typeof window !== "undefined" && window.speechSynthesis) window.speechSynthesis.cancel();
691
- setPlaying(false);
692
  }, []);
693
 
694
  // Stop narration when the neighbourhood changes or the panel unmounts.
695
  useEffect(() => stopSpeech, [selected.id, stopSpeech]);
696
 
697
- const togglePlay = () => {
698
- if (typeof window === "undefined" || !window.speechSynthesis) return;
699
- if (playing) {
700
  stopSpeech();
701
  return;
702
  }
703
- const utterance = new SpeechSynthesisUtterance(buildSpokenSummary(selected, recommendation, webResearch));
704
- utterance.rate = 1.02;
705
- utterance.pitch = 1;
706
- utterance.onend = () => setPlaying(false);
707
- utterance.onerror = () => setPlaying(false);
708
- window.speechSynthesis.cancel();
709
- window.speechSynthesis.speak(utterance);
710
- setPlaying(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711
  };
 
 
712
  const safetyFact = findNeighborhoodFact(webResearch, selected.name, "safety");
713
  const rentFact = findNeighborhoodFact(webResearch, selected.name, "rent");
714
  const commuteFact = findNeighborhoodFact(webResearch, selected.name, "commute");
@@ -831,8 +857,14 @@ function DetailPanel({
831
  onClick={togglePlay}
832
  aria-label={playing ? "Stop summary audio" : "Play summary audio"}
833
  >
834
- {playing ? <Square size={14} fill="currentColor" /> : <Volume2 size={15} />}
835
- {playing ? "Stop" : "Play"}
 
 
 
 
 
 
836
  </button>
837
  </div>
838
  </section>
@@ -1079,13 +1111,6 @@ function CompareModal({
1079
  );
1080
  }
1081
 
1082
- function ScoreBadge({ score }: { score: number }) {
1083
- return (
1084
- <span className="score-badge" style={{ background: colorForScore(score || 1) }}>
1085
- {score}
1086
- </span>
1087
- );
1088
- }
1089
 
1090
  function TrustBadge({ sourced }: { sourced: boolean }) {
1091
  return <span className={`trust-badge ${sourced ? "sourced" : "pending"}`}>{sourced ? "Sourced" : "Needs source"}</span>;
@@ -1162,7 +1187,48 @@ function buildSpokenSummary(
1162
  }
1163
  const text = parts.join(" ").replace(/\s+/g, " ").trim();
1164
  const words = text.split(" ");
1165
- return words.length > 60 ? `${words.slice(0, 60).join(" ").replace(/[,;:]+$/, "")}.` : text;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1166
  }
1167
 
1168
  function TowerLogo() {
@@ -1186,80 +1252,6 @@ function TowerLogo() {
1186
  );
1187
  }
1188
 
1189
- function Sparkline({ selected }: { selected: RankedNeighborhood }) {
1190
- const { path, area } = useMemo(() => {
1191
- const count = 26;
1192
- const width = 240;
1193
- const height = 56;
1194
- const pad = 5;
1195
- const values = Array.from({ length: count }, (_, index) => {
1196
- return (
1197
- 10 +
1198
- (selected.trend / 12) * index * 1.15 +
1199
- Math.sin(index * 1.3 + selected.name.length) * 3 +
1200
- Math.cos(index * 0.7 + selected.seed) * 2
1201
- );
1202
- });
1203
- const min = Math.min(...values);
1204
- const max = Math.max(...values);
1205
- const x = (index: number) => (index / (count - 1)) * (width - pad * 2) + pad;
1206
- const y = (value: number) =>
1207
- height - pad - ((value - min) / ((max - min) || 1)) * (height - pad * 2);
1208
- const line = values
1209
- .map((value, index) => `${index ? "L" : "M"}${x(index).toFixed(1)} ${y(value).toFixed(1)}`)
1210
- .join(" ");
1211
- return {
1212
- path: line,
1213
- area: `${line} L ${x(count - 1).toFixed(1)} ${height} L ${x(0).toFixed(1)} ${height} Z`,
1214
- };
1215
- }, [selected]);
1216
-
1217
- return (
1218
- <svg viewBox="0 0 240 56" preserveAspectRatio="none" aria-hidden="true">
1219
- <path d={area} fill="rgba(95, 127, 170, 0.16)" />
1220
- <path d={path} fill="none" stroke="#46648d" strokeWidth="2" strokeLinecap="round" />
1221
- </svg>
1222
- );
1223
- }
1224
-
1225
- function buildWhy(neighborhood: RankedNeighborhood) {
1226
- const rows: Array<{ tone: "good" | "warn"; text: string }> = [];
1227
- const candidates: Array<[DimensionKey, string]> = [
1228
- ["commute", `${neighborhood.comLo}-${neighborhood.comHi} min to Union via ${neighborhood.comMode}`],
1229
- ["affordability", "Lower rent pressure than the downtown core"],
1230
- ["amenities", "Strong grocery, cafe and service access"],
1231
- ["lifestyle", neighborhood.lifeHi],
1232
- ["transit", "Strong transit access"],
1233
- ["safety", "Relatively strong safety signals"],
1234
- ["growth", neighborhood.growthNote],
1235
- ];
1236
-
1237
- candidates.forEach(([dimension, text]) => {
1238
- if (rows.length < 4 && neighborhood.dims[dimension] >= 75) rows.push({ tone: "good", text });
1239
- });
1240
-
1241
- if (rows.length < 3) {
1242
- candidates.forEach(([, text]) => {
1243
- if (rows.length < 4 && !rows.some((row) => row.text === text)) rows.push({ tone: "good", text });
1244
- });
1245
- }
1246
-
1247
- const weakest = candidates.reduce((low, current) =>
1248
- neighborhood.dims[current[0]] < neighborhood.dims[low[0]] ? current : low,
1249
- );
1250
- const warnings: Record<DimensionKey, string> = {
1251
- affordability: "Pricey at the very top end",
1252
- safety: "Mixed safety signals, verify locally",
1253
- commute: "Longer commute on some routes",
1254
- transit: "Transit can need a transfer",
1255
- amenities: "Fewer late-night options",
1256
- lifestyle: "Quieter after dark",
1257
- growth: "Slower projected upside",
1258
- };
1259
- rows.push({ tone: "warn", text: warnings[weakest[0]] });
1260
- return rows;
1261
- }
1262
-
1263
  function priorityChips(parsed: ParsedPrompt) {
1264
  const meta: Record<DimensionKey, [string, string]> = {
1265
  affordability: ["Affordability", "#5f7faa"],
@@ -1336,25 +1328,6 @@ function alpha(hex: string, value: number) {
1336
  return `rgba(${red},${green},${blue},${value})`;
1337
  }
1338
 
1339
- function hasNeighborhoodSource(
1340
- webResearch: AgentBackendRun["webResearch"] | null,
1341
- neighborhood: string,
1342
- sourceTypes: string[],
1343
- ) {
1344
- if (!webResearch?.enabled) return false;
1345
- const target = normalizeEvidenceName(neighborhood);
1346
- return webResearch.sources.some((source) => {
1347
- const sourceNeighborhood = normalizeEvidenceName(source.neighborhood);
1348
- const sourceTypeMatch = sourceTypes.includes(source.sourceType);
1349
- const neighborhoodMatch =
1350
- !sourceNeighborhood ||
1351
- sourceNeighborhood.includes(target) ||
1352
- target.includes(sourceNeighborhood) ||
1353
- source.neighborhood.includes(", ");
1354
- return sourceTypeMatch && neighborhoodMatch;
1355
- });
1356
- }
1357
-
1358
  function findNeighborhoodFact(
1359
  webResearch: AgentBackendRun["webResearch"] | null,
1360
  neighborhood: string,
 
26
  import MapCanvas from "./components/MapCanvas";
27
  import { DIMENSIONS, type DimensionKey, type LayerKey } from "./data/neighborhoods";
28
  import { runAgentBackend, type AgentBackendRun } from "./lib/agentApi";
29
+ import { synthesizeKokoro } from "./lib/tts";
30
  import {
31
  agentDimension,
32
  cityAgents,
33
  colorForScore,
34
  consensus,
35
  defaultPrompt,
 
36
  getAgentFinding,
37
  getAgentThinking,
38
  initialAgentStates,
 
684
  recommendation: AgentBackendRun["recommendation"] | null;
685
  }) {
686
  const fit = consensus(selected.overall);
687
+ const [audioState, setAudioState] = useState<"idle" | "loading" | "playing">("idle");
688
+ const audioRef = useRef<HTMLAudioElement | null>(null);
689
+ const reqRef = useRef(0);
690
 
691
  const stopSpeech = useCallback(() => {
692
+ reqRef.current += 1; // invalidate any in-flight synthesis
693
+ if (audioRef.current) {
694
+ audioRef.current.pause();
695
+ if (audioRef.current.src) URL.revokeObjectURL(audioRef.current.src);
696
+ audioRef.current = null;
697
+ }
698
  if (typeof window !== "undefined" && window.speechSynthesis) window.speechSynthesis.cancel();
699
+ setAudioState("idle");
700
  }, []);
701
 
702
  // Stop narration when the neighbourhood changes or the panel unmounts.
703
  useEffect(() => stopSpeech, [selected.id, stopSpeech]);
704
 
705
+ const togglePlay = async () => {
706
+ if (audioState !== "idle") {
 
707
  stopSpeech();
708
  return;
709
  }
710
+ const text = buildSpokenSummary(selected, recommendation, webResearch);
711
+ const req = (reqRef.current += 1);
712
+ setAudioState("loading");
713
+ try {
714
+ const url = await synthesizeKokoro(text);
715
+ if (reqRef.current !== req) {
716
+ URL.revokeObjectURL(url);
717
+ return;
718
+ }
719
+ const element = new Audio(url);
720
+ audioRef.current = element;
721
+ element.onended = () => {
722
+ URL.revokeObjectURL(url);
723
+ if (reqRef.current === req) setAudioState("idle");
724
+ };
725
+ await element.play();
726
+ if (reqRef.current === req) setAudioState("playing");
727
+ } catch {
728
+ // Kokoro unavailable (still downloading / unsupported) — fall back to the browser voice.
729
+ if (reqRef.current !== req) return;
730
+ speakWithBrowser(text, () => {
731
+ if (reqRef.current === req) setAudioState("idle");
732
+ });
733
+ setAudioState("playing");
734
+ }
735
  };
736
+
737
+ const playing = audioState !== "idle";
738
  const safetyFact = findNeighborhoodFact(webResearch, selected.name, "safety");
739
  const rentFact = findNeighborhoodFact(webResearch, selected.name, "rent");
740
  const commuteFact = findNeighborhoodFact(webResearch, selected.name, "commute");
 
857
  onClick={togglePlay}
858
  aria-label={playing ? "Stop summary audio" : "Play summary audio"}
859
  >
860
+ {audioState === "loading" ? (
861
+ <LoaderCircle size={14} className="spin" />
862
+ ) : audioState === "playing" ? (
863
+ <Square size={14} fill="currentColor" />
864
+ ) : (
865
+ <Volume2 size={15} />
866
+ )}
867
+ {audioState === "loading" ? "Voice…" : audioState === "playing" ? "Stop" : "Play"}
868
  </button>
869
  </div>
870
  </section>
 
1111
  );
1112
  }
1113
 
 
 
 
 
 
 
 
1114
 
1115
  function TrustBadge({ sourced }: { sourced: boolean }) {
1116
  return <span className={`trust-badge ${sourced ? "sourced" : "pending"}`}>{sourced ? "Sourced" : "Needs source"}</span>;
 
1187
  }
1188
  const text = parts.join(" ").replace(/\s+/g, " ").trim();
1189
  const words = text.split(" ");
1190
+ return words.length > 55 ? `${words.slice(0, 55).join(" ").replace(/[,;:]+$/, "")}.` : text;
1191
+ }
1192
+
1193
+ // Choose the most natural-sounding English voice the browser offers. Cloud/"Natural" voices
1194
+ // (non-localService) sound far smoother than the default robotic system voice.
1195
+ function pickVoice(voices: SpeechSynthesisVoice[]) {
1196
+ if (!voices.length) return null;
1197
+ const english = voices.filter((voice) => /^en[-_]?/i.test(voice.lang));
1198
+ const pool = english.length ? english : voices;
1199
+ const score = (voice: SpeechSynthesisVoice) => {
1200
+ let value = 0;
1201
+ if (/natural|neural|premium|enhanced/i.test(voice.name)) value += 100;
1202
+ if (/google/i.test(voice.name)) value += 60;
1203
+ if (/(samantha|ava|allison|serena|zoe|jenny|aria|libby|sonia|nora|emma)/i.test(voice.name)) value += 50;
1204
+ if (!voice.localService) value += 30;
1205
+ if (/en[-_]US/i.test(voice.lang)) value += 12;
1206
+ if (/en[-_](CA|GB|AU)/i.test(voice.lang)) value += 8;
1207
+ if (/zira|david|mark|microsoft/i.test(voice.name)) value -= 15;
1208
+ return value;
1209
+ };
1210
+ return [...pool].sort((a, b) => score(b) - score(a))[0] ?? null;
1211
+ }
1212
+
1213
+ // Fallback narration using the browser's built-in speech, for the brief window while Kokoro
1214
+ // downloads or on browsers where it can't run.
1215
+ function speakWithBrowser(text: string, onEnd: () => void) {
1216
+ if (typeof window === "undefined" || !window.speechSynthesis) {
1217
+ onEnd();
1218
+ return;
1219
+ }
1220
+ const utterance = new SpeechSynthesisUtterance(text);
1221
+ const voice = pickVoice(window.speechSynthesis.getVoices());
1222
+ if (voice) {
1223
+ utterance.voice = voice;
1224
+ utterance.lang = voice.lang;
1225
+ }
1226
+ utterance.rate = 0.96;
1227
+ utterance.pitch = 1.02;
1228
+ utterance.onend = onEnd;
1229
+ utterance.onerror = onEnd;
1230
+ window.speechSynthesis.cancel();
1231
+ window.speechSynthesis.speak(utterance);
1232
  }
1233
 
1234
  function TowerLogo() {
 
1252
  );
1253
  }
1254
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1255
  function priorityChips(parsed: ParsedPrompt) {
1256
  const meta: Record<DimensionKey, [string, string]> = {
1257
  affordability: ["Affordability", "#5f7faa"],
 
1328
  return `rgba(${red},${green},${blue},${value})`;
1329
  }
1330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1331
  function findNeighborhoodFact(
1332
  webResearch: AgentBackendRun["webResearch"] | null,
1333
  neighborhood: string,
src/data/neighborhoods.ts CHANGED
@@ -47,11 +47,6 @@ export interface Neighborhood {
47
  tradeoff: string;
48
  }
49
 
50
- export interface NeighborhoodFeatureProperties {
51
- id: string;
52
- name: string;
53
- }
54
-
55
  export const DIMENSIONS: DimensionKey[] = [
56
  "affordability",
57
  "safety",
@@ -442,48 +437,3 @@ export const neighborhoods: Neighborhood[] = [
442
  tradeoff: "Well over the rent cap; you pay for the convenience.",
443
  },
444
  ];
445
-
446
- export const unionStation: LngLat = [-79.3806, 43.6452];
447
-
448
- function mockBoundary(
449
- [lng, lat]: LngLat,
450
- radiusLng: number,
451
- radiusLat: number,
452
- seed: number,
453
- ): LngLat[] {
454
- const points: LngLat[] = [];
455
- for (let i = 0; i < 18; i += 1) {
456
- const angle = (Math.PI * 2 * i) / 18;
457
- const wobble = 1 + Math.sin(seed * 1.7 + i * 0.9) * 0.14;
458
- points.push([
459
- Number((lng + Math.cos(angle) * radiusLng * wobble).toFixed(6)),
460
- Number((lat + Math.sin(angle) * radiusLat * wobble).toFixed(6)),
461
- ]);
462
- }
463
- points.push(points[0]);
464
- return points;
465
- }
466
-
467
- export function buildNeighborhoodGeoJson(rows = neighborhoods) {
468
- return {
469
- type: "FeatureCollection",
470
- features: rows.map((neighborhood) => ({
471
- type: "Feature",
472
- properties: {
473
- id: neighborhood.id,
474
- name: neighborhood.name,
475
- } satisfies NeighborhoodFeatureProperties,
476
- geometry: {
477
- type: "Polygon",
478
- coordinates: [
479
- mockBoundary(
480
- neighborhood.center,
481
- neighborhood.radiusLng,
482
- neighborhood.radiusLat,
483
- neighborhood.seed,
484
- ),
485
- ],
486
- },
487
- })),
488
- };
489
- }
 
47
  tradeoff: string;
48
  }
49
 
 
 
 
 
 
50
  export const DIMENSIONS: DimensionKey[] = [
51
  "affordability",
52
  "safety",
 
437
  tradeoff: "Well over the rent cap; you pay for the convenience.",
438
  },
439
  ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/tts.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Kokoro-82M neural text-to-speech, run entirely in the browser via transformers.js (ONNX).
2
+ // 82M params — well under the hackathon's 32B limit — with no API key and no backend. The
3
+ // model (~80MB, quantised) downloads from the Hugging Face CDN on first use, then is cached.
4
+
5
+ const MODEL_ID = "onnx-community/Kokoro-82M-v1.0-ONNX";
6
+ const DEFAULT_VOICE = "af_heart"; // warm, natural American female
7
+
8
+ // kokoro-js has no bundled types; keep the surface we use minimal.
9
+ type KokoroModel = {
10
+ generate: (text: string, options: { voice: string }) => Promise<{ toBlob: () => Blob }>;
11
+ };
12
+
13
+ let modelPromise: Promise<KokoroModel> | null = null;
14
+
15
+ async function loadModel(): Promise<KokoroModel> {
16
+ if (!modelPromise) {
17
+ modelPromise = (async () => {
18
+ const { KokoroTTS } = await import("kokoro-js");
19
+ return (await KokoroTTS.from_pretrained(MODEL_ID, {
20
+ dtype: "q8",
21
+ device: "wasm",
22
+ })) as unknown as KokoroModel;
23
+ })().catch((error) => {
24
+ modelPromise = null; // allow a retry on next click
25
+ throw error;
26
+ });
27
+ }
28
+ return modelPromise;
29
+ }
30
+
31
+ // Warm the model in the background so the first Play click is fast.
32
+ export function preloadKokoro(): void {
33
+ void loadModel().catch(() => {});
34
+ }
35
+
36
+ // Synthesize text and return an object-URL for an audio blob. Caller plays + revokes it.
37
+ export async function synthesizeKokoro(text: string, voice: string = DEFAULT_VOICE): Promise<string> {
38
+ const model = await loadModel();
39
+ const audio = await model.generate(text, { voice });
40
+ return URL.createObjectURL(audio.toBlob());
41
+ }
src/styles.css CHANGED
@@ -260,6 +260,14 @@ textarea:focus-visible {
260
  50% { box-shadow: 0 0 0 5px rgba(48, 75, 120, 0); }
261
  }
262
 
 
 
 
 
 
 
 
 
263
  .map-scan-fx {
264
  position: absolute;
265
  inset: 0;
 
260
  50% { box-shadow: 0 0 0 5px rgba(48, 75, 120, 0); }
261
  }
262
 
263
+ .secondary-action .spin {
264
+ animation: tts-spin 0.9s linear infinite;
265
+ }
266
+
267
+ @keyframes tts-spin {
268
+ to { transform: rotate(360deg); }
269
+ }
270
+
271
  .map-scan-fx {
272
  position: absolute;
273
  inset: 0;