@the-inclusionist/engine 7.0.1 → 8.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +5 -3
  2. package/app/css/style.css +22 -5
  3. package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
  4. package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
  5. package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
  6. package/app/public/vendor/fonts/playwrite-ar.woff2 +0 -0
  7. package/app/public/vendor/fonts/playwrite-br.woff2 +0 -0
  8. package/app/public/vendor/fonts/playwrite-ca.woff2 +0 -0
  9. package/app/public/vendor/fonts/playwrite-cl.woff2 +0 -0
  10. package/app/public/vendor/fonts/playwrite-co.woff2 +0 -0
  11. package/app/public/vendor/fonts/playwrite-mx.woff2 +0 -0
  12. package/app/public/vendor/fonts/playwrite-us-modern.woff2 +0 -0
  13. package/app/public/vendor/fonts/playwrite-us-trad.woff2 +0 -0
  14. package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
  15. package/app/public/vendor/fonts.css +64 -2
  16. package/dist-pkg/boot/create-game.d.ts +107 -4
  17. package/dist-pkg/boot/create-game.js +337 -18
  18. package/dist-pkg/core/constants.d.ts +0 -28
  19. package/dist-pkg/core/constants.js +34 -17
  20. package/dist-pkg/core/contract.d.ts +99 -0
  21. package/dist-pkg/core/contract.js +78 -0
  22. package/dist-pkg/core/entity.d.ts +47 -4
  23. package/dist-pkg/core/layers.d.ts +18 -0
  24. package/dist-pkg/core/layers.js +18 -0
  25. package/dist-pkg/core/rng.js +14 -4
  26. package/dist-pkg/core/route.d.ts +42 -0
  27. package/dist-pkg/core/route.js +158 -0
  28. package/dist-pkg/core/state.d.ts +10 -1
  29. package/dist-pkg/core/state.js +13 -0
  30. package/dist-pkg/educational/adaptive-engine.d.ts +65 -0
  31. package/dist-pkg/educational/adaptive-engine.js +117 -0
  32. package/dist-pkg/educational/segment-bar.d.ts +96 -0
  33. package/dist-pkg/educational/segment-bar.js +89 -0
  34. package/dist-pkg/i18n/en.js +44 -0
  35. package/dist-pkg/i18n/es.js +44 -0
  36. package/dist-pkg/i18n/pt.js +60 -0
  37. package/dist-pkg/input/default-bindings.d.ts +40 -0
  38. package/dist-pkg/input/default-bindings.js +151 -9
  39. package/dist-pkg/input/gamepad.d.ts +33 -12
  40. package/dist-pkg/input/gamepad.js +103 -16
  41. package/dist-pkg/input/keyboard-runtime.d.ts +6 -8
  42. package/dist-pkg/input/keyboard-runtime.js +24 -5
  43. package/dist-pkg/input/keyboard.d.ts +42 -0
  44. package/dist-pkg/input/keyboard.js +87 -17
  45. package/dist-pkg/input/keydown.d.ts +40 -2
  46. package/dist-pkg/input/keydown.js +29 -6
  47. package/dist-pkg/input/latch-edge.d.ts +28 -0
  48. package/dist-pkg/input/latch-edge.js +45 -0
  49. package/dist-pkg/input/latch-scope.d.ts +70 -0
  50. package/dist-pkg/input/latch-scope.js +110 -0
  51. package/dist-pkg/input/latch-store.d.ts +45 -0
  52. package/dist-pkg/input/latch-store.js +74 -0
  53. package/dist-pkg/input/latch-sync.d.ts +47 -0
  54. package/dist-pkg/input/latch-sync.js +43 -0
  55. package/dist-pkg/input/origem-sintetica.d.ts +44 -0
  56. package/dist-pkg/input/origem-sintetica.js +60 -0
  57. package/dist-pkg/input/pad-defaults.d.ts +22 -0
  58. package/dist-pkg/input/pad-defaults.js +34 -0
  59. package/dist-pkg/input/pointer.d.ts +61 -0
  60. package/dist-pkg/input/pointer.js +71 -0
  61. package/dist-pkg/input/state.d.ts +86 -1
  62. package/dist-pkg/input/state.js +128 -1
  63. package/dist-pkg/input/touch-bindings.d.ts +21 -2
  64. package/dist-pkg/input/touch-bindings.js +15 -3
  65. package/dist-pkg/input/touch.js +9 -1
  66. package/dist-pkg/input/transporte-em-uso.d.ts +101 -0
  67. package/dist-pkg/input/transporte-em-uso.js +130 -0
  68. package/dist-pkg/input/transports.d.ts +88 -2
  69. package/dist-pkg/input/transports.js +60 -5
  70. package/dist-pkg/input/vocabulary-migration.d.ts +18 -7
  71. package/dist-pkg/platform/audio-earcons.d.ts +29 -2
  72. package/dist-pkg/platform/audio-earcons.js +10 -0
  73. package/dist-pkg/platform/audio-nav.d.ts +1 -1
  74. package/dist-pkg/platform/audio-sonar.d.ts +149 -9
  75. package/dist-pkg/platform/audio-sonar.js +232 -21
  76. package/dist-pkg/platform/guide-intensity.d.ts +40 -0
  77. package/dist-pkg/platform/guide-intensity.js +73 -0
  78. package/dist-pkg/platform/pesados-catalogo.d.ts +14 -0
  79. package/dist-pkg/platform/pesados-catalogo.js +177 -0
  80. package/dist-pkg/platform/pesados.d.ts +31 -0
  81. package/dist-pkg/platform/pesados.js +75 -0
  82. package/dist-pkg/platform/storage.d.ts +30 -0
  83. package/dist-pkg/platform/storage.js +35 -0
  84. package/dist-pkg/platform/tts.js +16 -1
  85. package/dist-pkg/platform/voice-plan.d.ts +109 -0
  86. package/dist-pkg/platform/voice-plan.js +156 -0
  87. package/dist-pkg/platform/vozes-prontas.d.ts +28 -0
  88. package/dist-pkg/platform/vozes-prontas.js +61 -0
  89. package/dist-pkg/render/draw.d.ts +1 -1
  90. package/dist-pkg/render/draw.js +15 -5
  91. package/dist-pkg/render/viz-axes.d.ts +17 -0
  92. package/dist-pkg/render/viz-axes.js +19 -0
  93. package/dist-pkg/render/viz-setters.d.ts +34 -2
  94. package/dist-pkg/render/viz-setters.js +189 -33
  95. package/dist-pkg/render/wheelchair-sprites.d.ts +11 -3
  96. package/dist-pkg/render/wheelchair-sprites.js +10 -4
  97. package/dist-pkg/ui/dom.js +20 -2
  98. package/dist-pkg/ui/fonts.d.ts +71 -1
  99. package/dist-pkg/ui/fonts.js +111 -8
  100. package/dist-pkg/ui/latch-refusal.d.ts +32 -0
  101. package/dist-pkg/ui/latch-refusal.js +60 -0
  102. package/dist-pkg/ui/layout.d.ts +32 -0
  103. package/dist-pkg/ui/layout.js +64 -1
  104. package/dist-pkg/ui/motion-scene.d.ts +41 -0
  105. package/dist-pkg/ui/motion-scene.js +76 -0
  106. package/dist-pkg/ui/panel-shell.d.ts +53 -0
  107. package/dist-pkg/ui/panel-shell.js +103 -0
  108. package/dist-pkg/ui/pause-icons.d.ts +168 -20
  109. package/dist-pkg/ui/pause-icons.js +315 -45
  110. package/dist-pkg/ui/reach-notice.js +8 -0
  111. package/dist-pkg/ui/settings-audio.d.ts +14 -1
  112. package/dist-pkg/ui/settings-audio.js +43 -3
  113. package/dist-pkg/ui/settings-controls.d.ts +66 -6
  114. package/dist-pkg/ui/settings-controls.js +179 -17
  115. package/dist-pkg/ui/settings-empathy.d.ts +15 -0
  116. package/dist-pkg/ui/settings-empathy.js +2 -0
  117. package/dist-pkg/ui/settings-motion.d.ts +25 -19
  118. package/dist-pkg/ui/settings-motion.js +32 -19
  119. package/dist-pkg/ui/settings-motor.d.ts +101 -3
  120. package/dist-pkg/ui/settings-motor.js +136 -9
  121. package/dist-pkg/ui/settings-typo.d.ts +49 -5
  122. package/dist-pkg/ui/settings-typo.js +74 -23
  123. package/dist-pkg/ui/settings-visual.d.ts +21 -1
  124. package/dist-pkg/ui/settings-visual.js +48 -4
  125. package/dist-pkg/ui/shell.d.ts +13 -3
  126. package/dist-pkg/ui/shell.js +3 -4
  127. package/dist-pkg/ui/simulation-refusal.d.ts +32 -0
  128. package/dist-pkg/ui/simulation-refusal.js +57 -0
  129. package/dist-pkg/ui/visual-axes-panel.d.ts +48 -0
  130. package/dist-pkg/ui/visual-axes-panel.js +95 -0
  131. package/dist-pkg/ui/webcam.js +6 -1
  132. package/docs/CREDITS.md +22 -1
  133. package/docs/LICENSES.md +170 -132
  134. package/package.json +25 -4
  135. package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
  136. package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
