redmine-context 1.0.0

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 (229) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +449 -0
  3. package/dist/bundle/index.d.ts +5 -0
  4. package/dist/bundle/index.js +5 -0
  5. package/dist/bundle/json.d.ts +90 -0
  6. package/dist/bundle/json.js +266 -0
  7. package/dist/bundle/markdown.d.ts +75 -0
  8. package/dist/bundle/markdown.js +294 -0
  9. package/dist/bundle/search-list.d.ts +43 -0
  10. package/dist/bundle/search-list.js +53 -0
  11. package/dist/bundle/stable-stringify.d.ts +26 -0
  12. package/dist/bundle/stable-stringify.js +50 -0
  13. package/dist/cache/contract.d.ts +157 -0
  14. package/dist/cache/contract.js +0 -0
  15. package/dist/cache/disk-index.d.ts +82 -0
  16. package/dist/cache/disk-index.js +220 -0
  17. package/dist/cache/disk.d.ts +133 -0
  18. package/dist/cache/disk.js +313 -0
  19. package/dist/cache/gc.d.ts +78 -0
  20. package/dist/cache/gc.js +123 -0
  21. package/dist/cache/get-or-compute.d.ts +36 -0
  22. package/dist/cache/get-or-compute.js +52 -0
  23. package/dist/cache/index.d.ts +9 -0
  24. package/dist/cache/index.js +8 -0
  25. package/dist/cache/keys.d.ts +76 -0
  26. package/dist/cache/keys.js +78 -0
  27. package/dist/cache/memory.d.ts +48 -0
  28. package/dist/cache/memory.js +110 -0
  29. package/dist/cache-first.d.ts +127 -0
  30. package/dist/cache-first.js +227 -0
  31. package/dist/client/errors.d.ts +33 -0
  32. package/dist/client/errors.js +49 -0
  33. package/dist/client/http.d.ts +110 -0
  34. package/dist/client/http.js +207 -0
  35. package/dist/client/index.d.ts +5 -0
  36. package/dist/client/index.js +5 -0
  37. package/dist/client/issues.d.ts +71 -0
  38. package/dist/client/issues.js +100 -0
  39. package/dist/client/search.d.ts +58 -0
  40. package/dist/client/search.js +81 -0
  41. package/dist/config/credentials.d.ts +247 -0
  42. package/dist/config/credentials.js +427 -0
  43. package/dist/config/doctor.d.ts +123 -0
  44. package/dist/config/doctor.js +260 -0
  45. package/dist/config/index.d.ts +6 -0
  46. package/dist/config/index.js +6 -0
  47. package/dist/config/keyring.d.ts +96 -0
  48. package/dist/config/keyring.js +158 -0
  49. package/dist/config/login.d.ts +97 -0
  50. package/dist/config/login.js +189 -0
  51. package/dist/config/settings.d.ts +94 -0
  52. package/dist/config/settings.js +140 -0
  53. package/dist/contract.d.ts +173 -0
  54. package/dist/contract.js +27 -0
  55. package/dist/core.d.ts +1 -0
  56. package/dist/core.js +8 -0
  57. package/dist/extract/audio-extractor.d.ts +105 -0
  58. package/dist/extract/audio-extractor.js +156 -0
  59. package/dist/extract/audio.d.ts +126 -0
  60. package/dist/extract/audio.js +184 -0
  61. package/dist/extract/dispatcher.d.ts +132 -0
  62. package/dist/extract/dispatcher.js +115 -0
  63. package/dist/extract/download.d.ts +111 -0
  64. package/dist/extract/download.js +261 -0
  65. package/dist/extract/duration.d.ts +106 -0
  66. package/dist/extract/duration.js +148 -0
  67. package/dist/extract/ffmpeg.d.ts +56 -0
  68. package/dist/extract/ffmpeg.js +95 -0
  69. package/dist/extract/gguf.d.ts +137 -0
  70. package/dist/extract/gguf.js +215 -0
  71. package/dist/extract/index.d.ts +19 -0
  72. package/dist/extract/index.js +19 -0
  73. package/dist/extract/magic.d.ts +80 -0
  74. package/dist/extract/magic.js +282 -0
  75. package/dist/extract/ooxml.d.ts +131 -0
  76. package/dist/extract/ooxml.js +336 -0
  77. package/dist/extract/pdf.d.ts +147 -0
  78. package/dist/extract/pdf.js +322 -0
  79. package/dist/extract/queue.d.ts +167 -0
  80. package/dist/extract/queue.js +217 -0
  81. package/dist/extract/subprocess.d.ts +145 -0
  82. package/dist/extract/subprocess.js +181 -0
  83. package/dist/extract/tesseract.d.ts +153 -0
  84. package/dist/extract/tesseract.js +321 -0
  85. package/dist/extract/video-extractor.d.ts +84 -0
  86. package/dist/extract/video-extractor.js +89 -0
  87. package/dist/extract/video.d.ts +198 -0
  88. package/dist/extract/video.js +418 -0
  89. package/dist/extract/which.d.ts +63 -0
  90. package/dist/extract/which.js +72 -0
  91. package/dist/extract/whisper-extract.d.ts +211 -0
  92. package/dist/extract/whisper-extract.js +323 -0
  93. package/dist/extract/whisper.d.ts +50 -0
  94. package/dist/extract/whisper.js +67 -0
  95. package/dist/extract/zip.d.ts +50 -0
  96. package/dist/extract/zip.js +165 -0
  97. package/dist/extract-issue-attachments.d.ts +82 -0
  98. package/dist/extract-issue-attachments.js +156 -0
  99. package/dist/fetch-attachment-text.d.ts +113 -0
  100. package/dist/fetch-attachment-text.js +155 -0
  101. package/dist/fetch-issue-bundle.d.ts +81 -0
  102. package/dist/fetch-issue-bundle.js +93 -0
  103. package/dist/fetch-issue-search.d.ts +74 -0
  104. package/dist/fetch-issue-search.js +120 -0
  105. package/dist/index.d.ts +21 -0
  106. package/dist/index.js +67 -0
  107. package/dist/normalize/collections.d.ts +52 -0
  108. package/dist/normalize/collections.js +170 -0
  109. package/dist/normalize/helpers.d.ts +35 -0
  110. package/dist/normalize/helpers.js +59 -0
  111. package/dist/normalize/index.d.ts +2 -0
  112. package/dist/normalize/index.js +2 -0
  113. package/dist/normalize/issue.d.ts +34 -0
  114. package/dist/normalize/issue.js +154 -0
  115. package/dist/surfaces/cli/commands.d.ts +61 -0
  116. package/dist/surfaces/cli/commands.js +262 -0
  117. package/dist/surfaces/cli/main.d.ts +32 -0
  118. package/dist/surfaces/cli/main.js +210 -0
  119. package/dist/surfaces/cli/prompts.d.ts +66 -0
  120. package/dist/surfaces/cli/prompts.js +147 -0
  121. package/dist/surfaces/cli/tty.d.ts +27 -0
  122. package/dist/surfaces/cli/tty.js +35 -0
  123. package/dist/surfaces/cli/types.d.ts +39 -0
  124. package/dist/surfaces/cli/types.js +7 -0
  125. package/dist/surfaces/mcp/server.d.ts +171 -0
  126. package/dist/surfaces/mcp/server.js +427 -0
  127. package/dist/surfaces/tui/app.d.ts +55 -0
  128. package/dist/surfaces/tui/app.js +180 -0
  129. package/dist/surfaces/tui/attachment-status.d.ts +79 -0
  130. package/dist/surfaces/tui/attachment-status.js +113 -0
  131. package/dist/surfaces/tui/components/breadcrumb.d.ts +7 -0
  132. package/dist/surfaces/tui/components/breadcrumb.js +31 -0
  133. package/dist/surfaces/tui/components/gradient-text.d.ts +23 -0
  134. package/dist/surfaces/tui/components/gradient-text.js +75 -0
  135. package/dist/surfaces/tui/components/scroll-view.d.ts +25 -0
  136. package/dist/surfaces/tui/components/scroll-view.js +77 -0
  137. package/dist/surfaces/tui/components/spinner.d.ts +12 -0
  138. package/dist/surfaces/tui/components/spinner.js +39 -0
  139. package/dist/surfaces/tui/components/text-input.d.ts +45 -0
  140. package/dist/surfaces/tui/components/text-input.js +114 -0
  141. package/dist/surfaces/tui/format-file-size.d.ts +24 -0
  142. package/dist/surfaces/tui/format-file-size.js +43 -0
  143. package/dist/surfaces/tui/glyphs.d.ts +47 -0
  144. package/dist/surfaces/tui/glyphs.js +84 -0
  145. package/dist/surfaces/tui/hooks/use-auth-guard.d.ts +39 -0
  146. package/dist/surfaces/tui/hooks/use-auth-guard.js +135 -0
  147. package/dist/surfaces/tui/hooks/use-doctor-status.d.ts +64 -0
  148. package/dist/surfaces/tui/hooks/use-doctor-status.js +123 -0
  149. package/dist/surfaces/tui/hooks/use-escape-interceptor.d.ts +25 -0
  150. package/dist/surfaces/tui/hooks/use-escape-interceptor.js +65 -0
  151. package/dist/surfaces/tui/hooks/use-exit-guard.d.ts +18 -0
  152. package/dist/surfaces/tui/hooks/use-exit-guard.js +66 -0
  153. package/dist/surfaces/tui/hooks/use-export-bundle.d.ts +62 -0
  154. package/dist/surfaces/tui/hooks/use-export-bundle.js +100 -0
  155. package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +61 -0
  156. package/dist/surfaces/tui/hooks/use-issue-detail.js +132 -0
  157. package/dist/surfaces/tui/hooks/use-issue-search.d.ts +71 -0
  158. package/dist/surfaces/tui/hooks/use-issue-search.js +168 -0
  159. package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +44 -0
  160. package/dist/surfaces/tui/hooks/use-list-navigation.js +82 -0
  161. package/dist/surfaces/tui/hooks/use-media-binaries.d.ts +24 -0
  162. package/dist/surfaces/tui/hooks/use-media-binaries.js +44 -0
  163. package/dist/surfaces/tui/hooks/use-my-issues.d.ts +66 -0
  164. package/dist/surfaces/tui/hooks/use-my-issues.js +151 -0
  165. package/dist/surfaces/tui/hooks/use-onboarding-callbacks.d.ts +11 -0
  166. package/dist/surfaces/tui/hooks/use-onboarding-callbacks.js +104 -0
  167. package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +52 -0
  168. package/dist/surfaces/tui/hooks/use-terminal-width.js +90 -0
  169. package/dist/surfaces/tui/index.d.ts +50 -0
  170. package/dist/surfaces/tui/index.js +158 -0
  171. package/dist/surfaces/tui/instance.d.ts +37 -0
  172. package/dist/surfaces/tui/instance.js +36 -0
  173. package/dist/surfaces/tui/job-registry.d.ts +113 -0
  174. package/dist/surfaces/tui/job-registry.js +123 -0
  175. package/dist/surfaces/tui/job-status.d.ts +45 -0
  176. package/dist/surfaces/tui/job-status.js +81 -0
  177. package/dist/surfaces/tui/navigation.d.ts +74 -0
  178. package/dist/surfaces/tui/navigation.js +87 -0
  179. package/dist/surfaces/tui/palettes.d.ts +38 -0
  180. package/dist/surfaces/tui/palettes.js +244 -0
  181. package/dist/surfaces/tui/screen.d.ts +30 -0
  182. package/dist/surfaces/tui/screen.js +51 -0
  183. package/dist/surfaces/tui/screens/about.d.ts +2 -0
  184. package/dist/surfaces/tui/screens/about.js +35 -0
  185. package/dist/surfaces/tui/screens/appearance.d.ts +2 -0
  186. package/dist/surfaces/tui/screens/appearance.js +74 -0
  187. package/dist/surfaces/tui/screens/config.d.ts +2 -0
  188. package/dist/surfaces/tui/screens/config.js +82 -0
  189. package/dist/surfaces/tui/screens/doctor.d.ts +2 -0
  190. package/dist/surfaces/tui/screens/doctor.js +109 -0
  191. package/dist/surfaces/tui/screens/export.d.ts +2 -0
  192. package/dist/surfaces/tui/screens/export.js +168 -0
  193. package/dist/surfaces/tui/screens/home-selection.d.ts +70 -0
  194. package/dist/surfaces/tui/screens/home-selection.js +80 -0
  195. package/dist/surfaces/tui/screens/home.d.ts +2 -0
  196. package/dist/surfaces/tui/screens/home.js +200 -0
  197. package/dist/surfaces/tui/screens/issue-detail.d.ts +6 -0
  198. package/dist/surfaces/tui/screens/issue-detail.js +182 -0
  199. package/dist/surfaces/tui/screens/jobs.d.ts +7 -0
  200. package/dist/surfaces/tui/screens/jobs.js +89 -0
  201. package/dist/surfaces/tui/screens/loaded-issue-context.d.ts +49 -0
  202. package/dist/surfaces/tui/screens/loaded-issue-context.js +57 -0
  203. package/dist/surfaces/tui/screens/onboarding/api-key.d.ts +2 -0
  204. package/dist/surfaces/tui/screens/onboarding/api-key.js +69 -0
  205. package/dist/surfaces/tui/screens/onboarding/login.d.ts +2 -0
  206. package/dist/surfaces/tui/screens/onboarding/login.js +50 -0
  207. package/dist/surfaces/tui/screens/onboarding/mode.d.ts +2 -0
  208. package/dist/surfaces/tui/screens/onboarding/mode.js +42 -0
  209. package/dist/surfaces/tui/screens/onboarding/onboarding-context.d.ts +221 -0
  210. package/dist/surfaces/tui/screens/onboarding/onboarding-context.js +131 -0
  211. package/dist/surfaces/tui/screens/onboarding/success.d.ts +2 -0
  212. package/dist/surfaces/tui/screens/onboarding/success.js +41 -0
  213. package/dist/surfaces/tui/screens/onboarding/url.d.ts +24 -0
  214. package/dist/surfaces/tui/screens/onboarding/url.js +84 -0
  215. package/dist/surfaces/tui/screens/onboarding/validating.d.ts +1 -0
  216. package/dist/surfaces/tui/screens/onboarding/validating.js +86 -0
  217. package/dist/surfaces/tui/screens/welcome.d.ts +6 -0
  218. package/dist/surfaces/tui/screens/welcome.js +95 -0
  219. package/dist/surfaces/tui/status-color.d.ts +21 -0
  220. package/dist/surfaces/tui/status-color.js +23 -0
  221. package/dist/surfaces/tui/symbols.d.ts +231 -0
  222. package/dist/surfaces/tui/symbols.js +14 -0
  223. package/dist/surfaces/tui/terminal-colors.d.ts +29 -0
  224. package/dist/surfaces/tui/terminal-colors.js +39 -0
  225. package/dist/surfaces/tui/theme.d.ts +156 -0
  226. package/dist/surfaces/tui/theme.js +86 -0
  227. package/dist/surfaces/tui/truncate.d.ts +31 -0
  228. package/dist/surfaces/tui/truncate.js +81 -0
  229. package/package.json +93 -0
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Localização de executáveis no PATH + locais convencionais por SO (M4-01, #57,
3
+ * ADR-002).
4
+ *
5
+ * Generaliza o padrão de {@link findTesseract} (busca `tesseract` no PATH e em
6
+ * `/opt/homebrew/bin` etc.) para os demais binários de mídia da milestone M4
7
+ * (`ffmpeg`, `whisper.cpp`), que aceitam MÚLTIPLOS nomes de binário — o
8
+ * whisper.cpp mudou de `main` (legado) para `whisper-cli` (atual) e o brew
9
+ * empacota como `whisper-cpp`. Decisões (exercitadas por testes):
10
+ *
11
+ * - PURO E INJETÁVEL: `platform`, `PATH` e o predicado de executabilidade são
12
+ * injetáveis — os três SOs e os estados presente/ausente são testáveis num
13
+ * único host, sem tocar filesystem/binário reais.
14
+ * - PRIORIDADE POR NOME: os nomes candidatos são tentados NA ORDEM dada (o loop
15
+ * externo é o nome), e dentro de cada nome o `PATH` vem antes dos locais
16
+ * convencionais — assim `whisper-cli` é preferido a `main` mesmo que ambos
17
+ * existam, e reportamos QUAL foi encontrado ({@link ExecutableLocation.binaryName}).
18
+ * - SUFIXO `.exe` NO WINDOWS: aplicado a todos os candidatos automaticamente.
19
+ */
20
+ /** Diretórios convencionais de um binário, separados por família de SO. */
21
+ export interface ConventionalDirs {
22
+ /** Locais convencionais em UNIX (darwin/linux), consultados após o `PATH`. */
23
+ readonly unix: readonly string[];
24
+ /** Locais convencionais no Windows, consultados após o `PATH`. */
25
+ readonly windows: readonly string[];
26
+ }
27
+ /** Resultado de {@link findExecutable}: caminho absoluto + qual nome casou. */
28
+ export interface ExecutableLocation {
29
+ /** Caminho absoluto do executável encontrado. */
30
+ readonly path: string;
31
+ /** Nome base do binário que casou (ex.: `whisper-cli`), sem sufixo `.exe`. */
32
+ readonly binaryName: string;
33
+ }
34
+ /** Dependências injetáveis de {@link findExecutable} — defaults de produção. */
35
+ export interface FindExecutableDeps {
36
+ /** SO alvo; default `process.platform`. Decide `.exe` e os locais convencionais. */
37
+ readonly platform?: NodeJS.Platform;
38
+ /** Valor do `PATH`; default `process.env.PATH`. */
39
+ readonly pathValue?: string | undefined;
40
+ /** Predicado de executabilidade; default {@link isExecutable}. */
41
+ readonly isExecutable?: (candidate: string) => boolean;
42
+ }
43
+ /**
44
+ * Verifica se um caminho aponta para um arquivo executável (bit X), sem lançar.
45
+ *
46
+ * @param candidate - Caminho absoluto candidato ao binário.
47
+ * @returns `true` se o arquivo existe e é executável pelo processo atual.
48
+ */
49
+ export declare function isExecutable(candidate: string): boolean;
50
+ /**
51
+ * Localiza o PRIMEIRO executável de {@link baseNames} no `PATH` e nos locais
52
+ * convencionais do SO. Função pura e reutilizável pelo `doctor` (#57); não
53
+ * executa o binário. A ordem de {@link baseNames} é a prioridade (nome externo,
54
+ * PATH antes dos convencionais dentro de cada nome).
55
+ *
56
+ * @param baseNames - Nomes candidatos, em ordem de preferência (sem `.exe`).
57
+ * @param conventional - Locais convencionais por família de SO.
58
+ * @param deps - Deps injetáveis (plataforma, PATH, executabilidade).
59
+ * @returns A localização encontrada (path + nome que casou), ou `undefined`.
60
+ * @example
61
+ * findExecutable(['whisper-cli', 'main'], { unix: ['/opt/homebrew/bin'], windows: [] });
62
+ */
63
+ export declare function findExecutable(baseNames: readonly string[], conventional: ConventionalDirs, deps?: FindExecutableDeps): ExecutableLocation | undefined;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Localização de executáveis no PATH + locais convencionais por SO (M4-01, #57,
3
+ * ADR-002).
4
+ *
5
+ * Generaliza o padrão de {@link findTesseract} (busca `tesseract` no PATH e em
6
+ * `/opt/homebrew/bin` etc.) para os demais binários de mídia da milestone M4
7
+ * (`ffmpeg`, `whisper.cpp`), que aceitam MÚLTIPLOS nomes de binário — o
8
+ * whisper.cpp mudou de `main` (legado) para `whisper-cli` (atual) e o brew
9
+ * empacota como `whisper-cpp`. Decisões (exercitadas por testes):
10
+ *
11
+ * - PURO E INJETÁVEL: `platform`, `PATH` e o predicado de executabilidade são
12
+ * injetáveis — os três SOs e os estados presente/ausente são testáveis num
13
+ * único host, sem tocar filesystem/binário reais.
14
+ * - PRIORIDADE POR NOME: os nomes candidatos são tentados NA ORDEM dada (o loop
15
+ * externo é o nome), e dentro de cada nome o `PATH` vem antes dos locais
16
+ * convencionais — assim `whisper-cli` é preferido a `main` mesmo que ambos
17
+ * existam, e reportamos QUAL foi encontrado ({@link ExecutableLocation.binaryName}).
18
+ * - SUFIXO `.exe` NO WINDOWS: aplicado a todos os candidatos automaticamente.
19
+ */
20
+ import { accessSync, constants } from 'node:fs';
21
+ import { posix, win32 } from 'node:path';
22
+ /**
23
+ * Verifica se um caminho aponta para um arquivo executável (bit X), sem lançar.
24
+ *
25
+ * @param candidate - Caminho absoluto candidato ao binário.
26
+ * @returns `true` se o arquivo existe e é executável pelo processo atual.
27
+ */
28
+ export function isExecutable(candidate) {
29
+ try {
30
+ accessSync(candidate, constants.X_OK);
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ /**
38
+ * Localiza o PRIMEIRO executável de {@link baseNames} no `PATH` e nos locais
39
+ * convencionais do SO. Função pura e reutilizável pelo `doctor` (#57); não
40
+ * executa o binário. A ordem de {@link baseNames} é a prioridade (nome externo,
41
+ * PATH antes dos convencionais dentro de cada nome).
42
+ *
43
+ * @param baseNames - Nomes candidatos, em ordem de preferência (sem `.exe`).
44
+ * @param conventional - Locais convencionais por família de SO.
45
+ * @param deps - Deps injetáveis (plataforma, PATH, executabilidade).
46
+ * @returns A localização encontrada (path + nome que casou), ou `undefined`.
47
+ * @example
48
+ * findExecutable(['whisper-cli', 'main'], { unix: ['/opt/homebrew/bin'], windows: [] });
49
+ */
50
+ export function findExecutable(baseNames, conventional, deps = {}) {
51
+ const platform = deps.platform ?? process.platform;
52
+ const isWindows = platform === 'win32';
53
+ const executable = deps.isExecutable ?? isExecutable;
54
+ const pathValue = deps.pathValue ?? process.env.PATH ?? '';
55
+ // Reason: `platform` é injetável (testar os 3 SOs num host só) — o join/split do
56
+ // PATH DEVE seguir o SO alvo, não o host, senão os separadores divergem.
57
+ const path = isWindows ? win32 : posix;
58
+ const pathDirs = pathValue.split(path.delimiter).filter((dir) => dir.length > 0);
59
+ const dirs = [...pathDirs, ...(isWindows ? conventional.windows : conventional.unix)];
60
+ const suffix = isWindows ? '.exe' : '';
61
+ // Reason: prioridade por NOME (loop externo) — um `whisper-cli` no PATH ganha
62
+ // de um `main` convencional, e é o nome reportado ao usuário.
63
+ for (const name of baseNames) {
64
+ for (const dir of dirs) {
65
+ const candidate = path.join(dir, `${name}${suffix}`);
66
+ if (executable(candidate)) {
67
+ return { path: candidate, binaryName: name };
68
+ }
69
+ }
70
+ }
71
+ return undefined;
72
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Extrator de transcrição de áudio via whisper.cpp (M4-05, #61, ADR-002).
3
+ *
4
+ * Recebe um WAV (o formato PCM 16 kHz mono produzido pela conversão da #60) e o
5
+ * transcreve para texto rodando o binário do whisper.cpp com o modelo GGUF do
6
+ * cache local. Espelha o estilo dos extratores já estabelecidos
7
+ * ({@link TesseractExtractor}, {@link convertAudioToWav}). Decisões de
8
+ * segurança/robustez (ADR-002), todas exercitadas por testes:
9
+ *
10
+ * - INVOCAÇÃO SEM SHELL: usa `execFile` (nunca `exec`/shell) com uma lista de
11
+ * argumentos explícita — o caminho do WAV e do modelo NUNCA são interpolados
12
+ * numa string de shell.
13
+ * - ENV SANITIZADO: o subprocesso recebe um env MÍNIMO (só `PATH`) — segredos do
14
+ * processo pai (ex.: `REDMINE_API_KEY`) NUNCA vazam para o whisper.
15
+ * - MODELO GGUF DO CACHE: o modelo é passado via `-m <path>`, resolvido a partir
16
+ * do diretório canônico de modelos ({@link whisperModelDir}) + {@link GGUF_MODEL_NAME}
17
+ * — o binário e o modelo são artefatos independentes (ver `whisper.ts`).
18
+ * - IDIOMA AUTO-DETECT COM OVERRIDE (#62): sem `language`, nenhuma flag de idioma
19
+ * é passada e o auto-detect (default do whisper.cpp, ADR-002) vale. Com
20
+ * `language` (ex.: `'pt'`), o idioma é FORÇADO via `-l <lang>` — e o idioma entra
21
+ * no `params` da chave attachment-level (ADR-004) e no `extraction.json`, de modo
22
+ * que trocar modelo OU idioma gera chave distinta (reprocessa) e o mesmo par gera
23
+ * a mesma chave (cache hit). O idioma é opcional (`exactOptionalPropertyTypes`:
24
+ * nunca injetado como `undefined`).
25
+ * - TIMEOUT + KILL: um watchdog envia `SIGTERM` no timeout e, após a graça,
26
+ * `SIGKILL` — um whisper travado não pendura a fila de jobs.
27
+ * - DEGRADAÇÃO GRACIOSA: binário ausente, modelo GGUF ausente, exit != 0 ou
28
+ * timeout NÃO lançam; devolvem `{ status: 'failed', metadata.reason }` com dica.
29
+ *
30
+ * UNTRUSTED: a transcrição é conteúdo DERIVADO de um anexo (input hostil), logo é
31
+ * NÃO-CONFIÁVEL. Este extrator NÃO reembrulha o texto: expõe `text` cru, e a
32
+ * camada bundle (`src/bundle/json.ts` / `src/bundle/markdown.ts`) o isola em
33
+ * `{ untrusted: true }` / `<untrusted-content>` — o mesmo mecanismo do tesseract.
34
+ *
35
+ * CONFIDENCE: o stdout default do whisper.cpp (mesmo com timestamps) NÃO carrega
36
+ * um score de confiança agregado; obter probabilidades por token exigiria a saída
37
+ * JSON (`-oj`) + leitura de arquivo, fora do escopo XS desta issue. Por isso
38
+ * `confidence` é OMITIDO do resultado (respeitando `exactOptionalPropertyTypes`,
39
+ * nunca injetado como `undefined`) — "se disponível na saída, senão omita".
40
+ *
41
+ * TESTABILIDADE: o executor do subprocesso, o localizador do binário e o do modelo
42
+ * são INJETÁVEIS — os testes unitários são herméticos (sem whisper/modelo/FS reais)
43
+ * e asseguram os args passados ao executor.
44
+ */
45
+ import type { ExtractorParams } from '../cache/contract.js';
46
+ import type { ExtractorConfig } from '../cache/keys.js';
47
+ import type { ExtractionResult } from '../contract.js';
48
+ import type { ExtractOptions, Extractor } from './dispatcher.js';
49
+ /**
50
+ * MIMEs de áudio roteados para a transcrição. O whisper.cpp consome o WAV; o
51
+ * pipeline real (áudio → WAV via #60 → whisper) é orquestrado por quem chama.
52
+ * Registrar estes MIMEs deixa o extrator pronto no registry default.
53
+ */
54
+ export declare const WHISPER_MIMES: readonly string[];
55
+ /**
56
+ * Parseia o stdout do whisper.cpp numa transcrição de linha única. Remove o
57
+ * prefixo de timestamp de cada linha (robustez, mesmo com `-nt` ativo), descarta
58
+ * linhas vazias e junta o texto com espaço, colapsando espaços em branco.
59
+ *
60
+ * @param stdout - Saída bruta do whisper.cpp.
61
+ * @returns O texto transcrito, ou string vazia se não houver conteúdo.
62
+ * @example
63
+ * parseWhisperOutput('[00:00:00.000 --> 00:00:02.000] Olá\n'); // 'Olá'
64
+ */
65
+ export declare function parseWhisperOutput(stdout: string): string;
66
+ /** Invocação concreta do whisper passada ao {@link WhisperRunner}. */
67
+ export interface WhisperInvocation {
68
+ /** Caminho absoluto do binário whisper.cpp já resolvido. */
69
+ readonly bin: string;
70
+ /** Argumentos (sem shell): `-m <modelo GGUF> -f <wav> -nt`. */
71
+ readonly args: readonly string[];
72
+ /** Env SANITIZADO do subprocesso (só `PATH`). */
73
+ readonly env: NodeJS.ProcessEnv;
74
+ /** Timeout antes do `SIGTERM` (ms). */
75
+ readonly timeoutMs: number;
76
+ /** Graça `SIGTERM` → `SIGKILL` (ms). */
77
+ readonly killGraceMs: number;
78
+ /**
79
+ * Sinal de CANCELAMENTO (#69/#73) repassado ao {@link runWithWatchdog}, que MATA
80
+ * o whisper (`SIGTERM`→`SIGKILL`) ao abortar. Opcional/aditivo (respeita
81
+ * `exactOptionalPropertyTypes` — nunca injetado como `undefined`).
82
+ */
83
+ readonly signal?: AbortSignal;
84
+ }
85
+ /**
86
+ * Executor injetável do subprocesso whisper. Resolve com o `stdout` (a
87
+ * transcrição) no sucesso; rejeita em falha (exit != 0, binário não-executável em
88
+ * runtime) — rejeitando com um erro de nome `WhisperTimeoutError` no estouro de
89
+ * timeout.
90
+ */
91
+ export type WhisperRunner = (invocation: WhisperInvocation) => Promise<string>;
92
+ /** Opções de construção do {@link WhisperExtractor}. */
93
+ export interface WhisperExtractorOptions {
94
+ /**
95
+ * Caminho absoluto do binário já resolvido. `undefined` = não instalado; nesse
96
+ * caso {@link WhisperExtractor.extract} degrada para `failed` (não lança).
97
+ */
98
+ readonly binaryPath?: string | undefined;
99
+ /**
100
+ * Caminho absoluto do modelo GGUF no cache. `undefined` = modelo ausente; nesse
101
+ * caso {@link WhisperExtractor.extract} degrada para `failed` (não lança).
102
+ */
103
+ readonly modelPath?: string | undefined;
104
+ /** Versão exposta na chave de cache (default de fábrica: {@link INTEGRATION_VERSION}). */
105
+ readonly version: string;
106
+ /**
107
+ * Idioma a FORÇAR (#62), ex.: `'pt'`. Ausente (`undefined`) = auto-detect (o
108
+ * default do whisper.cpp). Quando dado, entra nos args (`-l <lang>`), no `params`
109
+ * da chave de cache (ADR-004) e no `extraction.json`. Opcional — nunca injetado
110
+ * como `undefined` (respeita `exactOptionalPropertyTypes`).
111
+ */
112
+ readonly language?: string | undefined;
113
+ /** Executor do subprocesso; default: watchdog real com `execFile`. */
114
+ readonly run?: WhisperRunner;
115
+ /** Timeout antes do `SIGTERM` (ms); default {@link DEFAULT_TIMEOUT_MS}. */
116
+ readonly timeoutMs?: number;
117
+ /** Graça `SIGTERM` → `SIGKILL` (ms); default {@link DEFAULT_KILL_GRACE_MS}. */
118
+ readonly killGraceMs?: number;
119
+ }
120
+ /**
121
+ * Extrator de transcrição baseado no binário whisper.cpp (ADR-002). Implementa
122
+ * {@link Extractor} e, além do contrato, expõe {@link extractorConfig} estável
123
+ * para {@link buildAttachmentKey}.
124
+ */
125
+ export declare class WhisperExtractor implements Extractor {
126
+ readonly id = "whisper-transcribe";
127
+ readonly version: string;
128
+ readonly supportedMimes: readonly string[];
129
+ /**
130
+ * Modelo lógico (nome do GGUF) para a chave de cache (ADR-004). Derivado do
131
+ * `modelPath` (basename) quando dado, para que TROCAR o modelo gere chave
132
+ * attachment-level DISTINTA (#72); sem `modelPath`, cai no default pinado.
133
+ */
134
+ readonly model: string;
135
+ /**
136
+ * Parâmetros escalares da chave de cache (ADR-004). Vazio no auto-detect; com
137
+ * idioma forçado (#62), `{ language }` — trocar o idioma gera chave distinta.
138
+ */
139
+ readonly params: ExtractorParams;
140
+ private readonly binaryPath;
141
+ private readonly modelPath;
142
+ /** Idioma forçado (#62), ou `undefined` = auto-detect. */
143
+ private readonly language;
144
+ private readonly run;
145
+ private readonly timeoutMs;
146
+ private readonly killGraceMs;
147
+ /**
148
+ * @param options - Ver {@link WhisperExtractorOptions}.
149
+ */
150
+ constructor(options: WhisperExtractorOptions);
151
+ /**
152
+ * Configuração do extrator pronta para {@link buildAttachmentKey} (ADR-004).
153
+ * @returns `{ version, model, params }` estáveis desta instância.
154
+ */
155
+ get extractorConfig(): ExtractorConfig;
156
+ /**
157
+ * Transcreve o WAV em `filePath`. Nunca lança: falhas (binário ausente, modelo
158
+ * GGUF ausente, timeout, erro de execução) viram `{ status: 'failed',
159
+ * metadata.reason }` — degradação graciosa (ADR-002).
160
+ *
161
+ * @param filePath - Caminho absoluto do WAV (ou áudio) a transcrever.
162
+ * @param options - MIME real + logger — ver {@link ExtractOptions}.
163
+ * @returns `done` com `text` no sucesso; `failed` com motivo claro na falha.
164
+ */
165
+ extract(filePath: string, options: ExtractOptions): Promise<ExtractionResult>;
166
+ /**
167
+ * Monta um `ExtractionResult` `failed` com metadados de diagnóstico, sem injetar
168
+ * chaves `undefined` (respeita `exactOptionalPropertyTypes`).
169
+ *
170
+ * @param mime - MIME real detectado (do dispatcher).
171
+ * @param reason - Motivo canônico da falha.
172
+ * @param extra - Metadados adicionais (hint/erro).
173
+ * @returns Resultado `failed` tipado.
174
+ */
175
+ private failed;
176
+ }
177
+ /** Opções de composição do {@link createWhisperExtractor} (localizadores injetáveis). */
178
+ export interface CreateWhisperExtractorOptions {
179
+ /**
180
+ * Localizador do binário whisper.cpp; retorna o caminho absoluto ou `undefined`
181
+ * (não instalado). Default: {@link findWhisper} no ambiente real.
182
+ */
183
+ readonly findBinary?: () => string | undefined;
184
+ /**
185
+ * Localizador do modelo GGUF no cache; retorna o caminho absoluto ou `undefined`
186
+ * (ausente). Default: {@link whisperModelDir} + {@link GGUF_MODEL_NAME} se existir.
187
+ */
188
+ readonly findModel?: () => string | undefined;
189
+ /**
190
+ * Idioma a FORÇAR (#62), ex.: `'pt'`. Ausente = auto-detect. Propagado ao
191
+ * {@link WhisperExtractor} (args `-l <lang>` + `params` da chave + extraction.json).
192
+ */
193
+ readonly language?: string | undefined;
194
+ /** Executor do subprocesso; default: watchdog real com `execFile`. */
195
+ readonly run?: WhisperRunner;
196
+ /** Timeout antes do `SIGTERM` (ms); default {@link DEFAULT_TIMEOUT_MS}. */
197
+ readonly timeoutMs?: number;
198
+ /** Graça `SIGTERM` → `SIGKILL` (ms); default {@link DEFAULT_KILL_GRACE_MS}. */
199
+ readonly killGraceMs?: number;
200
+ }
201
+ /**
202
+ * Cria um {@link WhisperExtractor} resolvendo binário e modelo GGUF. Localiza o
203
+ * whisper.cpp ({@link findWhisper}) e o modelo no cache; ambos ausentes fazem o
204
+ * extrator degradar graciosamente em `extract` (ADR-002), sem lançar aqui.
205
+ *
206
+ * @param options - Localizadores/executor/timeouts injetáveis — ver {@link CreateWhisperExtractorOptions}.
207
+ * @returns O extrator pronto para registro no {@link ExtractorRegistry}.
208
+ * @example
209
+ * const extractor = await createWhisperExtractor();
210
+ */
211
+ export declare function createWhisperExtractor(options?: CreateWhisperExtractorOptions): Promise<WhisperExtractor>;
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Extrator de transcrição de áudio via whisper.cpp (M4-05, #61, ADR-002).
3
+ *
4
+ * Recebe um WAV (o formato PCM 16 kHz mono produzido pela conversão da #60) e o
5
+ * transcreve para texto rodando o binário do whisper.cpp com o modelo GGUF do
6
+ * cache local. Espelha o estilo dos extratores já estabelecidos
7
+ * ({@link TesseractExtractor}, {@link convertAudioToWav}). Decisões de
8
+ * segurança/robustez (ADR-002), todas exercitadas por testes:
9
+ *
10
+ * - INVOCAÇÃO SEM SHELL: usa `execFile` (nunca `exec`/shell) com uma lista de
11
+ * argumentos explícita — o caminho do WAV e do modelo NUNCA são interpolados
12
+ * numa string de shell.
13
+ * - ENV SANITIZADO: o subprocesso recebe um env MÍNIMO (só `PATH`) — segredos do
14
+ * processo pai (ex.: `REDMINE_API_KEY`) NUNCA vazam para o whisper.
15
+ * - MODELO GGUF DO CACHE: o modelo é passado via `-m <path>`, resolvido a partir
16
+ * do diretório canônico de modelos ({@link whisperModelDir}) + {@link GGUF_MODEL_NAME}
17
+ * — o binário e o modelo são artefatos independentes (ver `whisper.ts`).
18
+ * - IDIOMA AUTO-DETECT COM OVERRIDE (#62): sem `language`, nenhuma flag de idioma
19
+ * é passada e o auto-detect (default do whisper.cpp, ADR-002) vale. Com
20
+ * `language` (ex.: `'pt'`), o idioma é FORÇADO via `-l <lang>` — e o idioma entra
21
+ * no `params` da chave attachment-level (ADR-004) e no `extraction.json`, de modo
22
+ * que trocar modelo OU idioma gera chave distinta (reprocessa) e o mesmo par gera
23
+ * a mesma chave (cache hit). O idioma é opcional (`exactOptionalPropertyTypes`:
24
+ * nunca injetado como `undefined`).
25
+ * - TIMEOUT + KILL: um watchdog envia `SIGTERM` no timeout e, após a graça,
26
+ * `SIGKILL` — um whisper travado não pendura a fila de jobs.
27
+ * - DEGRADAÇÃO GRACIOSA: binário ausente, modelo GGUF ausente, exit != 0 ou
28
+ * timeout NÃO lançam; devolvem `{ status: 'failed', metadata.reason }` com dica.
29
+ *
30
+ * UNTRUSTED: a transcrição é conteúdo DERIVADO de um anexo (input hostil), logo é
31
+ * NÃO-CONFIÁVEL. Este extrator NÃO reembrulha o texto: expõe `text` cru, e a
32
+ * camada bundle (`src/bundle/json.ts` / `src/bundle/markdown.ts`) o isola em
33
+ * `{ untrusted: true }` / `<untrusted-content>` — o mesmo mecanismo do tesseract.
34
+ *
35
+ * CONFIDENCE: o stdout default do whisper.cpp (mesmo com timestamps) NÃO carrega
36
+ * um score de confiança agregado; obter probabilidades por token exigiria a saída
37
+ * JSON (`-oj`) + leitura de arquivo, fora do escopo XS desta issue. Por isso
38
+ * `confidence` é OMITIDO do resultado (respeitando `exactOptionalPropertyTypes`,
39
+ * nunca injetado como `undefined`) — "se disponível na saída, senão omita".
40
+ *
41
+ * TESTABILIDADE: o executor do subprocesso, o localizador do binário e o do modelo
42
+ * são INJETÁVEIS — os testes unitários são herméticos (sem whisper/modelo/FS reais)
43
+ * e asseguram os args passados ao executor.
44
+ */
45
+ import { existsSync } from 'node:fs';
46
+ import { basename, join } from 'node:path';
47
+ import { GGUF_MODEL_NAME } from './gguf.js';
48
+ import { runWithWatchdog, sanitizedEnv } from './subprocess.js';
49
+ import { findWhisper, whisperModelDir } from './whisper.js';
50
+ /** Identificador estável do extrator (entra em metadados). */
51
+ const EXTRACTOR_ID = 'whisper-transcribe';
52
+ /**
53
+ * Modelo lógico para a chave de cache (ADR-004): o nome do arquivo GGUF. Trocar o
54
+ * modelo (ex.: `ggml-tiny` → `ggml-base`) muda a identidade e invalida o cache.
55
+ */
56
+ const EXTRACTOR_MODEL = GGUF_MODEL_NAME;
57
+ /**
58
+ * Versão da integração. Diferente de ffmpeg/tesseract, o whisper.cpp não tem um
59
+ * `--version` estável entre releases (ver `whisper.ts`), então usamos uma versão
60
+ * de integração fixa para a chave de cache (ADR-004).
61
+ */
62
+ const INTEGRATION_VERSION = 'whisper-integration-1';
63
+ /** Timeout default de uma transcrição antes do `SIGTERM` (ms) — áudio é lento. */
64
+ const DEFAULT_TIMEOUT_MS = 120_000;
65
+ /** Graça entre `SIGTERM` e `SIGKILL` (ms) — dá ao whisper chance de sair limpo. */
66
+ const DEFAULT_KILL_GRACE_MS = 2_000;
67
+ /** Teto do stdout capturado (16 MiB) — transcrições longas cabem com folga. */
68
+ const MAX_BUFFER_BYTES = 16 * 1024 * 1024;
69
+ /** Flag `-nt` (no timestamps): mantém o stdout como texto puro, fácil de parsear. */
70
+ const NO_TIMESTAMPS_FLAG = '-nt';
71
+ /**
72
+ * Flag de idioma do whisper-cli (whisper.cpp): `-l <lang>` (equiv. `--language`).
73
+ * Só é acrescentada quando o idioma é forçado (#62); ausente = auto-detect.
74
+ */
75
+ const LANGUAGE_FLAG = '-l';
76
+ /**
77
+ * MIMEs de áudio roteados para a transcrição. O whisper.cpp consome o WAV; o
78
+ * pipeline real (áudio → WAV via #60 → whisper) é orquestrado por quem chama.
79
+ * Registrar estes MIMEs deixa o extrator pronto no registry default.
80
+ */
81
+ export const WHISPER_MIMES = [
82
+ 'audio/wav',
83
+ 'audio/x-wav',
84
+ 'audio/mpeg',
85
+ 'audio/mp4',
86
+ 'audio/ogg',
87
+ 'audio/webm',
88
+ 'audio/flac',
89
+ ];
90
+ /** Regex do prefixo de timestamp do whisper.cpp: `[00:00:00.000 --> 00:00:02.000]`. */
91
+ const TIMESTAMP_PREFIX = /^\[[0-9:.]+\s*-->\s*[0-9:.]+\]\s*/;
92
+ /** Colapsa qualquer sequência de espaços em branco num único espaço. */
93
+ const WHITESPACE_RUN = /\s+/g;
94
+ /**
95
+ * Parseia o stdout do whisper.cpp numa transcrição de linha única. Remove o
96
+ * prefixo de timestamp de cada linha (robustez, mesmo com `-nt` ativo), descarta
97
+ * linhas vazias e junta o texto com espaço, colapsando espaços em branco.
98
+ *
99
+ * @param stdout - Saída bruta do whisper.cpp.
100
+ * @returns O texto transcrito, ou string vazia se não houver conteúdo.
101
+ * @example
102
+ * parseWhisperOutput('[00:00:00.000 --> 00:00:02.000] Olá\n'); // 'Olá'
103
+ */
104
+ export function parseWhisperOutput(stdout) {
105
+ return stdout
106
+ .split('\n')
107
+ .map((line) => line.replace(TIMESTAMP_PREFIX, '').trim())
108
+ .filter((line) => line.length > 0)
109
+ .join(' ')
110
+ .replace(WHITESPACE_RUN, ' ')
111
+ .trim();
112
+ }
113
+ /** Erro interno: o watchdog matou o whisper por estourar o timeout. */
114
+ class WhisperTimeoutError extends Error {
115
+ timeoutMs;
116
+ constructor(timeoutMs) {
117
+ super(`whisper excedeu o timeout de ${timeoutMs}ms e foi encerrado`);
118
+ this.timeoutMs = timeoutMs;
119
+ this.name = 'WhisperTimeoutError';
120
+ }
121
+ }
122
+ /**
123
+ * Monta os argumentos do whisper.cpp: modelo GGUF via `-m`, entrada WAV via `-f` e
124
+ * `-nt` (sem timestamps, stdout limpo). Se `language` for dado (#62), acrescenta
125
+ * `-l <lang>` para FORÇAR o idioma; ausente, nenhuma flag de idioma é passada e o
126
+ * auto-detect (default do whisper.cpp, ADR-002) vale.
127
+ *
128
+ * @param modelPath - Caminho absoluto do modelo GGUF no cache.
129
+ * @param inputPath - Caminho absoluto do WAV a transcrever.
130
+ * @param language - Código do idioma a forçar (ex.: `'pt'`), ou `undefined` = auto.
131
+ * @returns Lista de argumentos, na ordem esperada pelo whisper.cpp.
132
+ */
133
+ function buildWhisperArgs(modelPath, inputPath, language) {
134
+ const args = ['-m', modelPath, '-f', inputPath, NO_TIMESTAMPS_FLAG];
135
+ return language !== undefined ? [...args, LANGUAGE_FLAG, language] : args;
136
+ }
137
+ /** `true` se o erro sinaliza estouro de timeout do watchdog (por nome, robusto a DI). */
138
+ function isTimeoutError(error) {
139
+ return error instanceof Error && error.name === 'WhisperTimeoutError';
140
+ }
141
+ /**
142
+ * Executor default: delega ao watchdog compartilhado ({@link runWithWatchdog}) —
143
+ * whisper.cpp SEM shell, env sanitizado e escalonamento `SIGTERM` → graça →
144
+ * `SIGKILL`. Resolve com o `stdout` (a transcrição) no exit 0; o estouro de
145
+ * timeout é sinalizado com {@link WhisperTimeoutError} para preservar a
146
+ * classificação de falha (`reason: 'timeout'`).
147
+ *
148
+ * @param invocation - Ver {@link WhisperInvocation}.
149
+ * @returns O stdout (transcrição) do whisper.
150
+ */
151
+ function defaultRun(invocation) {
152
+ const { bin, args, env, timeoutMs, killGraceMs, signal } = invocation;
153
+ return runWithWatchdog({
154
+ bin,
155
+ args,
156
+ env,
157
+ timeoutMs,
158
+ killGraceMs,
159
+ maxBuffer: MAX_BUFFER_BYTES,
160
+ ...(signal !== undefined ? { signal } : {}),
161
+ }, { makeTimeoutError: (ms) => new WhisperTimeoutError(ms) });
162
+ }
163
+ /**
164
+ * Extrator de transcrição baseado no binário whisper.cpp (ADR-002). Implementa
165
+ * {@link Extractor} e, além do contrato, expõe {@link extractorConfig} estável
166
+ * para {@link buildAttachmentKey}.
167
+ */
168
+ export class WhisperExtractor {
169
+ id = EXTRACTOR_ID;
170
+ version;
171
+ supportedMimes = WHISPER_MIMES;
172
+ /**
173
+ * Modelo lógico (nome do GGUF) para a chave de cache (ADR-004). Derivado do
174
+ * `modelPath` (basename) quando dado, para que TROCAR o modelo gere chave
175
+ * attachment-level DISTINTA (#72); sem `modelPath`, cai no default pinado.
176
+ */
177
+ model;
178
+ /**
179
+ * Parâmetros escalares da chave de cache (ADR-004). Vazio no auto-detect; com
180
+ * idioma forçado (#62), `{ language }` — trocar o idioma gera chave distinta.
181
+ */
182
+ params;
183
+ binaryPath;
184
+ modelPath;
185
+ /** Idioma forçado (#62), ou `undefined` = auto-detect. */
186
+ language;
187
+ run;
188
+ timeoutMs;
189
+ killGraceMs;
190
+ /**
191
+ * @param options - Ver {@link WhisperExtractorOptions}.
192
+ */
193
+ constructor(options) {
194
+ this.version = options.version;
195
+ this.binaryPath = options.binaryPath;
196
+ this.modelPath = options.modelPath;
197
+ // O `model` da chave reflete o GGUF REAL em uso (basename do modelPath); assim
198
+ // trocar de modelo invalida a chave attachment-level (ADR-004, #72). Sem
199
+ // modelPath (modelo ausente), usa o nome pinado como rótulo estável.
200
+ this.model = options.modelPath !== undefined ? basename(options.modelPath) : EXTRACTOR_MODEL;
201
+ this.language = options.language;
202
+ // Idioma forçado entra no params da chave; auto-detect mantém params vazio (sem
203
+ // injetar `undefined` — respeita `exactOptionalPropertyTypes`).
204
+ this.params = options.language !== undefined ? { language: options.language } : {};
205
+ this.run = options.run ?? defaultRun;
206
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
207
+ this.killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
208
+ }
209
+ /**
210
+ * Configuração do extrator pronta para {@link buildAttachmentKey} (ADR-004).
211
+ * @returns `{ version, model, params }` estáveis desta instância.
212
+ */
213
+ get extractorConfig() {
214
+ return { version: this.version, model: this.model, params: this.params };
215
+ }
216
+ /**
217
+ * Transcreve o WAV em `filePath`. Nunca lança: falhas (binário ausente, modelo
218
+ * GGUF ausente, timeout, erro de execução) viram `{ status: 'failed',
219
+ * metadata.reason }` — degradação graciosa (ADR-002).
220
+ *
221
+ * @param filePath - Caminho absoluto do WAV (ou áudio) a transcrever.
222
+ * @param options - MIME real + logger — ver {@link ExtractOptions}.
223
+ * @returns `done` com `text` no sucesso; `failed` com motivo claro na falha.
224
+ */
225
+ async extract(filePath, options) {
226
+ const bin = this.binaryPath;
227
+ if (bin === undefined) {
228
+ options.logger?.warn('whisper: binário não encontrado no PATH nem em locais convencionais; ' +
229
+ 'instale o whisper.cpp (ex.: `brew install whisper-cpp`) — veja o doctor');
230
+ return this.failed(options.mime, 'whisper-nao-instalado', {
231
+ hint: 'instale o whisper.cpp (whisper-cli/whisper-cpp); o doctor (#57) valida a instalação',
232
+ });
233
+ }
234
+ const model = this.modelPath;
235
+ if (model === undefined) {
236
+ options.logger?.warn('whisper: modelo GGUF não encontrado no cache; ' +
237
+ 'baixe o modelo (ex.: `--download-binaries`) — veja o doctor');
238
+ return this.failed(options.mime, 'modelo-nao-encontrado', {
239
+ hint: 'o modelo GGUF do whisper.cpp não está no cache; o doctor (#57) orienta o download',
240
+ });
241
+ }
242
+ try {
243
+ const stdout = await this.run({
244
+ bin,
245
+ args: buildWhisperArgs(model, filePath, this.language),
246
+ env: sanitizedEnv(),
247
+ timeoutMs: this.timeoutMs,
248
+ killGraceMs: this.killGraceMs,
249
+ // Cancelamento (#69/#73): repassa o signal do dispatch até o watchdog, que
250
+ // MATA o whisper ao abortar. Só inclui quando dado (exactOptionalPropertyTypes).
251
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
252
+ });
253
+ return {
254
+ status: 'done',
255
+ text: parseWhisperOutput(stdout),
256
+ mime: options.mime,
257
+ // Reason (#62): model + params (incl. idioma) são persistidos no
258
+ // extraction.json — registram a identidade da extração produzida.
259
+ metadata: {
260
+ extractorId: this.id,
261
+ version: this.version,
262
+ model: this.model,
263
+ params: this.params,
264
+ },
265
+ };
266
+ }
267
+ catch (error) {
268
+ return this.failed(options.mime, isTimeoutError(error) ? 'timeout' : 'erro-transcricao', {
269
+ error: error instanceof Error ? error.message : String(error),
270
+ });
271
+ }
272
+ }
273
+ /**
274
+ * Monta um `ExtractionResult` `failed` com metadados de diagnóstico, sem injetar
275
+ * chaves `undefined` (respeita `exactOptionalPropertyTypes`).
276
+ *
277
+ * @param mime - MIME real detectado (do dispatcher).
278
+ * @param reason - Motivo canônico da falha.
279
+ * @param extra - Metadados adicionais (hint/erro).
280
+ * @returns Resultado `failed` tipado.
281
+ */
282
+ failed(mime, reason, extra) {
283
+ return {
284
+ status: 'failed',
285
+ mime,
286
+ metadata: { extractorId: this.id, version: this.version, reason, ...extra },
287
+ };
288
+ }
289
+ }
290
+ /**
291
+ * Resolve o caminho default do modelo GGUF no cache — {@link whisperModelDir} +
292
+ * {@link GGUF_MODEL_NAME} — retornando-o só se o arquivo existir.
293
+ *
294
+ * @returns O caminho absoluto do modelo, ou `undefined` se ausente do cache.
295
+ */
296
+ function defaultFindModel() {
297
+ const modelPath = join(whisperModelDir(), GGUF_MODEL_NAME);
298
+ return existsSync(modelPath) ? modelPath : undefined;
299
+ }
300
+ /**
301
+ * Cria um {@link WhisperExtractor} resolvendo binário e modelo GGUF. Localiza o
302
+ * whisper.cpp ({@link findWhisper}) e o modelo no cache; ambos ausentes fazem o
303
+ * extrator degradar graciosamente em `extract` (ADR-002), sem lançar aqui.
304
+ *
305
+ * @param options - Localizadores/executor/timeouts injetáveis — ver {@link CreateWhisperExtractorOptions}.
306
+ * @returns O extrator pronto para registro no {@link ExtractorRegistry}.
307
+ * @example
308
+ * const extractor = await createWhisperExtractor();
309
+ */
310
+ export function createWhisperExtractor(options = {}) {
311
+ const findBinary = options.findBinary ?? (() => findWhisper()?.path);
312
+ const findModel = options.findModel ?? defaultFindModel;
313
+ const extractor = new WhisperExtractor({
314
+ binaryPath: findBinary(),
315
+ modelPath: findModel(),
316
+ version: INTEGRATION_VERSION,
317
+ ...(options.language !== undefined ? { language: options.language } : {}),
318
+ ...(options.run !== undefined ? { run: options.run } : {}),
319
+ ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
320
+ ...(options.killGraceMs !== undefined ? { killGraceMs: options.killGraceMs } : {}),
321
+ });
322
+ return Promise.resolve(extractor);
323
+ }