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,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sonda de DURAÇÃO de vídeo via `ffprobe` (M4-09, #65, ADR-002).
|
|
3
|
+
*
|
|
4
|
+
* Mede a duração de um vídeo ANTES de convertê-lo/transcrevê-lo, para que o
|
|
5
|
+
* pipeline ({@link extractVideoTranscript}) possa PULAR com aviso os vídeos que
|
|
6
|
+
* excedem o limite configurável (default 20 min, ADR-002) — sem gastar CPU na
|
|
7
|
+
* conversão/transcrição de mídia longa.
|
|
8
|
+
*
|
|
9
|
+
* Extraído de `video.ts` para manter cada módulo sob o limite de tamanho e com um
|
|
10
|
+
* propósito único (Rule #24): aqui vive SÓ a sondagem de duração; a orquestração
|
|
11
|
+
* (skip + preservação do keyframe) permanece em `video.ts`.
|
|
12
|
+
*
|
|
13
|
+
* DECISÕES DE SEGURANÇA/ROBUSTEZ (ADR-002), como no restante da milestone:
|
|
14
|
+
* - INVOCAÇÃO SEM SHELL: reusa {@link runWithWatchdog} (`execFile`), lista de args
|
|
15
|
+
* explícita — o `inputPath` NUNCA é interpolado numa string de shell.
|
|
16
|
+
* - `-protocol_whitelist file`: o ffprobe só pode abrir o arquivo local dado, nunca
|
|
17
|
+
* protocolos remotos embutidos em playlists/manifests hostis.
|
|
18
|
+
* - ENV SANITIZADO: só `PATH` — segredos do processo pai não vazam para o ffprobe.
|
|
19
|
+
* - TIMEOUT + KILL: watchdog `SIGTERM`→ graça →`SIGKILL`.
|
|
20
|
+
* - DEGRADAÇÃO GRACIOSA: ffprobe ausente, exit != 0, timeout ou saída não-parseável
|
|
21
|
+
* NÃO lançam; devolvem `{ status: 'unavailable', reason }` — o chamador decide o
|
|
22
|
+
* comportamento seguro (o pipeline PROSSEGUE, não bloqueia).
|
|
23
|
+
*
|
|
24
|
+
* Fronteira: módulo de core (pipeline de extração) — sem `console.*` (ADR-005).
|
|
25
|
+
*/
|
|
26
|
+
import type { Logger } from '../client/index.js';
|
|
27
|
+
/** Invocação concreta do `ffprobe` passada ao {@link FfprobeRunner}. */
|
|
28
|
+
export interface FfprobeInvocation {
|
|
29
|
+
/** Caminho absoluto do binário `ffprobe` já resolvido. */
|
|
30
|
+
readonly bin: string;
|
|
31
|
+
/** Argumentos do ffprobe (sem shell), incluindo `-protocol_whitelist file`. */
|
|
32
|
+
readonly args: readonly string[];
|
|
33
|
+
/** Env SANITIZADO do subprocesso (só `PATH`). */
|
|
34
|
+
readonly env: NodeJS.ProcessEnv;
|
|
35
|
+
/** Timeout antes do `SIGTERM` (ms). */
|
|
36
|
+
readonly timeoutMs: number;
|
|
37
|
+
/** Graça `SIGTERM` → `SIGKILL` (ms). */
|
|
38
|
+
readonly killGraceMs: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Executor injetável do subprocesso ffprobe. Resolve com o `stdout` (a duração
|
|
42
|
+
* crua) no sucesso (exit 0); rejeita em falha — com um erro de nome
|
|
43
|
+
* `FfprobeTimeoutError` para sinalizar estouro de timeout.
|
|
44
|
+
*/
|
|
45
|
+
export type FfprobeRunner = (invocation: FfprobeInvocation) => Promise<string>;
|
|
46
|
+
/** Sonda de duração bem-sucedida: duração do container em segundos. */
|
|
47
|
+
export interface DurationProbeOk {
|
|
48
|
+
readonly status: 'ok';
|
|
49
|
+
/** Duração total do vídeo em segundos (float), como reportada pelo ffprobe. */
|
|
50
|
+
readonly seconds: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Sonda de duração INDISPONÍVEL (graciosa, ADR-002): o ffprobe está ausente, falhou,
|
|
54
|
+
* estourou o timeout ou devolveu uma duração não-parseável. O chamador decide o
|
|
55
|
+
* comportamento seguro — o pipeline PROSSEGUE sem bloquear (ver {@link extractVideoTranscript}).
|
|
56
|
+
*/
|
|
57
|
+
export interface DurationProbeUnavailable {
|
|
58
|
+
readonly status: 'unavailable';
|
|
59
|
+
/** Motivo canônico (`ffprobe-nao-instalado` | `duracao-indisponivel` | `erro-ffprobe` | `timeout`). */
|
|
60
|
+
readonly reason: string;
|
|
61
|
+
/** Mensagem de erro subjacente, quando houver. */
|
|
62
|
+
readonly error?: string;
|
|
63
|
+
}
|
|
64
|
+
/** Resultado tipado da sonda de duração via ffprobe. */
|
|
65
|
+
export type DurationProbeResult = DurationProbeOk | DurationProbeUnavailable;
|
|
66
|
+
/** Opções (todas injetáveis para testes herméticos) de {@link probeVideoDuration}. */
|
|
67
|
+
export interface ProbeVideoDurationOptions {
|
|
68
|
+
/** Localizador do `ffprobe`; default {@link findFfprobe}. */
|
|
69
|
+
readonly findFfprobeBinary?: () => string | undefined;
|
|
70
|
+
/** Executor do subprocesso; default: watchdog real com `execFile`. */
|
|
71
|
+
readonly run?: FfprobeRunner;
|
|
72
|
+
/** Timeout antes do `SIGTERM` (ms); default {@link DEFAULT_PROBE_TIMEOUT_MS}. */
|
|
73
|
+
readonly timeoutMs?: number;
|
|
74
|
+
/** Graça `SIGTERM` → `SIGKILL` (ms); default {@link PROBE_KILL_GRACE_MS}. */
|
|
75
|
+
readonly killGraceMs?: number;
|
|
76
|
+
/** Logger para o aviso de binário ausente; sem default de lib (ADR-003). */
|
|
77
|
+
readonly logger?: Logger;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Parseia a duração (segundos, float) da saída crua do `ffprobe`. Devolve
|
|
81
|
+
* `undefined` para saída vazia, `N/A`, não-numérica ou negativa — sinal de que a
|
|
82
|
+
* duração é INDISPONÍVEL (não presumimos zero nem crashamos).
|
|
83
|
+
*
|
|
84
|
+
* @param stdout - Saída crua do ffprobe (`format=duration`, uma linha).
|
|
85
|
+
* @returns A duração em segundos, ou `undefined` se não parseável.
|
|
86
|
+
* @example
|
|
87
|
+
* parseFfprobeDuration('123.45\n'); // 123.45
|
|
88
|
+
* parseFfprobeDuration('N/A'); // undefined
|
|
89
|
+
*/
|
|
90
|
+
export declare function parseFfprobeDuration(stdout: string): number | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Mede a duração de um vídeo via `ffprobe` ANTES de convertê-lo/transcrevê-lo
|
|
93
|
+
* (M4-09, #65). REUSA o subprocesso seguro compartilhado (sem shell,
|
|
94
|
+
* `-protocol_whitelist file`, env sanitizado, watchdog `SIGTERM`→`SIGKILL`). NUNCA
|
|
95
|
+
* lança (degradação graciosa, ADR-002): ffprobe ausente, exit != 0, timeout ou
|
|
96
|
+
* saída não-parseável viram `{ status: 'unavailable', reason }` — cabe ao chamador
|
|
97
|
+
* decidir o comportamento seguro (o pipeline PROSSEGUE quando a duração é indisponível).
|
|
98
|
+
*
|
|
99
|
+
* @param inputPath - Caminho absoluto do vídeo já baixado no cache.
|
|
100
|
+
* @param options - Deps injetáveis + timeouts + logger — ver {@link ProbeVideoDurationOptions}.
|
|
101
|
+
* @returns `ok` com `seconds` no sucesso; `unavailable` com motivo caso contrário.
|
|
102
|
+
* @example
|
|
103
|
+
* const probe = await probeVideoDuration('/cache/att/clip.mp4');
|
|
104
|
+
* if (probe.status === 'ok' && probe.seconds > 1200) skipTranscription();
|
|
105
|
+
*/
|
|
106
|
+
export declare function probeVideoDuration(inputPath: string, options?: ProbeVideoDurationOptions): Promise<DurationProbeResult>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sonda de DURAÇÃO de vídeo via `ffprobe` (M4-09, #65, ADR-002).
|
|
3
|
+
*
|
|
4
|
+
* Mede a duração de um vídeo ANTES de convertê-lo/transcrevê-lo, para que o
|
|
5
|
+
* pipeline ({@link extractVideoTranscript}) possa PULAR com aviso os vídeos que
|
|
6
|
+
* excedem o limite configurável (default 20 min, ADR-002) — sem gastar CPU na
|
|
7
|
+
* conversão/transcrição de mídia longa.
|
|
8
|
+
*
|
|
9
|
+
* Extraído de `video.ts` para manter cada módulo sob o limite de tamanho e com um
|
|
10
|
+
* propósito único (Rule #24): aqui vive SÓ a sondagem de duração; a orquestração
|
|
11
|
+
* (skip + preservação do keyframe) permanece em `video.ts`.
|
|
12
|
+
*
|
|
13
|
+
* DECISÕES DE SEGURANÇA/ROBUSTEZ (ADR-002), como no restante da milestone:
|
|
14
|
+
* - INVOCAÇÃO SEM SHELL: reusa {@link runWithWatchdog} (`execFile`), lista de args
|
|
15
|
+
* explícita — o `inputPath` NUNCA é interpolado numa string de shell.
|
|
16
|
+
* - `-protocol_whitelist file`: o ffprobe só pode abrir o arquivo local dado, nunca
|
|
17
|
+
* protocolos remotos embutidos em playlists/manifests hostis.
|
|
18
|
+
* - ENV SANITIZADO: só `PATH` — segredos do processo pai não vazam para o ffprobe.
|
|
19
|
+
* - TIMEOUT + KILL: watchdog `SIGTERM`→ graça →`SIGKILL`.
|
|
20
|
+
* - DEGRADAÇÃO GRACIOSA: ffprobe ausente, exit != 0, timeout ou saída não-parseável
|
|
21
|
+
* NÃO lançam; devolvem `{ status: 'unavailable', reason }` — o chamador decide o
|
|
22
|
+
* comportamento seguro (o pipeline PROSSEGUE, não bloqueia).
|
|
23
|
+
*
|
|
24
|
+
* Fronteira: módulo de core (pipeline de extração) — sem `console.*` (ADR-005).
|
|
25
|
+
*/
|
|
26
|
+
import { findFfprobe } from './ffmpeg.js';
|
|
27
|
+
import { runWithWatchdog, sanitizedEnv } from './subprocess.js';
|
|
28
|
+
/** Timeout default da sonda `ffprobe` antes do `SIGTERM` (ms) — a leitura é rápida. */
|
|
29
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 15_000;
|
|
30
|
+
/** Graça `SIGTERM` → `SIGKILL` da sonda `ffprobe` (ms). */
|
|
31
|
+
const PROBE_KILL_GRACE_MS = 2_000;
|
|
32
|
+
/** Teto do stdout/stderr do ffprobe (1 MiB) — a saída é uma única linha (a duração). */
|
|
33
|
+
const PROBE_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
34
|
+
/** Erro interno: o watchdog matou o ffprobe por estourar o timeout. */
|
|
35
|
+
class FfprobeTimeoutError extends Error {
|
|
36
|
+
timeoutMs;
|
|
37
|
+
constructor(timeoutMs) {
|
|
38
|
+
super(`ffprobe excedeu o timeout de ${timeoutMs}ms e foi encerrado`);
|
|
39
|
+
this.timeoutMs = timeoutMs;
|
|
40
|
+
this.name = 'FfprobeTimeoutError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Monta os argumentos do `ffprobe` para ler SÓ a duração do container em `inputPath`,
|
|
45
|
+
* como um número cru em segundos. `-protocol_whitelist file` precede o arquivo de
|
|
46
|
+
* entrada (ADR-002): o ffprobe só pode abrir o arquivo local dado, nunca protocolos
|
|
47
|
+
* remotos embutidos em playlists/manifests hostis. `-of default=nw=1:nk=1` imprime o
|
|
48
|
+
* valor sem chave nem cabeçalho de seção — só o número.
|
|
49
|
+
*
|
|
50
|
+
* @param inputPath - Caminho absoluto do vídeo de entrada.
|
|
51
|
+
* @returns Lista de argumentos, na ordem exigida pelo ffprobe.
|
|
52
|
+
*/
|
|
53
|
+
function buildFfprobeArgs(inputPath) {
|
|
54
|
+
return [
|
|
55
|
+
'-v',
|
|
56
|
+
'error', // silencioso, exceto erros
|
|
57
|
+
'-protocol_whitelist',
|
|
58
|
+
'file', // ADR-002: só o arquivo local dado, nada remoto
|
|
59
|
+
'-show_entries',
|
|
60
|
+
'format=duration', // só a duração do container
|
|
61
|
+
'-of',
|
|
62
|
+
'default=nw=1:nk=1', // saída crua: sem wrapper, sem chave
|
|
63
|
+
inputPath,
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Executor default da sonda: delega ao watchdog compartilhado
|
|
68
|
+
* ({@link runWithWatchdog}) — ffprobe SEM shell, env sanitizado e escalonamento
|
|
69
|
+
* `SIGTERM` → graça → `SIGKILL`. Diferente do ffmpeg, aqui o `stdout` IMPORTA (é a
|
|
70
|
+
* duração). O estouro de timeout é sinalizado com {@link FfprobeTimeoutError}.
|
|
71
|
+
*
|
|
72
|
+
* @param invocation - Ver {@link FfprobeInvocation}.
|
|
73
|
+
* @returns O `stdout` do ffprobe (a duração crua) no sucesso.
|
|
74
|
+
*/
|
|
75
|
+
function defaultProbeRun(invocation) {
|
|
76
|
+
const { bin, args, env, timeoutMs, killGraceMs } = invocation;
|
|
77
|
+
return runWithWatchdog({ bin, args, env, timeoutMs, killGraceMs, maxBuffer: PROBE_MAX_BUFFER_BYTES }, { makeTimeoutError: (ms) => new FfprobeTimeoutError(ms) });
|
|
78
|
+
}
|
|
79
|
+
/** `true` se o erro sinaliza estouro de timeout da sonda (por nome, robusto a DI). */
|
|
80
|
+
function isProbeTimeout(error) {
|
|
81
|
+
return error instanceof Error && error.name === 'FfprobeTimeoutError';
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Parseia a duração (segundos, float) da saída crua do `ffprobe`. Devolve
|
|
85
|
+
* `undefined` para saída vazia, `N/A`, não-numérica ou negativa — sinal de que a
|
|
86
|
+
* duração é INDISPONÍVEL (não presumimos zero nem crashamos).
|
|
87
|
+
*
|
|
88
|
+
* @param stdout - Saída crua do ffprobe (`format=duration`, uma linha).
|
|
89
|
+
* @returns A duração em segundos, ou `undefined` se não parseável.
|
|
90
|
+
* @example
|
|
91
|
+
* parseFfprobeDuration('123.45\n'); // 123.45
|
|
92
|
+
* parseFfprobeDuration('N/A'); // undefined
|
|
93
|
+
*/
|
|
94
|
+
export function parseFfprobeDuration(stdout) {
|
|
95
|
+
const trimmed = stdout.trim();
|
|
96
|
+
if (trimmed.length === 0)
|
|
97
|
+
return undefined;
|
|
98
|
+
const seconds = Number.parseFloat(trimmed);
|
|
99
|
+
// Reason: `parseFloat('N/A')` → NaN; duração negativa é dado degenerado. Em ambos,
|
|
100
|
+
// tratamos como indisponível em vez de presumir um valor (degradação graciosa).
|
|
101
|
+
if (!Number.isFinite(seconds) || seconds < 0)
|
|
102
|
+
return undefined;
|
|
103
|
+
return seconds;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Mede a duração de um vídeo via `ffprobe` ANTES de convertê-lo/transcrevê-lo
|
|
107
|
+
* (M4-09, #65). REUSA o subprocesso seguro compartilhado (sem shell,
|
|
108
|
+
* `-protocol_whitelist file`, env sanitizado, watchdog `SIGTERM`→`SIGKILL`). NUNCA
|
|
109
|
+
* lança (degradação graciosa, ADR-002): ffprobe ausente, exit != 0, timeout ou
|
|
110
|
+
* saída não-parseável viram `{ status: 'unavailable', reason }` — cabe ao chamador
|
|
111
|
+
* decidir o comportamento seguro (o pipeline PROSSEGUE quando a duração é indisponível).
|
|
112
|
+
*
|
|
113
|
+
* @param inputPath - Caminho absoluto do vídeo já baixado no cache.
|
|
114
|
+
* @param options - Deps injetáveis + timeouts + logger — ver {@link ProbeVideoDurationOptions}.
|
|
115
|
+
* @returns `ok` com `seconds` no sucesso; `unavailable` com motivo caso contrário.
|
|
116
|
+
* @example
|
|
117
|
+
* const probe = await probeVideoDuration('/cache/att/clip.mp4');
|
|
118
|
+
* if (probe.status === 'ok' && probe.seconds > 1200) skipTranscription();
|
|
119
|
+
*/
|
|
120
|
+
export async function probeVideoDuration(inputPath, options = {}) {
|
|
121
|
+
const findBinary = options.findFfprobeBinary ?? (() => findFfprobe()?.path);
|
|
122
|
+
const bin = findBinary();
|
|
123
|
+
if (bin === undefined) {
|
|
124
|
+
options.logger?.warn('ffprobe: binário não encontrado; a duração do vídeo não será medida ' +
|
|
125
|
+
'(a transcrição segue sem o limite de duração) — instale o ffmpeg/ffprobe (ver doctor)');
|
|
126
|
+
return { status: 'unavailable', reason: 'ffprobe-nao-instalado' };
|
|
127
|
+
}
|
|
128
|
+
const run = options.run ?? defaultProbeRun;
|
|
129
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
130
|
+
const killGraceMs = options.killGraceMs ?? PROBE_KILL_GRACE_MS;
|
|
131
|
+
const args = buildFfprobeArgs(inputPath);
|
|
132
|
+
try {
|
|
133
|
+
const stdout = await run({ bin, args, env: sanitizedEnv(), timeoutMs, killGraceMs });
|
|
134
|
+
const seconds = parseFfprobeDuration(stdout);
|
|
135
|
+
if (seconds === undefined) {
|
|
136
|
+
return { status: 'unavailable', reason: 'duracao-indisponivel' };
|
|
137
|
+
}
|
|
138
|
+
return { status: 'ok', seconds };
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
142
|
+
return {
|
|
143
|
+
status: 'unavailable',
|
|
144
|
+
reason: isProbeTimeout(error) ? 'timeout' : 'erro-ffprobe',
|
|
145
|
+
error: message,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Localização e versão do binário `ffmpeg` (M4-01, #57, ADR-002).
|
|
3
|
+
*
|
|
4
|
+
* O `doctor` (#57) usa estas funções para reportar a presença do ffmpeg — que a
|
|
5
|
+
* milestone M4 usará para extrair a faixa de áudio + 1 keyframe de vídeos (ADR-002)
|
|
6
|
+
* — sem reimplementar a detecção em cada superfície. Segue o padrão de
|
|
7
|
+
* {@link findTesseract}: PATH + locais convencionais (incl. `/opt/homebrew/bin`),
|
|
8
|
+
* puro e injetável, degradando graciosamente quando ausente.
|
|
9
|
+
*/
|
|
10
|
+
import { findExecutable } from './which.js';
|
|
11
|
+
/** Resultado de {@link findFfmpeg}: caminho absoluto do binário localizado. */
|
|
12
|
+
export interface FfmpegLocation {
|
|
13
|
+
/** Caminho absoluto do executável `ffmpeg` encontrado. */
|
|
14
|
+
readonly path: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Localiza o binário `ffmpeg` no `PATH` e em locais convencionais por plataforma.
|
|
18
|
+
* Função pura e reutilizável pelo `doctor` (#57). Não executa o binário.
|
|
19
|
+
*
|
|
20
|
+
* @param deps - Deps injetáveis (plataforma/PATH/executabilidade) — ver
|
|
21
|
+
* {@link findExecutable}. Default: ambiente real.
|
|
22
|
+
* @returns A localização encontrada, ou `undefined` se não instalado.
|
|
23
|
+
* @example
|
|
24
|
+
* const found = findFfmpeg();
|
|
25
|
+
* if (found === undefined) logger.warn('ffmpeg não instalado');
|
|
26
|
+
*/
|
|
27
|
+
export declare function findFfmpeg(deps?: Parameters<typeof findExecutable>[2]): FfmpegLocation | undefined;
|
|
28
|
+
/** Resultado de {@link findFfprobe}: caminho absoluto do binário localizado. */
|
|
29
|
+
export interface FfprobeLocation {
|
|
30
|
+
/** Caminho absoluto do executável `ffprobe` encontrado. */
|
|
31
|
+
readonly path: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Localiza o binário `ffprobe` no `PATH` e em locais convencionais por plataforma
|
|
35
|
+
* (os mesmos do ffmpeg — o ffprobe vem no MESMO pacote). Função pura e injetável;
|
|
36
|
+
* não executa o binário. Degrada para `undefined` quando ausente (ADR-002): sem
|
|
37
|
+
* ffprobe, a #65 não bloqueia — apenas não mede a duração.
|
|
38
|
+
*
|
|
39
|
+
* @param deps - Deps injetáveis (plataforma/PATH/executabilidade) — ver
|
|
40
|
+
* {@link findExecutable}. Default: ambiente real.
|
|
41
|
+
* @returns A localização encontrada, ou `undefined` se não instalado.
|
|
42
|
+
* @example
|
|
43
|
+
* const probe = findFfprobe();
|
|
44
|
+
* if (probe === undefined) logger.warn('ffprobe não instalado; duração não medida');
|
|
45
|
+
*/
|
|
46
|
+
export declare function findFfprobe(deps?: Parameters<typeof findExecutable>[2]): FfprobeLocation | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Lê a versão do binário via `ffmpeg -version` (primeira linha:
|
|
49
|
+
* `ffmpeg version X.Y.Z ...` — em builds distro pode vir `n6.1.1` ou hash git;
|
|
50
|
+
* capturamos o primeiro token após `version`). Não lança: retorna `undefined`
|
|
51
|
+
* se o binário falhar ou a saída for inesperada.
|
|
52
|
+
*
|
|
53
|
+
* @param bin - Caminho absoluto do binário.
|
|
54
|
+
* @returns A versão detectada (ex.: `6.1.1`), ou `undefined`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function detectFfmpegVersion(bin: string): Promise<string | undefined>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Localização e versão do binário `ffmpeg` (M4-01, #57, ADR-002).
|
|
3
|
+
*
|
|
4
|
+
* O `doctor` (#57) usa estas funções para reportar a presença do ffmpeg — que a
|
|
5
|
+
* milestone M4 usará para extrair a faixa de áudio + 1 keyframe de vídeos (ADR-002)
|
|
6
|
+
* — sem reimplementar a detecção em cada superfície. Segue o padrão de
|
|
7
|
+
* {@link findTesseract}: PATH + locais convencionais (incl. `/opt/homebrew/bin`),
|
|
8
|
+
* puro e injetável, degradando graciosamente quando ausente.
|
|
9
|
+
*/
|
|
10
|
+
import { execFile } from 'node:child_process';
|
|
11
|
+
import { findExecutable } from './which.js';
|
|
12
|
+
/** Nome base do binário do ffmpeg. */
|
|
13
|
+
const FFMPEG_BINARY = 'ffmpeg';
|
|
14
|
+
/**
|
|
15
|
+
* Nome base do binário do `ffprobe` — a ferramenta de SONDA do pacote ffmpeg. É
|
|
16
|
+
* distribuída junto do ffmpeg (mesmo diretório/pacote em brew/apt/BtbN), por isso
|
|
17
|
+
* reusa os MESMOS locais convencionais ({@link CONVENTIONAL}). Usada pela #65 para
|
|
18
|
+
* medir a duração de um vídeo ANTES de transcrevê-lo (limite configurável, ADR-002).
|
|
19
|
+
*/
|
|
20
|
+
const FFPROBE_BINARY = 'ffprobe';
|
|
21
|
+
/**
|
|
22
|
+
* Locais convencionais do `ffmpeg` por família de SO, consultados após o `PATH`.
|
|
23
|
+
* No Windows não há caminho canônico (builds estáticos BtbN são descompactados
|
|
24
|
+
* em qualquer lugar) — cobrimos os destinos mais comuns como best-effort.
|
|
25
|
+
*/
|
|
26
|
+
const CONVENTIONAL = {
|
|
27
|
+
unix: ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin'],
|
|
28
|
+
windows: ['C:\\ffmpeg\\bin', 'C:\\Program Files\\ffmpeg\\bin'],
|
|
29
|
+
};
|
|
30
|
+
/** Graça de timeout ao ler a versão do ffmpeg (ms) — a chamada é instantânea. */
|
|
31
|
+
const VERSION_TIMEOUT_MS = 2_000;
|
|
32
|
+
/**
|
|
33
|
+
* Localiza o binário `ffmpeg` no `PATH` e em locais convencionais por plataforma.
|
|
34
|
+
* Função pura e reutilizável pelo `doctor` (#57). Não executa o binário.
|
|
35
|
+
*
|
|
36
|
+
* @param deps - Deps injetáveis (plataforma/PATH/executabilidade) — ver
|
|
37
|
+
* {@link findExecutable}. Default: ambiente real.
|
|
38
|
+
* @returns A localização encontrada, ou `undefined` se não instalado.
|
|
39
|
+
* @example
|
|
40
|
+
* const found = findFfmpeg();
|
|
41
|
+
* if (found === undefined) logger.warn('ffmpeg não instalado');
|
|
42
|
+
*/
|
|
43
|
+
export function findFfmpeg(deps) {
|
|
44
|
+
const located = findExecutable([FFMPEG_BINARY], CONVENTIONAL, deps);
|
|
45
|
+
return located !== undefined ? { path: located.path } : undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Localiza o binário `ffprobe` no `PATH` e em locais convencionais por plataforma
|
|
49
|
+
* (os mesmos do ffmpeg — o ffprobe vem no MESMO pacote). Função pura e injetável;
|
|
50
|
+
* não executa o binário. Degrada para `undefined` quando ausente (ADR-002): sem
|
|
51
|
+
* ffprobe, a #65 não bloqueia — apenas não mede a duração.
|
|
52
|
+
*
|
|
53
|
+
* @param deps - Deps injetáveis (plataforma/PATH/executabilidade) — ver
|
|
54
|
+
* {@link findExecutable}. Default: ambiente real.
|
|
55
|
+
* @returns A localização encontrada, ou `undefined` se não instalado.
|
|
56
|
+
* @example
|
|
57
|
+
* const probe = findFfprobe();
|
|
58
|
+
* if (probe === undefined) logger.warn('ffprobe não instalado; duração não medida');
|
|
59
|
+
*/
|
|
60
|
+
export function findFfprobe(deps) {
|
|
61
|
+
const located = findExecutable([FFPROBE_BINARY], CONVENTIONAL, deps);
|
|
62
|
+
return located !== undefined ? { path: located.path } : undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Monta o env MÍNIMO do subprocesso: só `PATH`. Nenhum segredo do pai (ex.:
|
|
66
|
+
* `REDMINE_API_KEY`) vaza para o ffmpeg (ADR-002).
|
|
67
|
+
*
|
|
68
|
+
* @returns Env sanitizado para o subprocesso.
|
|
69
|
+
*/
|
|
70
|
+
function sanitizedEnv() {
|
|
71
|
+
return { PATH: process.env.PATH ?? '/usr/bin:/bin' };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Lê a versão do binário via `ffmpeg -version` (primeira linha:
|
|
75
|
+
* `ffmpeg version X.Y.Z ...` — em builds distro pode vir `n6.1.1` ou hash git;
|
|
76
|
+
* capturamos o primeiro token após `version`). Não lança: retorna `undefined`
|
|
77
|
+
* se o binário falhar ou a saída for inesperada.
|
|
78
|
+
*
|
|
79
|
+
* @param bin - Caminho absoluto do binário.
|
|
80
|
+
* @returns A versão detectada (ex.: `6.1.1`), ou `undefined`.
|
|
81
|
+
*/
|
|
82
|
+
export function detectFfmpegVersion(bin) {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
execFile(bin, ['-version'], { env: sanitizedEnv(), encoding: 'utf8', windowsHide: true, timeout: VERSION_TIMEOUT_MS }, (error, stdout) => {
|
|
85
|
+
if (error !== null) {
|
|
86
|
+
resolve(undefined);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Reason: 1ª linha "ffmpeg version <token> ..."; o token pode ser semver
|
|
90
|
+
// puro, prefixado (`n6.1`) ou um hash — reportamos o token cru como veio.
|
|
91
|
+
const match = /ffmpeg version (\S+)/i.exec(stdout);
|
|
92
|
+
resolve(match?.[1]);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download do modelo GGUF do whisper.cpp com verificação de integridade
|
|
3
|
+
* (M4-02, #58, ADR-002).
|
|
4
|
+
*
|
|
5
|
+
* A transcrição local (ADR-002) exige um modelo `.gguf`, artefato independente do
|
|
6
|
+
* binário `whisper-cli` ({@link findWhisper}). Este módulo baixa esse modelo de
|
|
7
|
+
* forma AUDITÁVEL e SEGURA, respeitando a política híbrida de binários do ADR-002:
|
|
8
|
+
*
|
|
9
|
+
* - CONSTANTES PINADAS: URL HTTPS oficial ({@link GGUF_MODEL_URL}) e SHA-256
|
|
10
|
+
* esperado ({@link GGUF_MODEL_SHA256}) ficam no código, não em config remota — a
|
|
11
|
+
* integridade não depende de o servidor "dizer" qual é o hash certo.
|
|
12
|
+
* - SÓ HTTPS: qualquer URL `http://` é RECUSADA antes de tocar a rede (defesa
|
|
13
|
+
* contra downgrade/MITM em ambiente corporativo).
|
|
14
|
+
* - CHECKSUM OBRIGATÓRIO: o conteúdo é gravado num `.part`, seu SHA-256 é comparado
|
|
15
|
+
* ao pinado e só então renomeado para o destino; divergência → o `.part` é
|
|
16
|
+
* DESCARTADO e um erro claro (esperado vs. obtido) é lançado.
|
|
17
|
+
* - RETOMADA VIA HTTP RANGE (M4-03, #59): se um `.part` de N bytes já existe, o
|
|
18
|
+
* request pede `Range: bytes=N-` e ANEXA os bytes recebidos (`206 Partial Content`),
|
|
19
|
+
* evitando rebaixar do zero. O checksum é sempre do arquivo COMPLETO. Fallbacks
|
|
20
|
+
* graciosos: servidor que IGNORA o Range (`200 OK` com o corpo inteiro) recomeça
|
|
21
|
+
* do zero sem corromper; `.part` inservível (`416`) é descartado e o download
|
|
22
|
+
* reinicia limpo.
|
|
23
|
+
* - NUNCA EM MCP HEADLESS: o download é OPT-IN INTERATIVO (`--download-binaries`,
|
|
24
|
+
* ADR-002). Quando a chamada sinaliza ambiente MCP/headless ({@link DownloadGgufOptions.headless}),
|
|
25
|
+
* a função RECUSA antes de qualquer IO, com erro acionável — download silencioso
|
|
26
|
+
* de executáveis/modelos é vetor de supply chain.
|
|
27
|
+
*
|
|
28
|
+
* Todas as dependências de efeito colateral (fetch/fs) são INJETÁVEIS
|
|
29
|
+
* ({@link GgufDeps}) para tornar os testes herméticos — sem rede nem FS real.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Modelo GGUF default: `ggml-tiny` (multilíngue, ~78 MB) do repositório oficial
|
|
33
|
+
* `ggerganov/whisper.cpp` no Hugging Face. Escolhido por ser o MENOR modelo
|
|
34
|
+
* multilíngue oficial — mantém o auto-detect de idioma do ADR-002 com o menor
|
|
35
|
+
* custo de download para o primeiro contato. Modelos maiores (base/small) são
|
|
36
|
+
* evolução futura configurável; a MECÂNICA de download+verificação é idêntica.
|
|
37
|
+
*/
|
|
38
|
+
export declare const GGUF_MODEL_NAME: "ggml-tiny.bin";
|
|
39
|
+
/** URL HTTPS oficial e fixa do {@link GGUF_MODEL_NAME} (Hugging Face resolve). */
|
|
40
|
+
export declare const GGUF_MODEL_URL: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin";
|
|
41
|
+
/**
|
|
42
|
+
* SHA-256 hex esperado do {@link GGUF_MODEL_NAME}. É a ÂNCORA de integridade:
|
|
43
|
+
* o arquivo baixado só é aceito se seu hash bater exatamente com esta constante.
|
|
44
|
+
*/
|
|
45
|
+
export declare const GGUF_MODEL_SHA256: "be07e048e1e599ad46341c8d2a135645097a538221678b7acdd1b1919c6e1b21";
|
|
46
|
+
/** Categorias de falha do download — discriminante acionável do {@link GgufDownloadError}. */
|
|
47
|
+
export type GgufDownloadFailure = 'headless' | 'insecure-url' | 'http-error' | 'checksum-mismatch';
|
|
48
|
+
/**
|
|
49
|
+
* Erro tipado de qualquer recusa/falha do download do modelo GGUF. O campo
|
|
50
|
+
* {@link GgufDownloadError.code} permite ao chamador (CLI/doctor) reagir de forma
|
|
51
|
+
* específica sem casar strings de mensagem.
|
|
52
|
+
*/
|
|
53
|
+
export declare class GgufDownloadError extends Error {
|
|
54
|
+
/** Categoria da falha — ver {@link GgufDownloadFailure}. */
|
|
55
|
+
readonly code: GgufDownloadFailure;
|
|
56
|
+
constructor(code: GgufDownloadFailure, message: string);
|
|
57
|
+
}
|
|
58
|
+
/** Opções de rede repassadas ao `fetch` — hoje apenas cabeçalhos (ex.: `Range`). */
|
|
59
|
+
export interface GgufFetchInit {
|
|
60
|
+
/** Cabeçalhos HTTP a enviar (ex.: `{ Range: 'bytes=1024-' }`). */
|
|
61
|
+
readonly headers?: Record<string, string>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Dependências injetáveis de efeito colateral (rede + filesystem). O default
|
|
65
|
+
* ({@link defaultGgufDeps}) usa `fetch` global e `node:fs/promises`; os testes
|
|
66
|
+
* injetam spies para permanecerem herméticos.
|
|
67
|
+
*/
|
|
68
|
+
export interface GgufDeps {
|
|
69
|
+
/**
|
|
70
|
+
* Busca a URL (com cabeçalhos opcionais, ex.: `Range`) e devolve a resposta —
|
|
71
|
+
* wrapper fino sobre o `fetch` global.
|
|
72
|
+
*/
|
|
73
|
+
readonly fetch: (url: string, init?: GgufFetchInit) => Promise<Response>;
|
|
74
|
+
/** Cria o diretório de destino recursivamente (idempotente). */
|
|
75
|
+
readonly mkdir: (dir: string) => Promise<void>;
|
|
76
|
+
/** Grava (truncando) os bytes baixados num caminho (o `.part`) — download novo. */
|
|
77
|
+
readonly writeFile: (path: string, data: Uint8Array) => Promise<void>;
|
|
78
|
+
/** ANEXA bytes ao fim do `.part` existente — usado na retomada via Range. */
|
|
79
|
+
readonly appendFile: (path: string, data: Uint8Array) => Promise<void>;
|
|
80
|
+
/** Lê o conteúdo atual do `.part` (para compor o checksum do arquivo completo). */
|
|
81
|
+
readonly readPart: (path: string) => Promise<Uint8Array>;
|
|
82
|
+
/**
|
|
83
|
+
* Tamanho, em bytes, de um `.part` já existente; `undefined` se ele não existe —
|
|
84
|
+
* é o ponto de retomada (`Range: bytes=<tamanho>-`).
|
|
85
|
+
*/
|
|
86
|
+
readonly statPart: (path: string) => Promise<number | undefined>;
|
|
87
|
+
/** Renomeia o `.part` verificado para o destino final. */
|
|
88
|
+
readonly rename: (from: string, to: string) => Promise<void>;
|
|
89
|
+
/** Remove um caminho (o `.part` descartado), sem lançar se ausente. */
|
|
90
|
+
readonly rm: (path: string) => Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
/** Opções de {@link downloadGgufModel}. Tudo tem default seguro para produção. */
|
|
93
|
+
export interface DownloadGgufOptions {
|
|
94
|
+
/** Diretório de destino do modelo. Default: {@link whisperModelDir}. */
|
|
95
|
+
readonly destDir?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Indica ambiente MCP/headless. Quando `true`, o download é RECUSADO (opt-in
|
|
98
|
+
* interativo, ADR-002). Default: `false`.
|
|
99
|
+
*/
|
|
100
|
+
readonly headless?: boolean;
|
|
101
|
+
/** URL de origem (HTTPS). Default: {@link GGUF_MODEL_URL}. */
|
|
102
|
+
readonly url?: string;
|
|
103
|
+
/** SHA-256 hex esperado. Default: {@link GGUF_MODEL_SHA256}. */
|
|
104
|
+
readonly sha256?: string;
|
|
105
|
+
/** Dependências injetáveis (fetch/fs). Default: {@link defaultGgufDeps}. */
|
|
106
|
+
readonly deps?: GgufDeps;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Constrói as dependências reais de produção: `fetch` global e `node:fs/promises`
|
|
110
|
+
* (`mkdir` recursivo, `rm` com `force`). Isolado para manter {@link downloadGgufModel}
|
|
111
|
+
* testável sem tocar rede/FS.
|
|
112
|
+
*
|
|
113
|
+
* @returns Deps ligadas ao ambiente real.
|
|
114
|
+
*/
|
|
115
|
+
export declare function defaultGgufDeps(): GgufDeps;
|
|
116
|
+
/**
|
|
117
|
+
* Baixa o modelo GGUF do whisper.cpp e o grava no cache local, verificando o
|
|
118
|
+
* SHA-256 pinado antes de considerá-lo válido (ADR-002, M4-02).
|
|
119
|
+
*
|
|
120
|
+
* Ordem dos portões (todos exercitados por testes): (1) guard headless — recusa
|
|
121
|
+
* antes de qualquer IO; (2) só HTTPS — recusa `http://` sem tocar a rede; (3)
|
|
122
|
+
* RETOMADA — se um `.part` de N bytes já existe, pede `Range: bytes=N-` e ANEXA o
|
|
123
|
+
* `206`; se o servidor ignora o Range (`200`) recomeça do zero, e um `.part`
|
|
124
|
+
* inservível (`416`) é descartado antes de reiniciar limpo; (4) compara o SHA-256
|
|
125
|
+
* do arquivo COMPLETO ao esperado — se bater, renomeia atomicamente para o destino
|
|
126
|
+
* e devolve o caminho; se não, DESCARTA o `.part` e lança {@link GgufDownloadError}
|
|
127
|
+
* com esperado vs. obtido.
|
|
128
|
+
*
|
|
129
|
+
* @param options - Destino, flag headless, origem/checksum e deps — ver {@link DownloadGgufOptions}.
|
|
130
|
+
* @returns Caminho absoluto do modelo `.gguf`/`.bin` gravado e verificado.
|
|
131
|
+
* @throws {GgufDownloadError} `headless` (ambiente MCP), `insecure-url` (não-HTTPS),
|
|
132
|
+
* `http-error` (resposta não-2xx) ou `checksum-mismatch` (integridade divergente).
|
|
133
|
+
* @example
|
|
134
|
+
* const modelPath = await downloadGgufModel({ headless: false });
|
|
135
|
+
* logger.info(`modelo pronto em ${modelPath}`);
|
|
136
|
+
*/
|
|
137
|
+
export declare function downloadGgufModel(options?: DownloadGgufOptions): Promise<string>;
|