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