@crafter/trx 0.1.1 → 0.2.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/bin/trx.ts +1 -1
- package/package.json +1 -1
- package/src/commands/transcribe.ts +10 -3
- package/src/core/pipeline.ts +3 -2
- package/src/core/whisper.ts +18 -2
- package/src/utils/spawn.ts +37 -0
package/bin/trx.ts
CHANGED
|
@@ -10,7 +10,7 @@ const program = new Command();
|
|
|
10
10
|
program
|
|
11
11
|
.name("trx")
|
|
12
12
|
.description("Agent-first CLI for audio/video transcription via Whisper")
|
|
13
|
-
.version("0.
|
|
13
|
+
.version("0.2.0")
|
|
14
14
|
.option("-o, --output <format>", "output format (json, table, auto)", "auto")
|
|
15
15
|
.hook("preAction", (thisCommand) => {
|
|
16
16
|
const opts = thisCommand.opts();
|
package/package.json
CHANGED
|
@@ -105,9 +105,12 @@ export function createTranscribeCommand(): Command {
|
|
|
105
105
|
onStep: (step) => {
|
|
106
106
|
if (spinner) spinner.start(step);
|
|
107
107
|
},
|
|
108
|
+
onProgress: (progress) => {
|
|
109
|
+
if (spinner) spinner.message(`Transcribing... ${progress.percent}%`);
|
|
110
|
+
},
|
|
108
111
|
});
|
|
109
112
|
|
|
110
|
-
if (spinner) spinner.stop("
|
|
113
|
+
if (spinner) spinner.stop("Transcription complete");
|
|
111
114
|
|
|
112
115
|
const filtered = opts.fields ? filterFields(result, opts.fields) : result;
|
|
113
116
|
output(format, {
|
|
@@ -118,12 +121,16 @@ export function createTranscribeCommand(): Command {
|
|
|
118
121
|
["Input", result.input],
|
|
119
122
|
["Language", result.metadata.language],
|
|
120
123
|
["Model", result.metadata.model],
|
|
121
|
-
["SRT", result.files.srt],
|
|
122
124
|
["TXT", result.files.txt],
|
|
123
|
-
["
|
|
125
|
+
["SRT", result.files.srt],
|
|
124
126
|
],
|
|
125
127
|
},
|
|
126
128
|
});
|
|
129
|
+
|
|
130
|
+
if (isTTY) {
|
|
131
|
+
const wordCount = result.text.split(/\s+/).filter(Boolean).length;
|
|
132
|
+
p.note(`${wordCount} words transcribed\n\nopen ${result.files.txt}`, "Next");
|
|
133
|
+
}
|
|
127
134
|
} catch (e) {
|
|
128
135
|
outputError((e as Error).message, format);
|
|
129
136
|
}
|
package/src/core/pipeline.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { basename, resolve } from "node:path";
|
|
|
2
2
|
import type { TrxConfig } from "../utils/config.ts";
|
|
3
3
|
import { cleanAudio } from "./audio.ts";
|
|
4
4
|
import { downloadMedia } from "./download.ts";
|
|
5
|
-
import { transcribe } from "./whisper.ts";
|
|
5
|
+
import { type WhisperProgress, transcribe } from "./whisper.ts";
|
|
6
6
|
|
|
7
7
|
export interface PipelineOptions {
|
|
8
8
|
input: string;
|
|
@@ -13,6 +13,7 @@ export interface PipelineOptions {
|
|
|
13
13
|
noDownload?: boolean;
|
|
14
14
|
noClean?: boolean;
|
|
15
15
|
onStep?: (step: string) => void;
|
|
16
|
+
onProgress?: (progress: WhisperProgress) => void;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export interface PipelineResult {
|
|
@@ -52,7 +53,7 @@ export async function runPipeline(opts: PipelineOptions): Promise<PipelineResult
|
|
|
52
53
|
|
|
53
54
|
const whisperInput = opts.noClean ? inputFile : wavPath;
|
|
54
55
|
opts.onStep?.("Transcribing with Whisper...");
|
|
55
|
-
const result = await transcribe(whisperInput, config, opts.language);
|
|
56
|
+
const result = await transcribe(whisperInput, config, opts.language, opts.onProgress);
|
|
56
57
|
|
|
57
58
|
return {
|
|
58
59
|
success: true,
|
package/src/core/whisper.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import type { TrxConfig } from "../utils/config.ts";
|
|
3
|
-
import { spawnOrThrow } from "../utils/spawn.ts";
|
|
3
|
+
import { spawnOrThrow, spawnStreaming } from "../utils/spawn.ts";
|
|
4
|
+
|
|
5
|
+
export interface WhisperProgress {
|
|
6
|
+
percent: number;
|
|
7
|
+
}
|
|
4
8
|
|
|
5
9
|
export interface WhisperResult {
|
|
6
10
|
srtPath: string;
|
|
@@ -12,6 +16,7 @@ export async function transcribe(
|
|
|
12
16
|
wavPath: string,
|
|
13
17
|
config: TrxConfig,
|
|
14
18
|
languageOverride?: string,
|
|
19
|
+
onProgress?: (progress: WhisperProgress) => void,
|
|
15
20
|
): Promise<WhisperResult> {
|
|
16
21
|
if (!existsSync(config.modelPath)) {
|
|
17
22
|
throw new Error(`Whisper model not found: ${config.modelPath}\nRun "trx init" to download a model.`);
|
|
@@ -42,7 +47,17 @@ export async function transcribe(
|
|
|
42
47
|
args.push("--entropy-thold", String(flags.entropyThold));
|
|
43
48
|
args.push("--logprob-thold", String(flags.logprobThold));
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
if (onProgress) {
|
|
51
|
+
args.push("--print-progress");
|
|
52
|
+
await spawnStreaming(args, "whisper-cli transcription", (line) => {
|
|
53
|
+
const match = line.match(/progress\s*=\s*(\d+)%/i);
|
|
54
|
+
if (match) {
|
|
55
|
+
onProgress({ percent: Number.parseInt(match[1], 10) });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
await spawnOrThrow(args, "whisper-cli transcription");
|
|
60
|
+
}
|
|
46
61
|
|
|
47
62
|
const srtPath = `${wavPath}.srt`;
|
|
48
63
|
if (!existsSync(srtPath)) {
|
|
@@ -62,6 +77,7 @@ function srtToPlainText(srt: string): string {
|
|
|
62
77
|
return srt
|
|
63
78
|
.split("\n")
|
|
64
79
|
.filter((line) => !/^\[|-->/.test(line))
|
|
80
|
+
.filter((line) => !/^\d+\s*$/.test(line))
|
|
65
81
|
.filter((line) => line.trim().length > 0)
|
|
66
82
|
.join("\n");
|
|
67
83
|
}
|
package/src/utils/spawn.ts
CHANGED
|
@@ -25,3 +25,40 @@ export async function spawnOrThrow(cmd: string[], context: string): Promise<stri
|
|
|
25
25
|
}
|
|
26
26
|
return result.stdout;
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
export async function spawnStreaming(
|
|
30
|
+
cmd: string[],
|
|
31
|
+
context: string,
|
|
32
|
+
onStderr?: (line: string) => void,
|
|
33
|
+
): Promise<string> {
|
|
34
|
+
const proc = Bun.spawn(cmd, {
|
|
35
|
+
stdout: "pipe",
|
|
36
|
+
stderr: "pipe",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const stderrReader = (async () => {
|
|
40
|
+
const reader = proc.stderr.getReader();
|
|
41
|
+
const decoder = new TextDecoder();
|
|
42
|
+
let buffer = "";
|
|
43
|
+
while (true) {
|
|
44
|
+
const { done, value } = await reader.read();
|
|
45
|
+
if (done) break;
|
|
46
|
+
buffer += decoder.decode(value, { stream: true });
|
|
47
|
+
const lines = buffer.split("\n");
|
|
48
|
+
buffer = lines.pop() || "";
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (line.trim()) onStderr?.(line.trim());
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (buffer.trim()) onStderr?.(buffer.trim());
|
|
54
|
+
})();
|
|
55
|
+
|
|
56
|
+
const stdout = await new Response(proc.stdout).text();
|
|
57
|
+
await stderrReader;
|
|
58
|
+
const exitCode = await proc.exited;
|
|
59
|
+
|
|
60
|
+
if (exitCode !== 0) {
|
|
61
|
+
throw new Error(`${context} failed (exit ${exitCode})`);
|
|
62
|
+
}
|
|
63
|
+
return stdout.trim();
|
|
64
|
+
}
|