@@ -0,0 +1,177 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // platform/pesados-catalogo.ts — O QUE É PESADO, DE ONDE VEM, E QUANTO PESA.
3
+ //
4
+ // 📌 SEPARADO DO BUSCADOR de propósito: a lista é DADO e muda por decisão registada; o buscador é regra e
5
+ // muda por defeito encontrado. Juntos, cada correcção de uma URL mexeria no ficheiro que decide a ordem das
6
+ // descargas, e cada correcção da ordem mexeria na lista que um registo governa.
7
+ //
8
+ // ========================= 🔴 A PRIMEIRA VERSÃO DESTE FICHEIRO COMETEU O DEFEITO QUE ELE SERVE =========================
9
+ // Ela escrevia o host à mão (`const HF = 'https://huggingface.co/…'`), a lista das quatro vozes outra vez, e
10
+ // uma segunda derivação do caminho `idioma/locale/nome/qualidade/…`. As três coisas JÁ EXISTEM no
11
+ // `platform/voice-plan.ts`, que o ADR-0114 designou como o sítio ÚNICO onde o host é nomeado — e cujo próprio
12
+ // comentário nomeia as três vezes que este repositório pagou por tabelas duplicadas (o `DomQuery`, os rótulos
13
+ // de movimento reduzido, as chaves de armazenamento).
14
+ //
15
+ // 📏 QUEM APANHOU FOI O INVENTÁRIO, e não pela duplicação: o `nada-vem-de-fora` recusou uma URL nova sem razão
16
+ // escrita. A URL era só a PROVA; declará-la teria feito o gate ficar verde com a duplicação lá dentro. ⚠️ É a
17
+ // razão de a saída ser APAGAR a cópia e não desculpá-la — declarar duas vezes o mesmo endereço é precisamente
18
+ // a cláusula que o `nada-de-cdn-a-mao` conta com um número.
19
+ import { VOZES_NEURAIS, urlDoModelo, urlDaConfig } from './voice-plan.js';
20
+ /** O nome da Cache Storage. Versionado: mudar o conteúdo do catálogo não deve servir bytes velhos. */
21
+ export const CACHE_PESADOS = 'incl-pesados-v1';
22
+ /**
23
+ * O PESO DE CADA VOZ, MEDIDO EM 2026-09-09 e não estimado — `Content-Length` do modelo e corpo da configuração.
24
+ *
25
+ * 📌 O PESO MORA AQUI E O ENDEREÇO MORA NO `voice-plan`, e a divisão não é arrumação: o `voice-plan` responde
26
+ * «que vozes a engine garante e onde estão», que é decisão do ADR-0110; isto responde «quanto custa descê-las
27
+ * hoje», que é uma medição com data e que muda quando o fornecedor recomprime um ficheiro.
28
+ *
29
+ * ⚠️ UMA VOZ NOVA NO `voice-plan` SEM MEDIÇÃO AQUI FICA SEM `bytes`, e o aviso de «faltam N MB» passaria a
30
+ * sub-reportar em silêncio. É por isso que o gate exige `bytes > 0` em toda entrada de voz COM url: o buraco
31
+ * é pequeno e mudo, que é a forma de defeito que este repositório persegue.
32
+ */
33
+ const PESO_MEDIDO = Object.freeze({
34
+ 'pt_BR-faber-medium': { modelo: 63_201_294, config: 4_855 },
35
+ 'en_US-ryan-medium': { modelo: 63_201_294, config: 4_883 },
36
+ 'en_US-amy-medium': { modelo: 63_201_294, config: 4_882 },
37
+ 'es_MX-claude-high': { modelo: 63_122_309, config: 4_963 },
38
+ });
39
+ /**
40
+ * ⚠️ O `urlDoModelo` devolve `null` para um identificador que não se deixe ler, e a razão viaja com a entrada
41
+ * em vez de a entrada desaparecer. Uma URL inventada dá 404 na escola; um `null` COM razão dá para reportar
42
+ * antes de sair de casa, que é a regra que o `voice-plan` já escreve na própria função.
43
+ */
44
+ const RAZAO_ID_TORTO = 'o identificador da voz não tem a forma `locale-nome-qualidade`, então o caminho no '
45
+ + 'fornecedor não se deixa derivar. Corrija o identificador no `platform/voice-plan.ts`.';
46
+ /**
47
+ * AS DUAS ENTRADAS DE UMA VOZ. ⚠️ SÃO DUAS E NÃO UMA: o `.onnx` é o modelo e o `.onnx.json` é a configuração,
48
+ * e o piper recusa-se a falar sem a segunda. Um catálogo que só trouxesse a primeira produziria uma voz
49
+ * «baixada» que não fala — que é pior do que uma voz em falta, porque a primeira parece resolvida.
50
+ */
51
+ function entradasDaVoz(v) {
52
+ const peso = PESO_MEDIDO[v.voice];
53
+ return [
54
+ { id: `voz:${v.voice}`, url: urlDoModelo(v), bytes: peso?.modelo, porQueNaoTemFonte: RAZAO_ID_TORTO },
55
+ { id: `voz:${v.voice}:cfg`, url: urlDaConfig(v), bytes: peso?.config, porQueNaoTemFonte: RAZAO_ID_TORTO },
56
+ ];
57
+ }
58
+ /**
59
+ * O RUNTIME DE VISÃO — **MediaPipe**, decidido pelo Dev em 2026-09-09 (ADR-0124): «piper-tts, mediapipe
60
+ * (webgazer não), e LPCP: devem acompanhar a engine».
61
+ *
62
+ * 🔴 ESTA ENTRADA DIZIA «a #129 ainda não escolheu o fornecedor» DEPOIS DE ELE TER ESCOLHIDO, e a linha
63
+ * sobreviveu ao registo que a contradizia. Não era só trabalho em falta: era uma afirmação FALSA a dirigir
64
+ * quem a lesse para uma issue já fechada. O Dev teve de perguntar três vezes.
65
+ *
66
+ * 📏 MEDIDO EM 2026-09-09, como as vozes e no mesmo minuto: os três ficheiros respondem 200 em jsDelivr com
67
+ * `Access-Control-Allow-Origin: *`, na versão FIXADA — 155 439 + 323 377 + 11 756 954 bytes.
68
+ *
69
+ * ⚠️ CDN FIXADA É PERMITIDA E O ADR-0116 DIZ PORQUÊ: o que o pilar 8 proíbe é depender da rede DEPOIS do
70
+ * primeiro dia. Isto desce na INSTALAÇÃO, com o resto — e é a diferença inteira para o WebGazer, que busca
71
+ * quando a criança liga o controle por olhar, logo a máquina que nunca o ligou fica sem ele para sempre.
72
+ * 📌 A versão vai na URL, que é o que o `check:precache` exige de qualquer entrada externa: bytes diferentes
73
+ * chegam por endereço diferente, e uma entrada fixada nunca congela.
74
+ *
75
+ * 🎯 SÃO OS TRÊS FICHEIROS E NÃO SÓ O `.wasm`: o `vision_bundle.mjs` é quem o carrega e o
76
+ * `vision_wasm_internal.js` é a cola do Emscripten. Baixar o wasm sozinho é a mesma armadilha do `.onnx` sem
77
+ * o `.onnx.json` — uma coisa «baixada» que não corre.
78
+ *
79
+ * ⬜ O que continua por fazer é a FIAÇÃO (issue #11): estes bytes descem e ainda ninguém os lê. O
80
+ * `tests/o-que-desce-tem-quem-leia.node.test.js` é onde essa dívida está declarada.
81
+ */
82
+ const MP = 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@1.0.1';
83
+ const MP_MODELOS = 'https://storage.googleapis.com/mediapipe-models';
84
+ /**
85
+ * 🔴 A PRIMEIRA VERSÃO DESTA LISTA TRAZIA O RUNTIME E NENHUM MODELO, e o Dev apanhou-o ao perguntar o que
86
+ * tinha ficado de fora. 11,7 MB de WebAssembly sem um `.task` não reconhecem coisa nenhuma — é o `.onnx` sem
87
+ * o `.onnx.json` outra vez, no ficheiro que escreve essa lição doze linhas acima.
88
+ *
89
+ * 📏 MEDIDOS EM 2026-09-09, todos 200 com CORS aberto. `float16` e não `float32`: metade do peso, e a precisão
90
+ * que se perde é irrelevante para dizer onde está um íris num ecrã de 320×180.
91
+ */
92
+ const MEDIAPIPE = Object.freeze([
93
+ { id: 'visao:runtime', url: `${MP}/vision_bundle.mjs`, bytes: 155_439 },
94
+ { id: 'visao:runtime:cola', url: `${MP}/wasm/vision_wasm_internal.js`, bytes: 323_377 },
95
+ { id: 'visao:runtime:wasm', url: `${MP}/wasm/vision_wasm_internal.wasm`, bytes: 11_756_954 },
96
+ { id: 'visao:modelo:rosto', url: `${MP_MODELOS}/face_landmarker/face_landmarker/float16/1/face_landmarker.task`, bytes: 3_758_596 },
97
+ { id: 'visao:modelo:gestos', url: `${MP_MODELOS}/gesture_recognizer/gesture_recognizer/float16/1/gesture_recognizer.task`, bytes: 8_373_440 },
98
+ { id: 'visao:modelo:maos', url: `${MP_MODELOS}/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task`, bytes: 7_819_105 },
99
+ ]);
100
+ /**
101
+ * O WEBGAZER — **volta em 2026-09-09, ao lado do MediaPipe** (ADR-0132): «Traga o WebGazer de volta. Vamos
102
+ * usar ambos.»
103
+ *
104
+ * 🎯 ELES NÃO SE SOBREPÕEM ONDE IMPORTA, e é essa medição que produziu a decisão: o MediaPipe diz ONDE O ÍRIS
105
+ * ESTÁ — `FACE_LANDMARKS_LEFT_IRIS` são conjuntos de conexões sobre marcos —, e o WebGazer diz PARA ONDE A
106
+ * CRIANÇA OLHA NO ECRÃ, que é um modelo de regressão com calibração. Nenhuma das quinze tarefas do
107
+ * `tasks-vision` faz a segunda.
108
+ *
109
+ * ⚠️ E O DEFEITO DELE NUNCA FOI O FORNECEDOR: era ser PREGUIÇOSO. Buscado quando a criança liga o controle
110
+ * por olhar, a máquina que nunca o ligou fica sem ele, e numa escola sem rede não acontece nada — sem erro e
111
+ * sem explicação. Aqui desce na INSTALAÇÃO com tudo o resto, e o defeito desaparece com a capacidade intacta.
112
+ * 📌 Fica a dívida que o ADR-0132 nomeia: o `<script src>` do `ui/webcam.ts` tem de sair no mesmo commit em
113
+ * que a fiação o ler daqui, senão passam a existir dois caminhos para o mesmo ficheiro.
114
+ */
115
+ const WEBGAZER = Object.freeze([
116
+ { id: 'visao:olhar', url: 'https://webgazer.cs.brown.edu/webgazer.js', bytes: 1_895_169 },
117
+ ]);
118
+ /**
119
+ * O RUNTIME DE VOZ — **piper**, decidido no ADR-0127, e o Dev disse para que serve: «PiperTTS é o que será
120
+ * usado para ler para o usuário. Precisa ser carregado com a engine».
121
+ *
122
+ * 🔴 ATÉ AQUI ELE SÓ CHEGAVA PELA PORTA DO CARTUCHO (ADR-0094), e três dos seis jogos não a declaram — nesses,
123
+ * a engine descarregava 241 MB de modelos e não tinha com que os tocar. Descer os modelos sem o motor é a
124
+ * mesma armadilha do `.onnx` sem o `.onnx.json`, um nível acima.
125
+ *
126
+ * 📏 MEDIDO EM 2026-09-09 em jsDelivr, versões FIXADAS, todos 200 com `Access-Control-Allow-Origin: *`.
127
+ * ⚠️ SÃO CINCO FICHEIROS E NÃO UM: o `piper-tts-web.js` é só a entrada (23 KB) — os dois pedaços com hash no
128
+ * nome são o corpo e a tabela de vozes, e o `onnxruntime-web` é quem corre o modelo. Trazer só a entrada dá
129
+ * um módulo que importa o que não está lá.
130
+ * 📌 `ort-wasm-simd-threaded` e não o `jsep`: o jsep é o caminho WebGPU e pesa 21,7 MB contra 11,2 — e o
131
+ * hardware do pilar 1 não é onde a WebGPU se ganha.
132
+ */
133
+ const PP = 'https://cdn.jsdelivr.net/npm/@mintplex-labs/piper-tts-web@1.0.5/dist';
134
+ const ORT = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.1/dist';
135
+ const PIPER = Object.freeze([
136
+ { id: 'voz:runtime', url: `${PP}/piper-tts-web.js`, bytes: 23_646 },
137
+ { id: 'voz:runtime:corpo', url: `${PP}/piper-o91UDS6e.js`, bytes: 158_217 },
138
+ { id: 'voz:runtime:tabela', url: `${PP}/voices_static-D_OtJDHM.js`, bytes: 147_377 },
139
+ { id: 'voz:runtime:ort', url: `${ORT}/ort.min.js`, bytes: 446_284 },
140
+ { id: 'voz:runtime:ort-wasm', url: `${ORT}/ort-wasm-simd-threaded.wasm`, bytes: 11_246_032 },
141
+ ]);
142
+ export const PESADOS = Object.freeze([
143
+ ...VOZES_NEURAIS.flatMap(entradasDaVoz),
144
+ ...PIPER,
145
+ /*
146
+ * 🔴 O RUNTIME DE VISÃO — decidido e SEM FONTE, e a ausência é medida.
147
+ *
148
+ * A issue #11 diz «MediaPipe» e o `ui/webcam.ts` faz WebGazer, buscado de `webgazer.cs.brown.edu` por um
149
+ * `<script src>` com preguiça no PRIMEIRO USO — sem SRI, sem `crossorigin`, e a falhar em silêncio numa
150
+ * escola sem rede. `git grep -i mediapipe` em `app/js` devolve ZERO (ADR-0119).
151
+ *
152
+ * ⚠️ NÃO PONHO AQUI A URL DO WEBGAZER. Trocar o fornecedor é decisão da #129, e escrevê-la aqui seria
153
+ * decidi-la de lado — a mesma coisa que o ADR-0119 apanhou: um subsistema que a engine DECLARA possuir e
154
+ * que na prática é outra coisa.
155
+ */
156
+ ...MEDIAPIPE,
157
+ ...WEBGAZER,
158
+ /*
159
+ * 🔴 O ACERVO DE ARTE — a quarta coisa pesada do ADR-0119, e a única SEM FONTE. Medido: `art/` tem DOIS
160
+ * ficheiros — um README e um `ATTRIBUTION.csv` de 40 bytes, só o cabeçalho. Zero arte.
161
+ *
162
+ * ⚠️ ATÉ 2026-09-09 ESTA LINHA CULPAVA A COISA ERRADA. Dizia que a quarentena estava vazia e que a arte
163
+ * era CC BY-SA 3.0 com autoria por recurso — descrevendo o Liberated Pixel Cup, que o ADR-0133 recusou:
164
+ * os dois braços dele são share-alike ou GPL, e nenhum está na lista fechada de quatro licenças.
165
+ *
166
+ * 📌 A razão de continuar sem fonte MUDOU e é mais simples: não há acervo escolhido. A arte entra sob
167
+ * CC0, CC BY 3.0, CC BY 4.0 ou OGA-BY, com uma linha de livro por recurso e a URL da origem — e nada
168
+ * disso é uma URL única que um buscador possa pedir. A fonte desta linha é o dia em que houver acervo.
169
+ */
170
+ {
171
+ id: 'arte:acervo',
172
+ url: null,
173
+ porQueNaoTemFonte: 'não há acervo escolhido: `art/` tem só README e cabeçalho do CSV. O ADR-0133 fechou '
174
+ + 'a lista em CC0, CC BY 3.0/4.0 e OGA-BY, e a arte entra recurso a recurso com autoria e URL de '
175
+ + 'origem — não por uma URL solta que este buscador possa pedir.',
176
+ },
177
+ ]);
@@ -0,0 +1,31 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import { CACHE_PESADOS, PESADOS, type Pesado } from './pesados-catalogo.js';
3
+ export { CACHE_PESADOS, PESADOS };
4
+ export type { Pesado };
5
+ /** O que aconteceu com cada entrada, para quem chama poder dizê-lo a uma pessoa. */
6
+ export interface RelatorioPesado {
7
+ readonly id: string;
8
+ readonly estado: 'ja-tinha' | 'baixado' | 'falhou' | 'sem-fonte';
9
+ readonly bytes?: number;
10
+ readonly erro?: string;
11
+ }
12
+ export interface OpcoesDosPesados {
13
+ /** `caches` do navegador. Injectado para o gate não precisar de um. */
14
+ readonly cacheStorage?: CacheStorage;
15
+ /** `fetch`. Injectado pela mesma razão. */
16
+ readonly buscar?: typeof fetch;
17
+ /** Chamado a cada entrada resolvida — é o que deixa a interface dizer o que está a acontecer. */
18
+ readonly aoProgredir?: (r: RelatorioPesado) => void;
19
+ /** Só estas ids, se dado. Serve ao consumidor que quer as vozes e não o resto. */
20
+ readonly apenas?: readonly string[];
21
+ }
22
+ /**
23
+ * BAIXA O QUE FALTA, UM DE CADA VEZ, E DEVOLVE O QUE ACONTECEU COM CADA UM.
24
+ *
25
+ * ⚠️ AS ENTRADAS SEM `url` NÃO SÃO SALTADAS EM SILÊNCIO — devolvem `sem-fonte`. É a diferença entre «este
26
+ * subsistema ainda não tem de onde vir» e «este subsistema está tratado», e é exactamente a distinção que o
27
+ * ADR-0119 mediu em falta: a engine PROMETIA quatro coisas e entregava uma, sem nada a dizê-lo.
28
+ */
29
+ export declare function baixarPesados(opcoes?: OpcoesDosPesados): Promise<RelatorioPesado[]>;
30
+ /** O peso do que ainda falta, em bytes — para um aviso poder dizer «faltam 241 MB» antes de começar. */
31
+ export declare function pesoPorBaixar(relatorio: readonly RelatorioPesado[]): number;
@@ -0,0 +1,75 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // platform/pesados.ts — AS COISAS PESADAS, BAIXADAS NO PRIMEIRO CARREGAMENTO (ADR-0110, ADR-0116, ADR-0119).
3
+ //
4
+ // ========================= O QUE ISTO É =========================
5
+ // O pilar 8 diz «PWA no primeiro dia ONLINE, depois OFFLINE-FIRST», e o ADR-0116 tirou a contradição que
6
+ // travava isto: instalar já é um acto de rede, logo buscar na instalação não viola nada. O que faltava era
7
+ // alguém a buscar.
8
+ //
9
+ // 📏 MEDIDO EM 2026-09-09, e é o estado que este ficheiro existe para mudar: das quatro coisas pesadas que a
10
+ // engine promete (fontes · voz neural · runtime de visão · arte do LCP), só as FONTES viajavam de verdade.
11
+ // As vozes eram porta do cartucho, o runtime de visão era um `<script src>` de CDN buscado com preguiça no
12
+ // PRIMEIRO USO, e a arte do LCP não existe.
13
+ //
14
+ // ⚠️ E ISTO NÃO BLOQUEIA O JOGO. A criança joga enquanto os 241 MB descem; o que não pode acontecer é ela
15
+ // chegar ao segundo dia, sem rede, e descobrir que a voz nunca foi buscada.
16
+ //
17
+ // ========================= AS TRÊS REGRAS QUE A FORMA IMPÕE =========================
18
+ // 1. **UM DE CADA VEZ.** Quatro descargas de 60 MB em paralelo num link de escola disputam a mesma banda e
19
+ // nenhuma acaba primeiro — e o jogo, que precisa da rede para as próprias imagens, fica atrás delas.
20
+ // 2. **NUNCA LANÇA.** Uma falha de rede é REPORTADA e a lista continua. Um `throw` aqui derrubaria o
21
+ // arranque de um jogo por causa de um recurso que ele nem usa hoje.
22
+ // 3. **IDEMPOTENTE.** O que já está na Cache Storage não é buscado outra vez — é o que torna isto seguro de
23
+ // chamar em todo arranque em vez de só «no primeiro», que ninguém sabe detectar com honestidade.
24
+ import { CACHE_PESADOS, PESADOS } from './pesados-catalogo.js';
25
+ export { CACHE_PESADOS, PESADOS };
26
+ /**
27
+ * BAIXA O QUE FALTA, UM DE CADA VEZ, E DEVOLVE O QUE ACONTECEU COM CADA UM.
28
+ *
29
+ * ⚠️ AS ENTRADAS SEM `url` NÃO SÃO SALTADAS EM SILÊNCIO — devolvem `sem-fonte`. É a diferença entre «este
30
+ * subsistema ainda não tem de onde vir» e «este subsistema está tratado», e é exactamente a distinção que o
31
+ * ADR-0119 mediu em falta: a engine PROMETIA quatro coisas e entregava uma, sem nada a dizê-lo.
32
+ */
33
+ export async function baixarPesados(opcoes = {}) {
34
+ const cs = opcoes.cacheStorage ?? (typeof caches !== 'undefined' ? caches : undefined);
35
+ const buscar = opcoes.buscar ?? (typeof fetch !== 'undefined' ? fetch : undefined);
36
+ const alvos = opcoes.apenas
37
+ ? PESADOS.filter((p) => opcoes.apenas.includes(p.id))
38
+ : PESADOS;
39
+ const out = [];
40
+ const conta = (r) => { out.push(r); opcoes.aoProgredir?.(r); };
41
+ if (!cs || !buscar) {
42
+ for (const p of alvos)
43
+ conta({ id: p.id, estado: 'falhou', erro: 'sem Cache Storage ou sem fetch' });
44
+ return out;
45
+ }
46
+ const cache = await cs.open(CACHE_PESADOS);
47
+ for (const p of alvos) {
48
+ if (!p.url) {
49
+ conta({ id: p.id, estado: 'sem-fonte', erro: p.porQueNaoTemFonte });
50
+ continue;
51
+ }
52
+ try {
53
+ if (await cache.match(p.url)) {
54
+ conta({ id: p.id, estado: 'ja-tinha' });
55
+ continue;
56
+ }
57
+ const resp = await buscar(p.url);
58
+ if (!resp.ok) {
59
+ conta({ id: p.id, estado: 'falhou', erro: `HTTP ${resp.status}` });
60
+ continue;
61
+ }
62
+ await cache.put(p.url, resp.clone());
63
+ conta({ id: p.id, estado: 'baixado', bytes: p.bytes });
64
+ }
65
+ catch (e) {
66
+ conta({ id: p.id, estado: 'falhou', erro: e instanceof Error ? e.message : String(e) });
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ /** O peso do que ainda falta, em bytes — para um aviso poder dizer «faltam 241 MB» antes de começar. */
72
+ export function pesoPorBaixar(relatorio) {
73
+ const feitos = new Set(relatorio.filter((r) => r.estado === 'ja-tinha' || r.estado === 'baixado').map((r) => r.id));
74
+ return PESADOS.filter((p) => p.url && !feitos.has(p.id)).reduce((s, p) => s + (p.bytes ?? 0), 0);
75
+ }
@@ -62,6 +62,7 @@ export declare const KEYS: {
62
62
  lang: string;
63
63
  letterCase: string;
64
64
  captions: string;
65
+ tea: string;
65
66
  menuIndex: string;
66
67
  fontKey: string;
67
68
  padDesign: string;
@@ -74,9 +75,38 @@ export declare const KEYS: {
74
75
  padDpadMm: string;
75
76
  reducedMotion: string;
76
77
  toggleMoveLegacy: string;
78
+ /**
79
+ * @deprecated ⚠️ A CHAVE LEGADA do modo visual — UM valor, do tempo em que só cabia um (issue #104).
80
+ *
81
+ * Continua a ser LIDA, e é isso que impede a criança de perder o que já escolheu; continua a ser ESCRITA
82
+ * enquanto os controles ainda escreverem um valor de cada vez, porque um leitor antigo (o cartucho na
83
+ * versão publicada) faz `if (v && VIZ_BY_KEY[v])` e rejeitaria um JSON — escrever a forma nova AQUI
84
+ * apagaria o ajuste dela em silêncio, que é exactamente o defeito que a migração existe para não cometer.
85
+ */
77
86
  vizP: (i: number) => string;
87
+ /**
88
+ * O ESTADO VISUAL de dois eixos, em JSON (ADR-0076, issue #104).
89
+ *
90
+ * ⚠️ CHAVE NOVA AO LADO DA VELHA, e não a mesma chave com conteúdo novo. É o mesmo desenho que o campo
91
+ * `visual` usa ao lado do `viz`: as duas formas coexistem enquanto houver leitores das duas, cada um lê a
92
+ * que entende, e a velha só morre quando não sobrar quem a leia. `migrarVisual` aceita as duas, então o
93
+ * recuo — chave nova ausente, chave velha presente — devolve exactamente o que a criança escolheu.
94
+ */
95
+ visualP: (i: number) => string;
78
96
  sinkP: (i: number) => string;
79
97
  easyP: (i: number) => string;
98
+ /**
99
+ * ⚠️ AS DUAS DE BAIXO SÃO AS CHAVES LEGADAS desde 2026-09-08 (ADR-0104 §C, issue #114). Continuam a ser
100
+ * LIDAS — é o ajuste da criança, e a herança dele é o que a impede de o perder — e não voltam a ser
101
+ * escritas. O que se escreve é a chave COM TRANSPORTE, porque a alternância é do APARELHO e não da pessoa:
102
+ * ligá-la no controle de tela, onde ninguém segura um botão virtual com conforto, ligava-a também no
103
+ * teclado, onde segurar uma tecla é exactamente o que a criança sabe fazer.
104
+ *
105
+ * ⚠️ E A CHAVE NOVA NÃO MORA AQUI, de propósito. Ela é `chaveDaAlternancia`, em `input/latch-scope` — este
106
+ * ficheiro é módulo-FOLHA e `platform/` não importa de `input/`, que é a camada acima. Montá-la aqui
107
+ * exigiria ou uma aresta ao contrário ou uma segunda cópia do nome, e a segunda cópia é exactamente o que
108
+ * o comentário do bloco acima existe para impedir. O dono do nome é quem conhece a regra do transporte.
109
+ */
80
110
  toggleMoveP: (i: number) => string;
81
111
  toggleRunP: (i: number) => string;
82
112
  rmWalkP: (i: number) => string;
@@ -123,6 +123,12 @@ export const KEYS = {
123
123
  ttsEngine: 'incl_tts_engine', ttsVoice: 'incl_tts_voice', lang: 'incl_lang', // audiocat_{k}
124
124
  // comunicação / legendas (ADR-0028: todo menu persiste)
125
125
  letterCase: 'incl_lettercase', captions: 'incl_captions',
126
+ // ⚠️ O NÍVEL TEA (calmo / silencioso) PASSOU A PERSISTIR EM 2026-09-07, e antes não persistia: era um
127
+ // `let calmMode = 0` em `ui/pause-icons`, com o comentário «deliberately NOT persisted — verbatim: game.js
128
+ // never wrote it to storage». O «verbatim» é a chave — foi PRESERVADO na extração do monólito, não
129
+ // decidido. O custo era da criança que mais precisa dele: quem usa o modo silencioso voltava a pô-lo a
130
+ // cada sessão, e é para quem o barulho inesperado custa mais. O ADR-0028 diz que todo menu persiste.
131
+ tea: 'incl_tea',
126
132
  menuIndex: 'incl_menuindex', // "6 de 10" no fim do anuncio de item (ADR-0044, item 3)
127
133
  // tipografia / controles / toque
128
134
  fontKey: 'incl_font_k', padDesign: 'incl_paddesign', padDir: 'incl_paddir', touchmap: 'incl_touchmap',
@@ -133,9 +139,38 @@ export const KEYS = {
133
139
  reducedMotion: 'inclusionist.reducedmotion.v1', toggleMoveLegacy: 'inclusionist.togglemove',
134
140
  // POR JOGADOR — parametrizadas pelo indice da tela. Eram sufixos '_p'+i montados a mao em varios
135
141
  // pontos do game.js; virar funcao aqui e o que impede que um deles escreva num nome torto.
142
+ /**
143
+ * @deprecated ⚠️ A CHAVE LEGADA do modo visual — UM valor, do tempo em que só cabia um (issue #104).
144
+ *
145
+ * Continua a ser LIDA, e é isso que impede a criança de perder o que já escolheu; continua a ser ESCRITA
146
+ * enquanto os controles ainda escreverem um valor de cada vez, porque um leitor antigo (o cartucho na
147
+ * versão publicada) faz `if (v && VIZ_BY_KEY[v])` e rejeitaria um JSON — escrever a forma nova AQUI
148
+ * apagaria o ajuste dela em silêncio, que é exactamente o defeito que a migração existe para não cometer.
149
+ */
136
150
  vizP: (i) => 'incl_viz_p' + i,
151
+ /**
152
+ * O ESTADO VISUAL de dois eixos, em JSON (ADR-0076, issue #104).
153
+ *
154
+ * ⚠️ CHAVE NOVA AO LADO DA VELHA, e não a mesma chave com conteúdo novo. É o mesmo desenho que o campo
155
+ * `visual` usa ao lado do `viz`: as duas formas coexistem enquanto houver leitores das duas, cada um lê a
156
+ * que entende, e a velha só morre quando não sobrar quem a leia. `migrarVisual` aceita as duas, então o
157
+ * recuo — chave nova ausente, chave velha presente — devolve exactamente o que a criança escolheu.
158
+ */
159
+ visualP: (i) => 'incl_visual_p' + i,
137
160
  sinkP: (i) => 'incl_sink_p' + i,
138
161
  easyP: (i) => 'incl_easy_p' + i,
162
+ /**
163
+ * ⚠️ AS DUAS DE BAIXO SÃO AS CHAVES LEGADAS desde 2026-09-08 (ADR-0104 §C, issue #114). Continuam a ser
164
+ * LIDAS — é o ajuste da criança, e a herança dele é o que a impede de o perder — e não voltam a ser
165
+ * escritas. O que se escreve é a chave COM TRANSPORTE, porque a alternância é do APARELHO e não da pessoa:
166
+ * ligá-la no controle de tela, onde ninguém segura um botão virtual com conforto, ligava-a também no
167
+ * teclado, onde segurar uma tecla é exactamente o que a criança sabe fazer.
168
+ *
169
+ * ⚠️ E A CHAVE NOVA NÃO MORA AQUI, de propósito. Ela é `chaveDaAlternancia`, em `input/latch-scope` — este
170
+ * ficheiro é módulo-FOLHA e `platform/` não importa de `input/`, que é a camada acima. Montá-la aqui
171
+ * exigiria ou uma aresta ao contrário ou uma segunda cópia do nome, e a segunda cópia é exactamente o que
172
+ * o comentário do bloco acima existe para impedir. O dono do nome é quem conhece a regra do transporte.
173
+ */
139
174
  toggleMoveP: (i) => 'incl_togglemove_p' + i,
140
175
  toggleRunP: (i) => 'incl_togglerun_p' + i, // alternância do botão de CORRER (irmã da de movimento)
141
176
  rmWalkP: (i) => 'incl_rmWalk_p' + i,
@@ -10,7 +10,22 @@ import { t, bcp47 } from '../core/i18n.js';
10
10
  import { criarFalaInterrompivel } from './interruptible-speech.js';
11
11
  // ⚠️ MIGRAÇÃO PENDENTE (ADR-0022): a implementação neural de hoje é o @mintplex-labs/piper-tts-web e será SUBSTITUÍDA
12
12
  // por sherpa-onnx-wasm (loader universal, modelos VITS/Piper + Kokoro-multi-lang carregados de qq URL R2 em runtime via
13
- // FS.writeFile; lazy-fetch; eSpeak/Web Speech de fallback). @mintplex-labs foi descontinuado e só carrega 2 vozes pt-BR.
13
+ // FS.writeFile; lazy-fetch; eSpeak/Web Speech de fallback).
14
+ // 🔴 O MOTIVO ESCRITO AQUI ERA «@mintplex-labs foi descontinuado e só carrega 2 vozes pt-BR», E AS DUAS METADES
15
+ // FORAM MEDIDAS FALSAS EM 2026-09-09 — não pela leitura de um registo, que é como elas se propagaram por seis
16
+ // sítios, mas pelo registo npm e pelo pacote instalado:
17
+ // · DESCONTINUADO: nenhuma versão tem campo `deprecated`, e a 1.0.5 foi publicada em 2026-08-11 — depois de
18
+ // o ADR-0022 (2026-07-06) a dar por morta. A frase é de Julho e está atribuída ao Dev; o registo não a nega
19
+ // no passado, nega-a HOJE.
20
+ // · DUAS VOZES pt-BR: o `VoiceId` da 1.0.4 instalada traz 118 vozes, e entre elas as QUATRO exactas do
21
+ // ADR-0110 — `pt_BR-faber-medium`, `en_US-ryan-medium`, `en_US-amy-medium`, `es_MX-claude-high`.
22
+ // ⚠️ O QUE CONTINUA VERDADEIRO, e é mais afiado do que o motivo velho: o bundle prende `HF_BASE` a
23
+ // `huggingface.co/diffusionstudio/piper-voices` COM UMA GUARDA (`if (!url.match("https://huggingface.co")) return`),
24
+ // logo nenhum espelho de escola é alcançável por ele; e ele busca o PRÓPRIO runtime de cdnjs/jsDelivr por
25
+ // omissão, que é o que o ADR-0114 retira. A montante, o Piper mudou para `OHF-Voice/piper1-gpl` e a Open Home
26
+ // Foundation procura mantenedores — o risco existe, mas está noutro sítio.
27
+ // 📌 A ESCOLHA CONTINUA A SER A #129, e este comentário não a toma: descreve o que foi medido para que o
28
+ // próximo leitor não herde o motivo velho como se fosse medição.
14
29
  // ⚠️ E DESDE O ADR-0094 ESTE MÓDULO NÃO NOMEIA FORNECEDOR NENHUM — quem o nomeia é o JOGO, por `ctx.carregarVozNeural`.
15
30
  // O nome estava aqui num `import()` e o pacote em `devDependencies`, o que publicou uma engine que não compilava
16
31
  // (ADR-0093); pô-lo em `dependencies` consertava o build e obrigava todo consumidor a 135,4 MB de `onnxruntime-web`,
@@ -0,0 +1,109 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** Uma voz do catálogo. `voice` é o identificador no fornecedor; `engine` é o motor que a lê. */
3
+ export interface VozNeural {
4
+ readonly locale: string;
5
+ readonly engine: string;
6
+ readonly voice: string;
7
+ }
8
+ /**
9
+ * AS QUATRO QUE A ENGINE GARANTE (ADR-0110), decididas pelo Dev em 2026-09-08.
10
+ *
11
+ * ⚠️ TODAS `vits-piper` E NENHUMA `ncnn`, que o ADR-0065 §5 já proibia: `ncnn` é outro MOTOR DE INFERÊNCIA —
12
+ * modelos `.param`/`.bin` e não `.onnx` — e este projeto corre sherpa-onnx. Dois dos links que chegaram em
13
+ * 2026-09-08 apontavam para os espelhos `ncnn`, e é por isso que a proibição tem gate e não só um comentário.
14
+ * ⚠️ E NENHUMA `int8`, pela razão MEDIDA do mesmo §5: no backend WASM o int8 corre ~3× MAIS LENTO que o fp32
15
+ * (RTF 5,6 contra 2,5, do log do próprio Dev) e produz saída incorrecta ou muda. Quantizar não encurta a
16
+ * espera aqui — alonga-a.
17
+ */
18
+ export declare const VOZES_NEURAIS: readonly VozNeural[];
19
+ /**
20
+ * DE ONDE VÊM OS MODELOS (ADR-0114), num sítio só.
21
+ *
22
+ * 📏 MEDIDO EM 2026-09-08, e não escolhido: o `piper.ttstool.com` — que o Dev nomeou ao perguntar — serve o
23
+ * próprio runtime da própria origem e busca os modelos aqui. As quatro vozes deste catálogo respondem 200
24
+ * neste endereço, com `Access-Control-Allow-Origin: *`, logo um PWA pode buscá-las de outra origem.
25
+ *
26
+ * ⚠️ UM SÍTIO SÓ É A METADE QUE O REGISTO PEDE. Um endereço repetido no ponto de uso é como o CDN do
27
+ * WebGazer chegou ao `ui/webcam` — escrito à mão, sem política, e sem ninguém a poder mudá-lo de uma vez.
28
+ *
29
+ * ========================= 🔴 MUDOU DE ESPELHO EM 2026-09-09, E A REGRA É «BUSCAR DE ONDE O LEITOR LÊ» ====
30
+ * Era `rhasspy/piper-voices`, medido a 08/09 como o espelho que o `piper.ttstool.com` usa. Depois de o
31
+ * `platform/pesados` passar a descer os modelos no primeiro carregamento, mediu-se o outro lado — o
32
+ * `@mintplex-labs/piper-tts-web@1.0.4` INSTALADO, lido do bundle e não do README — e ele busca em
33
+ * `diffusionstudio/piper-voices`, com uma guarda que recusa qualquer URL fora de `huggingface.co`.
34
+ *
35
+ * 🎯 ENDEREÇOS DIFERENTES SIGNIFICAM CACHE DIFERENTE: a Cache Storage é indexada pela URL do pedido, logo os
36
+ * 241 MB que desciam no primeiro dia NÃO ERAM LIDOS POR NINGUÉM, e a biblioteca descarregava tudo outra vez
37
+ * no dia em que a voz fosse pedida. Até 482 MB num link de escola para uma voz.
38
+ *
39
+ * 📏 E OS DOIS ESPELHOS SERVEM OS MESMOS BYTES — 63 201 294 para o `pt_BR-faber-medium`, caminhos idênticos,
40
+ * `Access-Control-Allow-Origin: *` nos dois. Medido nos dois no mesmo minuto, o que torna esta troca uma
41
+ * correcção de facto e não uma preferência entre fornecedores.
42
+ *
43
+ * ⚠️ A CLÁUSULA DO ADR-0114 NÃO SE MEXE: o host continua nomeado num ponto só. O que mudou foi QUAL, e a
44
+ * regra que decide isso passa a estar escrita para não voltar a divergir — **busca-se de onde o LEITOR lê**.
45
+ * No dia em que a engine for dona do runtime (ADR-0124 cláusula 2), o leitor passa a ser ela e é ela que
46
+ * escolhe; até lá, o único leitor é a porta do cartucho.
47
+ */
48
+ export declare const HOST_DOS_MODELOS = "https://huggingface.co/diffusionstudio/piper-voices/resolve/main/";
49
+ /**
50
+ * O CAMINHO DO MODELO, DERIVADO DO IDENTIFICADOR — e não uma segunda tabela.
51
+ *
52
+ * `pt_BR-faber-medium` diz tudo o que o caminho precisa: `pt/pt_BR/faber/medium/pt_BR-faber-medium.onnx`.
53
+ * 🎯 DERIVAR EM VEZ DE TABELAR é a decisão inteira desta função: uma tabela de caminhos ao lado da tabela
54
+ * de vozes seria o mesmo facto escrito duas vezes, e este repositório já pagou isso três vezes — o
55
+ * `DomQuery`, os rótulos de movimento reduzido, as chaves de armazenamento. Duas listas divergem, e
56
+ * divergem uma entrada de cada vez.
57
+ *
58
+ * ⚠️ Devolve `null` para um identificador que não tenha a forma esperada, em vez de montar um caminho
59
+ * torto: uma URL inventada dá 404 na escola, e um `null` dá para reportar antes de sair de casa.
60
+ */
61
+ export declare function caminhoDoModelo(v: VozNeural): string | null;
62
+ /** O endereço completo do modelo. `null` quando o identificador não se deixa ler. */
63
+ export declare function urlDoModelo(v: VozNeural): string | null;
64
+ /**
65
+ * A CONFIGURAÇÃO da voz, que o motor lê junto com o modelo.
66
+ *
67
+ * 📌 `.onnx.json` e não um segundo caminho: é o mesmo ficheiro com outro sufixo, medido a responder 200 no
68
+ * mesmo sítio. Escrevê-lo como derivação mantém a regra de que o identificador é a única fonte.
69
+ */
70
+ export declare function urlDaConfig(v: VozNeural): string | null;
71
+ /** Em que pé está cada voz. `falhou` é um estado e não uma excepção — ver `vozEmUso`. */
72
+ export type EstadoDaVoz = 'ausente' | 'a-buscar' | 'pronta' | 'falhou';
73
+ /** O que se sabe de cada voz, por identificador. Uma voz ausente do mapa é `ausente`. */
74
+ export type EstadosDasVozes = Readonly<Record<string, EstadoDaVoz | undefined>>;
75
+ export declare const estadoDe: (estados: EstadosDasVozes, v: VozNeural) => EstadoDaVoz;
76
+ /**
77
+ * A ORDEM POR QUE SE BUSCAM: primeiro as do idioma corrente, depois as outras, cada grupo na ordem do
78
+ * catálogo. Só entram as que ainda não estão prontas nem em curso.
79
+ *
80
+ * ⚠️ O IDIOMA CORRENTE PRIMEIRO É A REGRA INTEIRA, e ela é sobre uma criança e não sobre eficiência: as
81
+ * outras são para «conforme interesse do jogador», mas a dela é a que ela precisa AGORA. Buscar em ordem de
82
+ * catálogo faria uma criança brasileira esperar por duas vozes inglesas na rede de uma escola.
83
+ *
84
+ * 📌 `falhou` VOLTA À FILA. Uma escola perde a rede a meio da manhã e recupera-a; uma voz marcada como
85
+ * falhada para sempre seria uma criança sem voz até alguém recarregar a página. Quem chama decide QUANDO
86
+ * tentar de novo — este módulo só diz que ainda há o que buscar.
87
+ */
88
+ export declare function ordemDeBusca(estados: EstadosDasVozes, localeCorrente: string, catalogo?: readonly VozNeural[]): VozNeural[];
89
+ /** O que a criança ouve agora. `recuo` é a voz do sistema — eSpeak ou Web Speech. */
90
+ export type VozEmUso = {
91
+ readonly tipo: 'neural';
92
+ readonly voice: string;
93
+ } | {
94
+ readonly tipo: 'recuo';
95
+ readonly porque: 'a-buscar' | 'sem-voz-para-o-idioma' | 'falhou' | 'ausente';
96
+ };
97
+ /**
98
+ * QUEM ESTÁ A FALAR, E POR QUE — e esta função é o gate 3 do ADR-0110 em forma de código.
99
+ *
100
+ * ⚠️ O DEFEITO QUE ELA IMPEDE TEM NOME NESTE REPOSITÓRIO: «um recuo que se apresenta como a coisa real é a
101
+ * família do `reflectTTS`». Uma engine que dissesse «voz neural» enquanto o modelo ainda desce faria um adulto
102
+ * concluir que a qualidade que ouve É a qualidade final — e desistir de esperar por uma coisa que já vinha a
103
+ * caminho. Dizer `recuo` e dizer PORQUÊ custa uma linha e é a diferença entre informar e enganar.
104
+ *
105
+ * 📌 A PRIMEIRA VOZ PRONTA DO IDIOMA, e nada mais: onde há duas prontas, esta função NÃO escolhe entre elas —
106
+ * devolve a primeira do catálogo porque tem de devolver alguma, e qual delas a criança prefere é a pergunta
107
+ * que o ADR-0110 deixou em aberto. Ver o cabeçalho.
108
+ */
109
+ export declare function vozEmUso(estados: EstadosDasVozes, localeCorrente: string, catalogo?: readonly VozNeural[]): VozEmUso;