@crafter/trx 0.1.0 → 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 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.1.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crafter/trx",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Agent-first CLI for audio/video transcription via Whisper",
5
5
  "module": "bin/trx.ts",
6
6
  "type": "module",
@@ -38,5 +38,8 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/bun": "latest"
41
+ },
42
+ "engines": {
43
+ "bun": ">=1.0.0"
41
44
  }
42
45
  }
@@ -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("Done");
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
- ["WAV", result.files.wav],
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
  }
@@ -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,
@@ -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
- await spawnOrThrow(args, "whisper-cli transcription");
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
  }
@@ -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
+ }