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.
- package/LICENSE +21 -0
- package/README.md +449 -0
- package/dist/bundle/index.d.ts +5 -0
- package/dist/bundle/index.js +5 -0
- package/dist/bundle/json.d.ts +90 -0
- package/dist/bundle/json.js +266 -0
- package/dist/bundle/markdown.d.ts +75 -0
- package/dist/bundle/markdown.js +294 -0
- package/dist/bundle/search-list.d.ts +43 -0
- package/dist/bundle/search-list.js +53 -0
- package/dist/bundle/stable-stringify.d.ts +26 -0
- package/dist/bundle/stable-stringify.js +50 -0
- package/dist/cache/contract.d.ts +157 -0
- package/dist/cache/contract.js +0 -0
- package/dist/cache/disk-index.d.ts +82 -0
- package/dist/cache/disk-index.js +220 -0
- package/dist/cache/disk.d.ts +133 -0
- package/dist/cache/disk.js +313 -0
- package/dist/cache/gc.d.ts +78 -0
- package/dist/cache/gc.js +123 -0
- package/dist/cache/get-or-compute.d.ts +36 -0
- package/dist/cache/get-or-compute.js +52 -0
- package/dist/cache/index.d.ts +9 -0
- package/dist/cache/index.js +8 -0
- package/dist/cache/keys.d.ts +76 -0
- package/dist/cache/keys.js +78 -0
- package/dist/cache/memory.d.ts +48 -0
- package/dist/cache/memory.js +110 -0
- package/dist/cache-first.d.ts +127 -0
- package/dist/cache-first.js +227 -0
- package/dist/client/errors.d.ts +33 -0
- package/dist/client/errors.js +49 -0
- package/dist/client/http.d.ts +110 -0
- package/dist/client/http.js +207 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +5 -0
- package/dist/client/issues.d.ts +71 -0
- package/dist/client/issues.js +100 -0
- package/dist/client/search.d.ts +58 -0
- package/dist/client/search.js +81 -0
- package/dist/config/credentials.d.ts +247 -0
- package/dist/config/credentials.js +427 -0
- package/dist/config/doctor.d.ts +123 -0
- package/dist/config/doctor.js +260 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.js +6 -0
- package/dist/config/keyring.d.ts +96 -0
- package/dist/config/keyring.js +158 -0
- package/dist/config/login.d.ts +97 -0
- package/dist/config/login.js +189 -0
- package/dist/config/settings.d.ts +94 -0
- package/dist/config/settings.js +140 -0
- package/dist/contract.d.ts +173 -0
- package/dist/contract.js +27 -0
- package/dist/core.d.ts +1 -0
- package/dist/core.js +8 -0
- package/dist/extract/audio-extractor.d.ts +105 -0
- package/dist/extract/audio-extractor.js +156 -0
- package/dist/extract/audio.d.ts +126 -0
- package/dist/extract/audio.js +184 -0
- package/dist/extract/dispatcher.d.ts +132 -0
- package/dist/extract/dispatcher.js +115 -0
- package/dist/extract/download.d.ts +111 -0
- package/dist/extract/download.js +261 -0
- package/dist/extract/duration.d.ts +106 -0
- package/dist/extract/duration.js +148 -0
- package/dist/extract/ffmpeg.d.ts +56 -0
- package/dist/extract/ffmpeg.js +95 -0
- package/dist/extract/gguf.d.ts +137 -0
- package/dist/extract/gguf.js +215 -0
- package/dist/extract/index.d.ts +19 -0
- package/dist/extract/index.js +19 -0
- package/dist/extract/magic.d.ts +80 -0
- package/dist/extract/magic.js +282 -0
- package/dist/extract/ooxml.d.ts +131 -0
- package/dist/extract/ooxml.js +336 -0
- package/dist/extract/pdf.d.ts +147 -0
- package/dist/extract/pdf.js +322 -0
- package/dist/extract/queue.d.ts +167 -0
- package/dist/extract/queue.js +217 -0
- package/dist/extract/subprocess.d.ts +145 -0
- package/dist/extract/subprocess.js +181 -0
- package/dist/extract/tesseract.d.ts +153 -0
- package/dist/extract/tesseract.js +321 -0
- package/dist/extract/video-extractor.d.ts +84 -0
- package/dist/extract/video-extractor.js +89 -0
- package/dist/extract/video.d.ts +198 -0
- package/dist/extract/video.js +418 -0
- package/dist/extract/which.d.ts +63 -0
- package/dist/extract/which.js +72 -0
- package/dist/extract/whisper-extract.d.ts +211 -0
- package/dist/extract/whisper-extract.js +323 -0
- package/dist/extract/whisper.d.ts +50 -0
- package/dist/extract/whisper.js +67 -0
- package/dist/extract/zip.d.ts +50 -0
- package/dist/extract/zip.js +165 -0
- package/dist/extract-issue-attachments.d.ts +82 -0
- package/dist/extract-issue-attachments.js +156 -0
- package/dist/fetch-attachment-text.d.ts +113 -0
- package/dist/fetch-attachment-text.js +155 -0
- package/dist/fetch-issue-bundle.d.ts +81 -0
- package/dist/fetch-issue-bundle.js +93 -0
- package/dist/fetch-issue-search.d.ts +74 -0
- package/dist/fetch-issue-search.js +120 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +67 -0
- package/dist/normalize/collections.d.ts +52 -0
- package/dist/normalize/collections.js +170 -0
- package/dist/normalize/helpers.d.ts +35 -0
- package/dist/normalize/helpers.js +59 -0
- package/dist/normalize/index.d.ts +2 -0
- package/dist/normalize/index.js +2 -0
- package/dist/normalize/issue.d.ts +34 -0
- package/dist/normalize/issue.js +154 -0
- package/dist/surfaces/cli/commands.d.ts +61 -0
- package/dist/surfaces/cli/commands.js +262 -0
- package/dist/surfaces/cli/main.d.ts +32 -0
- package/dist/surfaces/cli/main.js +210 -0
- package/dist/surfaces/cli/prompts.d.ts +66 -0
- package/dist/surfaces/cli/prompts.js +147 -0
- package/dist/surfaces/cli/tty.d.ts +27 -0
- package/dist/surfaces/cli/tty.js +35 -0
- package/dist/surfaces/cli/types.d.ts +39 -0
- package/dist/surfaces/cli/types.js +7 -0
- package/dist/surfaces/mcp/server.d.ts +171 -0
- package/dist/surfaces/mcp/server.js +427 -0
- package/dist/surfaces/tui/app.d.ts +55 -0
- package/dist/surfaces/tui/app.js +180 -0
- package/dist/surfaces/tui/attachment-status.d.ts +79 -0
- package/dist/surfaces/tui/attachment-status.js +113 -0
- package/dist/surfaces/tui/components/breadcrumb.d.ts +7 -0
- package/dist/surfaces/tui/components/breadcrumb.js +31 -0
- package/dist/surfaces/tui/components/gradient-text.d.ts +23 -0
- package/dist/surfaces/tui/components/gradient-text.js +75 -0
- package/dist/surfaces/tui/components/scroll-view.d.ts +25 -0
- package/dist/surfaces/tui/components/scroll-view.js +77 -0
- package/dist/surfaces/tui/components/spinner.d.ts +12 -0
- package/dist/surfaces/tui/components/spinner.js +39 -0
- package/dist/surfaces/tui/components/text-input.d.ts +45 -0
- package/dist/surfaces/tui/components/text-input.js +114 -0
- package/dist/surfaces/tui/format-file-size.d.ts +24 -0
- package/dist/surfaces/tui/format-file-size.js +43 -0
- package/dist/surfaces/tui/glyphs.d.ts +47 -0
- package/dist/surfaces/tui/glyphs.js +84 -0
- package/dist/surfaces/tui/hooks/use-auth-guard.d.ts +39 -0
- package/dist/surfaces/tui/hooks/use-auth-guard.js +135 -0
- package/dist/surfaces/tui/hooks/use-doctor-status.d.ts +64 -0
- package/dist/surfaces/tui/hooks/use-doctor-status.js +123 -0
- package/dist/surfaces/tui/hooks/use-escape-interceptor.d.ts +25 -0
- package/dist/surfaces/tui/hooks/use-escape-interceptor.js +65 -0
- package/dist/surfaces/tui/hooks/use-exit-guard.d.ts +18 -0
- package/dist/surfaces/tui/hooks/use-exit-guard.js +66 -0
- package/dist/surfaces/tui/hooks/use-export-bundle.d.ts +62 -0
- package/dist/surfaces/tui/hooks/use-export-bundle.js +100 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +61 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.js +132 -0
- package/dist/surfaces/tui/hooks/use-issue-search.d.ts +71 -0
- package/dist/surfaces/tui/hooks/use-issue-search.js +168 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +44 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.js +82 -0
- package/dist/surfaces/tui/hooks/use-media-binaries.d.ts +24 -0
- package/dist/surfaces/tui/hooks/use-media-binaries.js +44 -0
- package/dist/surfaces/tui/hooks/use-my-issues.d.ts +66 -0
- package/dist/surfaces/tui/hooks/use-my-issues.js +151 -0
- package/dist/surfaces/tui/hooks/use-onboarding-callbacks.d.ts +11 -0
- package/dist/surfaces/tui/hooks/use-onboarding-callbacks.js +104 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +52 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.js +90 -0
- package/dist/surfaces/tui/index.d.ts +50 -0
- package/dist/surfaces/tui/index.js +158 -0
- package/dist/surfaces/tui/instance.d.ts +37 -0
- package/dist/surfaces/tui/instance.js +36 -0
- package/dist/surfaces/tui/job-registry.d.ts +113 -0
- package/dist/surfaces/tui/job-registry.js +123 -0
- package/dist/surfaces/tui/job-status.d.ts +45 -0
- package/dist/surfaces/tui/job-status.js +81 -0
- package/dist/surfaces/tui/navigation.d.ts +74 -0
- package/dist/surfaces/tui/navigation.js +87 -0
- package/dist/surfaces/tui/palettes.d.ts +38 -0
- package/dist/surfaces/tui/palettes.js +244 -0
- package/dist/surfaces/tui/screen.d.ts +30 -0
- package/dist/surfaces/tui/screen.js +51 -0
- package/dist/surfaces/tui/screens/about.d.ts +2 -0
- package/dist/surfaces/tui/screens/about.js +35 -0
- package/dist/surfaces/tui/screens/appearance.d.ts +2 -0
- package/dist/surfaces/tui/screens/appearance.js +74 -0
- package/dist/surfaces/tui/screens/config.d.ts +2 -0
- package/dist/surfaces/tui/screens/config.js +82 -0
- package/dist/surfaces/tui/screens/doctor.d.ts +2 -0
- package/dist/surfaces/tui/screens/doctor.js +109 -0
- package/dist/surfaces/tui/screens/export.d.ts +2 -0
- package/dist/surfaces/tui/screens/export.js +168 -0
- package/dist/surfaces/tui/screens/home-selection.d.ts +70 -0
- package/dist/surfaces/tui/screens/home-selection.js +80 -0
- package/dist/surfaces/tui/screens/home.d.ts +2 -0
- package/dist/surfaces/tui/screens/home.js +200 -0
- package/dist/surfaces/tui/screens/issue-detail.d.ts +6 -0
- package/dist/surfaces/tui/screens/issue-detail.js +182 -0
- package/dist/surfaces/tui/screens/jobs.d.ts +7 -0
- package/dist/surfaces/tui/screens/jobs.js +89 -0
- package/dist/surfaces/tui/screens/loaded-issue-context.d.ts +49 -0
- package/dist/surfaces/tui/screens/loaded-issue-context.js +57 -0
- package/dist/surfaces/tui/screens/onboarding/api-key.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/api-key.js +69 -0
- package/dist/surfaces/tui/screens/onboarding/login.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/login.js +50 -0
- package/dist/surfaces/tui/screens/onboarding/mode.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/mode.js +42 -0
- package/dist/surfaces/tui/screens/onboarding/onboarding-context.d.ts +221 -0
- package/dist/surfaces/tui/screens/onboarding/onboarding-context.js +131 -0
- package/dist/surfaces/tui/screens/onboarding/success.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/success.js +41 -0
- package/dist/surfaces/tui/screens/onboarding/url.d.ts +24 -0
- package/dist/surfaces/tui/screens/onboarding/url.js +84 -0
- package/dist/surfaces/tui/screens/onboarding/validating.d.ts +1 -0
- package/dist/surfaces/tui/screens/onboarding/validating.js +86 -0
- package/dist/surfaces/tui/screens/welcome.d.ts +6 -0
- package/dist/surfaces/tui/screens/welcome.js +95 -0
- package/dist/surfaces/tui/status-color.d.ts +21 -0
- package/dist/surfaces/tui/status-color.js +23 -0
- package/dist/surfaces/tui/symbols.d.ts +231 -0
- package/dist/surfaces/tui/symbols.js +14 -0
- package/dist/surfaces/tui/terminal-colors.d.ts +29 -0
- package/dist/surfaces/tui/terminal-colors.js +39 -0
- package/dist/surfaces/tui/theme.d.ts +156 -0
- package/dist/surfaces/tui/theme.js +86 -0
- package/dist/surfaces/tui/truncate.d.ts +31 -0
- package/dist/surfaces/tui/truncate.js +81 -0
- 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
|
+
}
|