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,153 @@
1
+ /**
2
+ * Extrator OCR via tesseract (M3-09, ADR-002).
3
+ *
4
+ * Roda o binário `tesseract` sobre uma imagem já baixada no cache e devolve o
5
+ * texto reconhecido num {@link ExtractionResult}. Decisões de segurança/robustez
6
+ * (ADR-002), todas exercitadas por testes:
7
+ *
8
+ * - INVOCAÇÃO SEM SHELL: usa `execFile` (nunca `exec`/shell) com uma lista de
9
+ * argumentos explícita — o `filePath` NUNCA é interpolado numa string de shell,
10
+ * eliminando injeção por nome de arquivo malicioso.
11
+ * - ENV SANITIZADO: o subprocesso recebe um env MÍNIMO e EXPLÍCITO (só `PATH` e,
12
+ * se presente, `TESSDATA_PREFIX`) — segredos do processo pai (ex.:
13
+ * `REDMINE_API_KEY`) NUNCA vazam para o tesseract.
14
+ * - TIMEOUT + KILL: um watchdog envia `SIGTERM` no timeout e, após um período de
15
+ * graça, `SIGKILL` — um tesseract travado não pendura a fila de jobs.
16
+ * - DEGRADAÇÃO GRACIOSA: binário ausente do PATH/locais convencionais NÃO lança;
17
+ * devolve `{ status: 'failed', metadata.reason }` com dica de instalação (o
18
+ * `doctor` da #53 orienta o usuário).
19
+ *
20
+ * A chave de cache do anexo (ADR-004) depende de `version` + `model` + `params`
21
+ * ({@link buildAttachmentKey}); por isso o extrator expõe {@link TesseractExtractor.version}
22
+ * (derivada do binário quando detectável, senão a versão da integração) e
23
+ * {@link TesseractExtractor.params} (inclui `lang`, que participa da identidade).
24
+ */
25
+ import type { ExtractorParams } from '../cache/contract.js';
26
+ import type { ExtractorConfig } from '../cache/keys.js';
27
+ import type { ExtractionResult } from '../contract.js';
28
+ import { ExtractorRegistry, type ExtractOptions, type Extractor } from './dispatcher.js';
29
+ /** MIMEs REAIS (magic bytes) que o tesseract aceita via leptonica. */
30
+ export declare const TESSERACT_MIMES: readonly string[];
31
+ /** Resultado de {@link findTesseract}: caminho absoluto do binário localizado. */
32
+ export interface TesseractLocation {
33
+ /** Caminho absoluto do executável `tesseract` encontrado. */
34
+ readonly path: string;
35
+ }
36
+ /**
37
+ * Localiza o binário `tesseract` no `PATH` e em locais convencionais por
38
+ * plataforma (`/opt/homebrew/bin`, `/usr/local/bin`, `C:\\Program Files\\Tesseract-OCR`).
39
+ * Função pura e reutilizável pelo `doctor` (#53). Não executa o binário.
40
+ *
41
+ * @returns A localização encontrada, ou `undefined` se não instalado.
42
+ * @example
43
+ * const found = findTesseract();
44
+ * if (found === undefined) logger.warn('tesseract não instalado');
45
+ */
46
+ export declare function findTesseract(): TesseractLocation | undefined;
47
+ /** Opções de construção do {@link TesseractExtractor}. */
48
+ export interface TesseractExtractorOptions {
49
+ /**
50
+ * Caminho absoluto do binário já resolvido. `undefined` = não instalado; nesse
51
+ * caso {@link TesseractExtractor.extract} degrada para `failed` (não lança).
52
+ */
53
+ readonly binaryPath?: string | undefined;
54
+ /** Versão exposta na chave de cache (ver {@link INTEGRATION_VERSION} como fallback). */
55
+ readonly version: string;
56
+ /** Idiomas do OCR; default {@link DEFAULT_LANG} (`por+eng`). */
57
+ readonly lang?: string;
58
+ /** Page Segmentation Mode; default {@link DEFAULT_PSM}. */
59
+ readonly psm?: number;
60
+ /** Timeout antes do `SIGTERM` (ms); default {@link DEFAULT_TIMEOUT_MS}. */
61
+ readonly timeoutMs?: number;
62
+ /** Graça `SIGTERM`→`SIGKILL` (ms); default {@link DEFAULT_KILL_GRACE_MS}. */
63
+ readonly killGraceMs?: number;
64
+ }
65
+ /**
66
+ * Extrator OCR baseado no binário `tesseract` (ADR-002). Implementa
67
+ * {@link Extractor} e, além do contrato, expõe {@link params} e {@link extractorConfig}
68
+ * estáveis para {@link buildAttachmentKey}.
69
+ */
70
+ export declare class TesseractExtractor implements Extractor {
71
+ readonly id = "tesseract-ocr";
72
+ readonly version: string;
73
+ readonly supportedMimes: readonly string[];
74
+ /** Modelo lógico para a chave de cache (ADR-004). */
75
+ readonly model = "tesseract";
76
+ /** Parâmetros escalares estáveis (`lang`, `psm`) — participam da chave de cache. */
77
+ readonly params: ExtractorParams;
78
+ private readonly binaryPath;
79
+ private readonly lang;
80
+ private readonly psm;
81
+ private readonly timeoutMs;
82
+ private readonly killGraceMs;
83
+ /**
84
+ * @param options - Ver {@link TesseractExtractorOptions}.
85
+ */
86
+ constructor(options: TesseractExtractorOptions);
87
+ /**
88
+ * Configuração do extrator pronta para {@link buildAttachmentKey} (ADR-004).
89
+ * @returns `{ version, model, params }` estáveis desta instância.
90
+ */
91
+ get extractorConfig(): ExtractorConfig;
92
+ /**
93
+ * Roda o OCR sobre `filePath`. Nunca lança: falhas (binário ausente, timeout,
94
+ * erro de execução) viram `{ status: 'failed', metadata.reason }` — degradação
95
+ * graciosa (ADR-002).
96
+ *
97
+ * @param filePath - Caminho absoluto da imagem baixada.
98
+ * @param options - MIME real + logger — ver {@link ExtractOptions}.
99
+ * @returns `done` com `text` no sucesso; `failed` com motivo claro na falha.
100
+ */
101
+ extract(filePath: string, options: ExtractOptions): Promise<ExtractionResult>;
102
+ /**
103
+ * Monta um `ExtractionResult` `failed` com metadados de diagnóstico, sem
104
+ * injetar chaves `undefined` (respeita `exactOptionalPropertyTypes`).
105
+ *
106
+ * @param mime - MIME real detectado (do dispatcher).
107
+ * @param reason - Motivo canônico da falha.
108
+ * @param extra - Metadados adicionais (hint/erro).
109
+ * @returns Resultado `failed` tipado.
110
+ */
111
+ private failed;
112
+ }
113
+ /**
114
+ * Lê a versão do binário via `tesseract --version` (primeira linha:
115
+ * `tesseract X.Y.Z`). Não lança: retorna `undefined` se o binário falhar ou a
116
+ * saída for inesperada.
117
+ *
118
+ * @param bin - Caminho absoluto do binário.
119
+ * @returns A versão semântica detectada (ex.: `5.5.2`), ou `undefined`.
120
+ */
121
+ export declare function detectTesseractVersion(bin: string): Promise<string | undefined>;
122
+ /**
123
+ * Cria um {@link TesseractExtractor} resolvendo binário e versão. Localiza o
124
+ * tesseract ({@link findTesseract}); se presente, lê a versão real do binário e
125
+ * expõe `version = tesseract-<X.Y.Z>`; se ausente (não instalado) ou versão
126
+ * ilegível, usa {@link INTEGRATION_VERSION} e o extrator degrada em `extract`.
127
+ *
128
+ * @param config - Sobrescreve `lang`/`psm`/timeouts — ver {@link TesseractExtractorOptions}.
129
+ * @returns O extrator pronto para registro no {@link ExtractorRegistry}.
130
+ * @example
131
+ * const extractor = await createTesseractExtractor({ lang: 'eng' });
132
+ */
133
+ export declare function createTesseractExtractor(config?: Omit<TesseractExtractorOptions, 'version' | 'binaryPath'>): Promise<TesseractExtractor>;
134
+ /**
135
+ * Cria o registry DEFAULT do pipeline de extração com os extratores de produção
136
+ * registrados: {@link TesseractExtractor} para imagens (OCR), {@link PdfExtractor}
137
+ * para PDF (poppler/pdftotext), {@link AudioExtractor} para áudio (ffmpeg→whisper) e
138
+ * {@link VideoExtractor} para vídeo (ffmpeg→áudio→whisper + keyframe). Ponto único de
139
+ * composição consumido pela fila de jobs e por {@link extractIssueAttachments}.
140
+ *
141
+ * O whisper (#61) NÃO é mais registrado diretamente para áudio cru (era código morto/
142
+ * armadilha — MINOR-2 do gap analysis): ele consome WAV, então áudio e vídeo passam
143
+ * pelos extratores acima, que fazem a conversão ffmpeg → WAV antes da transcrição.
144
+ * Um único {@link WhisperExtractor} é compartilhado como transcritor de ambos, de
145
+ * modo que a identidade de cache (modelo GGUF) seja consistente (ADR-004).
146
+ *
147
+ * @param config - Config repassada ao {@link createTesseractExtractor} (OCR).
148
+ * @returns Um {@link ExtractorRegistry} com os extratores default registrados.
149
+ * @example
150
+ * const registry = await createDefaultRegistry();
151
+ * const result = await dispatchExtraction(filePath, { registry });
152
+ */
153
+ export declare function createDefaultRegistry(config?: Omit<TesseractExtractorOptions, 'version' | 'binaryPath'>): Promise<ExtractorRegistry>;
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Extrator OCR via tesseract (M3-09, ADR-002).
3
+ *
4
+ * Roda o binário `tesseract` sobre uma imagem já baixada no cache e devolve o
5
+ * texto reconhecido num {@link ExtractionResult}. Decisões de segurança/robustez
6
+ * (ADR-002), todas exercitadas por testes:
7
+ *
8
+ * - INVOCAÇÃO SEM SHELL: usa `execFile` (nunca `exec`/shell) com uma lista de
9
+ * argumentos explícita — o `filePath` NUNCA é interpolado numa string de shell,
10
+ * eliminando injeção por nome de arquivo malicioso.
11
+ * - ENV SANITIZADO: o subprocesso recebe um env MÍNIMO e EXPLÍCITO (só `PATH` e,
12
+ * se presente, `TESSDATA_PREFIX`) — segredos do processo pai (ex.:
13
+ * `REDMINE_API_KEY`) NUNCA vazam para o tesseract.
14
+ * - TIMEOUT + KILL: um watchdog envia `SIGTERM` no timeout e, após um período de
15
+ * graça, `SIGKILL` — um tesseract travado não pendura a fila de jobs.
16
+ * - DEGRADAÇÃO GRACIOSA: binário ausente do PATH/locais convencionais NÃO lança;
17
+ * devolve `{ status: 'failed', metadata.reason }` com dica de instalação (o
18
+ * `doctor` da #53 orienta o usuário).
19
+ *
20
+ * A chave de cache do anexo (ADR-004) depende de `version` + `model` + `params`
21
+ * ({@link buildAttachmentKey}); por isso o extrator expõe {@link TesseractExtractor.version}
22
+ * (derivada do binário quando detectável, senão a versão da integração) e
23
+ * {@link TesseractExtractor.params} (inclui `lang`, que participa da identidade).
24
+ */
25
+ import { execFile } from 'node:child_process';
26
+ import { accessSync, constants } from 'node:fs';
27
+ import { delimiter, join } from 'node:path';
28
+ import { createAudioExtractor } from './audio-extractor.js';
29
+ import { ExtractorRegistry } from './dispatcher.js';
30
+ import { createOoxmlExtractor } from './ooxml.js';
31
+ import { createPdfExtractor } from './pdf.js';
32
+ import { runWithWatchdog, sanitizedEnv } from './subprocess.js';
33
+ import { createVideoExtractor } from './video-extractor.js';
34
+ import { createWhisperExtractor } from './whisper-extract.js';
35
+ /** Identificador estável do extrator (entra em metadados). */
36
+ const EXTRACTOR_ID = 'tesseract-ocr';
37
+ /** Modelo lógico para a chave de cache (ADR-004). OCR não versiona "modelo". */
38
+ const EXTRACTOR_MODEL = 'tesseract';
39
+ /**
40
+ * Versão de FALLBACK da integração, usada quando o binário não é detectável (não
41
+ * instalado) e portanto sua versão não pode ser lida. Mantém `version` estável e
42
+ * não-vazia para {@link buildAttachmentKey}.
43
+ */
44
+ const INTEGRATION_VERSION = 'tesseract-integration-1';
45
+ /** Idiomas default do OCR — português + inglês (ADR-002). Configurável. */
46
+ const DEFAULT_LANG = 'por+eng';
47
+ /** Page Segmentation Mode default (3 = automático, sem OSD) — o mais genérico. */
48
+ const DEFAULT_PSM = 3;
49
+ /** Timeout default de uma extração antes do `SIGTERM` (ms). */
50
+ const DEFAULT_TIMEOUT_MS = 30_000;
51
+ /** Graça entre `SIGTERM` e `SIGKILL` (ms) — dá ao tesseract chance de sair limpo. */
52
+ const DEFAULT_KILL_GRACE_MS = 2_000;
53
+ /** Teto do stdout capturado (16 MiB) — OCR de páginas densas cabe com folga. */
54
+ const MAX_BUFFER_BYTES = 16 * 1024 * 1024;
55
+ /** MIMEs REAIS (magic bytes) que o tesseract aceita via leptonica. */
56
+ export const TESSERACT_MIMES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
57
+ /**
58
+ * Locais convencionais do binário `tesseract`, por plataforma — consultados após
59
+ * o `PATH`. Reutilizados pelo `doctor` (#53) via {@link findTesseract}.
60
+ */
61
+ const CONVENTIONAL_UNIX = ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin'];
62
+ const CONVENTIONAL_WINDOWS = ['C:\\Program Files\\Tesseract-OCR', 'C:\\Program Files (x86)\\Tesseract-OCR'];
63
+ /**
64
+ * Verifica se um caminho aponta para um arquivo executável (bit X), sem lançar.
65
+ *
66
+ * @param candidate - Caminho absoluto candidato ao binário.
67
+ * @returns `true` se o arquivo existe e é executável pelo processo atual.
68
+ */
69
+ function isExecutable(candidate) {
70
+ try {
71
+ accessSync(candidate, constants.X_OK);
72
+ return true;
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+ /**
79
+ * Localiza o binário `tesseract` no `PATH` e em locais convencionais por
80
+ * plataforma (`/opt/homebrew/bin`, `/usr/local/bin`, `C:\\Program Files\\Tesseract-OCR`).
81
+ * Função pura e reutilizável pelo `doctor` (#53). Não executa o binário.
82
+ *
83
+ * @returns A localização encontrada, ou `undefined` se não instalado.
84
+ * @example
85
+ * const found = findTesseract();
86
+ * if (found === undefined) logger.warn('tesseract não instalado');
87
+ */
88
+ export function findTesseract() {
89
+ const isWindows = process.platform === 'win32';
90
+ const exe = isWindows ? 'tesseract.exe' : 'tesseract';
91
+ const pathDirs = (process.env.PATH ?? '').split(delimiter).filter((dir) => dir.length > 0);
92
+ const conventional = isWindows ? CONVENTIONAL_WINDOWS : CONVENTIONAL_UNIX;
93
+ for (const dir of [...pathDirs, ...conventional]) {
94
+ const candidate = join(dir, exe);
95
+ if (isExecutable(candidate)) {
96
+ return { path: candidate };
97
+ }
98
+ }
99
+ return undefined;
100
+ }
101
+ /**
102
+ * Env sanitizado do tesseract: o env MÍNIMO compartilhado ({@link sanitizedEnv})
103
+ * mais `TESSDATA_PREFIX` (localização dos `traineddata`) quando definido — o
104
+ * tesseract precisa dele para achar os modelos de idioma. Nenhum segredo do pai é
105
+ * herdado (allowlist explícita, ADR-002).
106
+ *
107
+ * @returns Env sanitizado para o subprocesso tesseract.
108
+ */
109
+ function tesseractEnv() {
110
+ return sanitizedEnv({ allow: ['TESSDATA_PREFIX'] });
111
+ }
112
+ /** Erro interno: o watchdog matou o tesseract por estourar o timeout. */
113
+ class TesseractTimeoutError extends Error {
114
+ timeoutMs;
115
+ constructor(timeoutMs) {
116
+ super(`tesseract excedeu o timeout de ${timeoutMs}ms e foi encerrado`);
117
+ this.timeoutMs = timeoutMs;
118
+ this.name = 'TesseractTimeoutError';
119
+ }
120
+ }
121
+ /**
122
+ * Executa `tesseract <file> stdout -l <lang> --psm <psm>` SEM shell, com env
123
+ * sanitizado e watchdog de timeout (`SIGTERM` → graça → `SIGKILL`).
124
+ *
125
+ * @param options - Ver {@link RunOptions}.
126
+ * @returns O stdout (texto OCR) do tesseract.
127
+ * @throws {TesseractTimeoutError} Se estourar o timeout.
128
+ * @throws {Error} Se o binário falhar (exit != 0, não encontrado em runtime, etc.).
129
+ */
130
+ function runTesseract(options) {
131
+ const { bin, filePath, lang, psm, timeoutMs, killGraceMs, signal } = options;
132
+ const args = [filePath, 'stdout', '-l', lang, '--psm', String(psm)];
133
+ // Delega ao watchdog compartilhado (SEM shell, env sanitizado, SIGTERM → graça →
134
+ // SIGKILL); o estouro de timeout preserva o {@link TesseractTimeoutError} para a
135
+ // classificação de falha (`reason: 'timeout'`). O `signal` (#69/#73) faz o abort
136
+ // MATAR o subprocesso — incluído só quando dado (exactOptionalPropertyTypes).
137
+ return runWithWatchdog({
138
+ bin,
139
+ args,
140
+ env: tesseractEnv(),
141
+ timeoutMs,
142
+ killGraceMs,
143
+ maxBuffer: MAX_BUFFER_BYTES,
144
+ ...(signal !== undefined ? { signal } : {}),
145
+ }, { makeTimeoutError: (ms) => new TesseractTimeoutError(ms) });
146
+ }
147
+ /**
148
+ * Extrator OCR baseado no binário `tesseract` (ADR-002). Implementa
149
+ * {@link Extractor} e, além do contrato, expõe {@link params} e {@link extractorConfig}
150
+ * estáveis para {@link buildAttachmentKey}.
151
+ */
152
+ export class TesseractExtractor {
153
+ id = EXTRACTOR_ID;
154
+ version;
155
+ supportedMimes = TESSERACT_MIMES;
156
+ /** Modelo lógico para a chave de cache (ADR-004). */
157
+ model = EXTRACTOR_MODEL;
158
+ /** Parâmetros escalares estáveis (`lang`, `psm`) — participam da chave de cache. */
159
+ params;
160
+ binaryPath;
161
+ lang;
162
+ psm;
163
+ timeoutMs;
164
+ killGraceMs;
165
+ /**
166
+ * @param options - Ver {@link TesseractExtractorOptions}.
167
+ */
168
+ constructor(options) {
169
+ this.version = options.version;
170
+ this.binaryPath = options.binaryPath;
171
+ this.lang = options.lang ?? DEFAULT_LANG;
172
+ this.psm = options.psm ?? DEFAULT_PSM;
173
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
174
+ this.killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
175
+ this.params = { lang: this.lang, psm: this.psm };
176
+ }
177
+ /**
178
+ * Configuração do extrator pronta para {@link buildAttachmentKey} (ADR-004).
179
+ * @returns `{ version, model, params }` estáveis desta instância.
180
+ */
181
+ get extractorConfig() {
182
+ return { version: this.version, model: this.model, params: this.params };
183
+ }
184
+ /**
185
+ * Roda o OCR sobre `filePath`. Nunca lança: falhas (binário ausente, timeout,
186
+ * erro de execução) viram `{ status: 'failed', metadata.reason }` — degradação
187
+ * graciosa (ADR-002).
188
+ *
189
+ * @param filePath - Caminho absoluto da imagem baixada.
190
+ * @param options - MIME real + logger — ver {@link ExtractOptions}.
191
+ * @returns `done` com `text` no sucesso; `failed` com motivo claro na falha.
192
+ */
193
+ async extract(filePath, options) {
194
+ const bin = this.binaryPath;
195
+ if (bin === undefined) {
196
+ options.logger?.warn('tesseract: binário não encontrado no PATH nem em locais convencionais; ' +
197
+ 'instale o tesseract (ex.: `brew install tesseract`) — veja o doctor');
198
+ return this.failed(options.mime, 'tesseract-nao-instalado', {
199
+ hint: 'instale o tesseract e os traineddata por+eng; o doctor (#53) valida a instalação',
200
+ });
201
+ }
202
+ try {
203
+ const stdout = await runTesseract({
204
+ bin,
205
+ filePath,
206
+ lang: this.lang,
207
+ psm: this.psm,
208
+ timeoutMs: this.timeoutMs,
209
+ killGraceMs: this.killGraceMs,
210
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
211
+ });
212
+ return {
213
+ status: 'done',
214
+ text: stdout.trim(),
215
+ mime: options.mime,
216
+ metadata: { extractorId: this.id, version: this.version, lang: this.lang, psm: this.psm },
217
+ };
218
+ }
219
+ catch (error) {
220
+ const isTimeout = error instanceof TesseractTimeoutError;
221
+ return this.failed(options.mime, isTimeout ? 'timeout' : 'erro-execucao', {
222
+ error: error instanceof Error ? error.message : String(error),
223
+ });
224
+ }
225
+ }
226
+ /**
227
+ * Monta um `ExtractionResult` `failed` com metadados de diagnóstico, sem
228
+ * injetar chaves `undefined` (respeita `exactOptionalPropertyTypes`).
229
+ *
230
+ * @param mime - MIME real detectado (do dispatcher).
231
+ * @param reason - Motivo canônico da falha.
232
+ * @param extra - Metadados adicionais (hint/erro).
233
+ * @returns Resultado `failed` tipado.
234
+ */
235
+ failed(mime, reason, extra) {
236
+ return {
237
+ status: 'failed',
238
+ mime,
239
+ metadata: { extractorId: this.id, version: this.version, reason, ...extra },
240
+ };
241
+ }
242
+ }
243
+ /**
244
+ * Lê a versão do binário via `tesseract --version` (primeira linha:
245
+ * `tesseract X.Y.Z`). Não lança: retorna `undefined` se o binário falhar ou a
246
+ * saída for inesperada.
247
+ *
248
+ * @param bin - Caminho absoluto do binário.
249
+ * @returns A versão semântica detectada (ex.: `5.5.2`), ou `undefined`.
250
+ */
251
+ export function detectTesseractVersion(bin) {
252
+ return new Promise((resolve) => {
253
+ execFile(bin, ['--version'], { env: tesseractEnv(), encoding: 'utf8', windowsHide: true, timeout: DEFAULT_KILL_GRACE_MS }, (error, stdout) => {
254
+ if (error !== null) {
255
+ resolve(undefined);
256
+ return;
257
+ }
258
+ const match = /tesseract\s+(\d+\.\d+\.\d+)/i.exec(stdout);
259
+ resolve(match?.[1]);
260
+ });
261
+ });
262
+ }
263
+ /**
264
+ * Cria um {@link TesseractExtractor} resolvendo binário e versão. Localiza o
265
+ * tesseract ({@link findTesseract}); se presente, lê a versão real do binário e
266
+ * expõe `version = tesseract-<X.Y.Z>`; se ausente (não instalado) ou versão
267
+ * ilegível, usa {@link INTEGRATION_VERSION} e o extrator degrada em `extract`.
268
+ *
269
+ * @param config - Sobrescreve `lang`/`psm`/timeouts — ver {@link TesseractExtractorOptions}.
270
+ * @returns O extrator pronto para registro no {@link ExtractorRegistry}.
271
+ * @example
272
+ * const extractor = await createTesseractExtractor({ lang: 'eng' });
273
+ */
274
+ export async function createTesseractExtractor(config = {}) {
275
+ const found = findTesseract();
276
+ let version = INTEGRATION_VERSION;
277
+ if (found !== undefined) {
278
+ const detected = await detectTesseractVersion(found.path);
279
+ version = detected !== undefined ? `tesseract-${detected}` : INTEGRATION_VERSION;
280
+ }
281
+ return new TesseractExtractor({ ...config, binaryPath: found?.path, version });
282
+ }
283
+ /**
284
+ * Cria o registry DEFAULT do pipeline de extração com os extratores de produção
285
+ * registrados: {@link TesseractExtractor} para imagens (OCR), {@link PdfExtractor}
286
+ * para PDF (poppler/pdftotext), {@link AudioExtractor} para áudio (ffmpeg→whisper) e
287
+ * {@link VideoExtractor} para vídeo (ffmpeg→áudio→whisper + keyframe). Ponto único de
288
+ * composição consumido pela fila de jobs e por {@link extractIssueAttachments}.
289
+ *
290
+ * O whisper (#61) NÃO é mais registrado diretamente para áudio cru (era código morto/
291
+ * armadilha — MINOR-2 do gap analysis): ele consome WAV, então áudio e vídeo passam
292
+ * pelos extratores acima, que fazem a conversão ffmpeg → WAV antes da transcrição.
293
+ * Um único {@link WhisperExtractor} é compartilhado como transcritor de ambos, de
294
+ * modo que a identidade de cache (modelo GGUF) seja consistente (ADR-004).
295
+ *
296
+ * @param config - Config repassada ao {@link createTesseractExtractor} (OCR).
297
+ * @returns Um {@link ExtractorRegistry} com os extratores default registrados.
298
+ * @example
299
+ * const registry = await createDefaultRegistry();
300
+ * const result = await dispatchExtraction(filePath, { registry });
301
+ */
302
+ export async function createDefaultRegistry(config = {}) {
303
+ const registry = new ExtractorRegistry();
304
+ const [tesseract, pdf, whisper] = await Promise.all([
305
+ createTesseractExtractor(config),
306
+ createPdfExtractor(),
307
+ createWhisperExtractor(),
308
+ ]);
309
+ // Áudio e vídeo compartilham o MESMO transcritor whisper: a chave de cache reflete
310
+ // o modelo GGUF por ambos os caminhos (ADR-004). O ffmpeg é resolvido internamente
311
+ // pelos pipelines de conversão (defaults reais; degradam graciosamente se ausente).
312
+ registry.register(tesseract);
313
+ registry.register(pdf);
314
+ registry.register(createAudioExtractor({ transcriber: whisper }));
315
+ registry.register(createVideoExtractor({ transcriber: whisper }));
316
+ // OOXML (docx/pptx/xlsx, #184): 100% local sem binário — registrado para
317
+ // `application/zip` (o magic.ts reporta OOXML como zip; a diferenciação fina é
318
+ // do extrator). Um zip não-OOXML devolve `unsupported`.
319
+ registry.register(createOoxmlExtractor());
320
+ return registry;
321
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Adaptador do pipeline de VÍDEO ao contrato {@link Extractor} (M4-14, #73, ADR-002).
3
+ *
4
+ * O pipeline vídeo→áudio→WAV→whisper + keyframe + limite de duração já existe em
5
+ * {@link extractVideoTranscript} (#63/#64/#65), mas como FUNÇÃO — não plugável no
6
+ * registry. Este módulo o embrulha num {@link Extractor} para que o
7
+ * `dispatchExtraction` roteie contêineres de vídeo (detectados por magic bytes)
8
+ * automaticamente no caminho REAL (`extractIssueAttachments`), fechando a lacuna do
9
+ * gap analysis (vídeo virava `unsupported`).
10
+ *
11
+ * O extrator NÃO reimplementa nada: apenas repassa `filePath`/`mime`/`logger` ao
12
+ * pipeline e injeta (nos testes) as deps de subprocesso (conversão/keyframe/sonda).
13
+ *
14
+ * CACHE (ADR-004): `version`/`model`/`params` são DELEGADOS ao transcritor whisper
15
+ * — a chave attachment-level reflete o modelo GGUF, então trocar o modelo reprocessa
16
+ * e o mesmo modelo é cache-hit (a MESMA invariante já provada pela #72). O
17
+ * `ExtractionResult` devolvido inclui a transcrição (`text`), o keyframe (`artifacts`)
18
+ * e o status corretos, exatamente como o pipeline os produz.
19
+ */
20
+ import type { ExtractorParams } from '../cache/contract.js';
21
+ import type { ExtractionResult } from '../contract.js';
22
+ import type { ExtractOptions, Extractor } from './dispatcher.js';
23
+ import { type ExtractVideoTranscriptOptions } from './video.js';
24
+ /**
25
+ * Overrides injetáveis do pipeline de vídeo — tudo de {@link ExtractVideoTranscriptOptions}
26
+ * exceto o que é resolvido por chamada (`transcriber`/`mime`/`logger`/`signal`). Em
27
+ * produção fica vazio (o pipeline usa ffmpeg/ffprobe reais por default); nos testes
28
+ * carrega conversão/keyframe/sonda falsos para hermetismo. `signal` é omitido porque
29
+ * vem do `ExtractOptions` da chamada (cancelamento, #73), não do pipeline estático.
30
+ */
31
+ export type VideoPipelineOverrides = Omit<ExtractVideoTranscriptOptions, 'transcriber' | 'mime' | 'logger' | 'signal'>;
32
+ /** Opções de construção do {@link VideoExtractor}. */
33
+ export interface VideoExtractorOptions {
34
+ /**
35
+ * Extrator whisper (#61) que transcreve o WAV extraído do vídeo. `version`/`model`/
36
+ * `params` dele definem a identidade de cache deste extrator (ADR-004).
37
+ */
38
+ readonly transcriber: Extractor;
39
+ /** Overrides injetáveis do pipeline (default: vazio → ffmpeg/ffprobe reais). */
40
+ readonly pipeline?: VideoPipelineOverrides;
41
+ }
42
+ /**
43
+ * Extrator de vídeo (ADR-002) que embrulha {@link extractVideoTranscript} no contrato
44
+ * {@link Extractor}. Expõe `version`/`model`/`params` do transcritor para
45
+ * {@link buildAttachmentKey} (ADR-004).
46
+ */
47
+ export declare class VideoExtractor implements Extractor {
48
+ readonly id = "video-transcribe";
49
+ readonly version: string;
50
+ readonly supportedMimes: readonly string[];
51
+ /** Modelo lógico da chave de cache — herdado do whisper (GGUF), fallback no id. */
52
+ readonly model: string;
53
+ /** Parâmetros da chave de cache — herdados do whisper (ex.: `{ language }`). */
54
+ readonly params: ExtractorParams;
55
+ private readonly transcriber;
56
+ private readonly pipeline;
57
+ /**
58
+ * @param options - Ver {@link VideoExtractorOptions}.
59
+ */
60
+ constructor(options: VideoExtractorOptions);
61
+ /**
62
+ * Extrai a transcrição (e o keyframe) de um vídeo em `filePath`, delegando ao
63
+ * pipeline {@link extractVideoTranscript}. Nunca lança (o pipeline é gracioso por
64
+ * contrato, ADR-002): binário/modelo ausente, vídeo sem áudio, duração acima do
65
+ * limite ou timeout viram `failed`/`skipped` com motivo.
66
+ *
67
+ * @param filePath - Caminho absoluto do vídeo baixado no cache.
68
+ * @param options - MIME real + logger — ver {@link ExtractOptions}.
69
+ * @returns O {@link ExtractionResult} com `text` (transcrição), `artifacts`
70
+ * (keyframe) e status corretos.
71
+ */
72
+ extract(filePath: string, options: ExtractOptions): Promise<ExtractionResult>;
73
+ }
74
+ /**
75
+ * Cria um {@link VideoExtractor} a partir de um transcritor whisper. Fino wrapper de
76
+ * composição — ponto de entrada consumido por {@link createDefaultRegistry}.
77
+ *
78
+ * @param options - Transcritor + overrides do pipeline — ver {@link VideoExtractorOptions}.
79
+ * @returns O extrator pronto para registro no {@link ExtractorRegistry}.
80
+ * @example
81
+ * const whisper = await createWhisperExtractor();
82
+ * const video = createVideoExtractor({ transcriber: whisper });
83
+ */
84
+ export declare function createVideoExtractor(options: VideoExtractorOptions): VideoExtractor;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Adaptador do pipeline de VÍDEO ao contrato {@link Extractor} (M4-14, #73, ADR-002).
3
+ *
4
+ * O pipeline vídeo→áudio→WAV→whisper + keyframe + limite de duração já existe em
5
+ * {@link extractVideoTranscript} (#63/#64/#65), mas como FUNÇÃO — não plugável no
6
+ * registry. Este módulo o embrulha num {@link Extractor} para que o
7
+ * `dispatchExtraction` roteie contêineres de vídeo (detectados por magic bytes)
8
+ * automaticamente no caminho REAL (`extractIssueAttachments`), fechando a lacuna do
9
+ * gap analysis (vídeo virava `unsupported`).
10
+ *
11
+ * O extrator NÃO reimplementa nada: apenas repassa `filePath`/`mime`/`logger` ao
12
+ * pipeline e injeta (nos testes) as deps de subprocesso (conversão/keyframe/sonda).
13
+ *
14
+ * CACHE (ADR-004): `version`/`model`/`params` são DELEGADOS ao transcritor whisper
15
+ * — a chave attachment-level reflete o modelo GGUF, então trocar o modelo reprocessa
16
+ * e o mesmo modelo é cache-hit (a MESMA invariante já provada pela #72). O
17
+ * `ExtractionResult` devolvido inclui a transcrição (`text`), o keyframe (`artifacts`)
18
+ * e o status corretos, exatamente como o pipeline os produz.
19
+ */
20
+ import { extractVideoTranscript, VIDEO_MIMES } from './video.js';
21
+ /** Identificador estável do extrator (coincide com o `extractorId` dos metadados do pipeline). */
22
+ const EXTRACTOR_ID = 'video-transcribe';
23
+ /**
24
+ * Extrator de vídeo (ADR-002) que embrulha {@link extractVideoTranscript} no contrato
25
+ * {@link Extractor}. Expõe `version`/`model`/`params` do transcritor para
26
+ * {@link buildAttachmentKey} (ADR-004).
27
+ */
28
+ export class VideoExtractor {
29
+ id = EXTRACTOR_ID;
30
+ version;
31
+ supportedMimes = VIDEO_MIMES;
32
+ /** Modelo lógico da chave de cache — herdado do whisper (GGUF), fallback no id. */
33
+ model;
34
+ /** Parâmetros da chave de cache — herdados do whisper (ex.: `{ language }`). */
35
+ params;
36
+ transcriber;
37
+ pipeline;
38
+ /**
39
+ * @param options - Ver {@link VideoExtractorOptions}.
40
+ */
41
+ constructor(options) {
42
+ this.transcriber = options.transcriber;
43
+ this.version = options.transcriber.version;
44
+ // Reason (ADR-004): identidade de cache = a do whisper (trocar GGUF reprocessa).
45
+ this.model = options.transcriber.model ?? EXTRACTOR_ID;
46
+ this.params = options.transcriber.params ?? {};
47
+ // NOTA (gap #73, MINOR-1): o limite de duração (`maxDurationSeconds`, #65) NÃO
48
+ // entra na chave de cache. Hoje é um default constante sem superfície de config,
49
+ // então mudá-lo não é alcançável pelo usuário. Se o limite virar configurável,
50
+ // inclua-o aqui nos `params` (ou invalide o cache ao alterá-lo) — senão um vídeo
51
+ // antes `skipped` por exceder o limite permaneceria `skipped` no cache.
52
+ this.pipeline = options.pipeline ?? {};
53
+ }
54
+ /**
55
+ * Extrai a transcrição (e o keyframe) de um vídeo em `filePath`, delegando ao
56
+ * pipeline {@link extractVideoTranscript}. Nunca lança (o pipeline é gracioso por
57
+ * contrato, ADR-002): binário/modelo ausente, vídeo sem áudio, duração acima do
58
+ * limite ou timeout viram `failed`/`skipped` com motivo.
59
+ *
60
+ * @param filePath - Caminho absoluto do vídeo baixado no cache.
61
+ * @param options - MIME real + logger — ver {@link ExtractOptions}.
62
+ * @returns O {@link ExtractionResult} com `text` (transcrição), `artifacts`
63
+ * (keyframe) e status corretos.
64
+ */
65
+ extract(filePath, options) {
66
+ return extractVideoTranscript(filePath, {
67
+ transcriber: this.transcriber,
68
+ mime: options.mime,
69
+ ...this.pipeline,
70
+ ...(options.logger !== undefined ? { logger: options.logger } : {}),
71
+ // Cancelamento (#69/#73): o abort do dispatch alcança conversão/whisper/keyframe
72
+ // do pipeline. Só inclui quando dado (exactOptionalPropertyTypes).
73
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
74
+ });
75
+ }
76
+ }
77
+ /**
78
+ * Cria um {@link VideoExtractor} a partir de um transcritor whisper. Fino wrapper de
79
+ * composição — ponto de entrada consumido por {@link createDefaultRegistry}.
80
+ *
81
+ * @param options - Transcritor + overrides do pipeline — ver {@link VideoExtractorOptions}.
82
+ * @returns O extrator pronto para registro no {@link ExtractorRegistry}.
83
+ * @example
84
+ * const whisper = await createWhisperExtractor();
85
+ * const video = createVideoExtractor({ transcriber: whisper });
86
+ */
87
+ export function createVideoExtractor(options) {
88
+ return new VideoExtractor(options);
89
+ }