@drakulavich/parakeet-cli 0.1.3 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/parakeet-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Fast multilingual speech-to-text CLI powered by NVIDIA Parakeet ONNX models",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,7 +44,8 @@
44
44
  "bun": ">=1.3.0"
45
45
  },
46
46
  "devDependencies": {
47
- "@types/bun": "latest"
47
+ "@types/bun": "latest",
48
+ "typescript": "^6.0.2"
48
49
  },
49
50
  "dependencies": {
50
51
  "onnxruntime-node": "^1.24.0"
@@ -1,8 +1,10 @@
1
1
  import { describe, test, expect } from "bun:test";
2
2
  import { convertToFloat32PCM } from "../audio";
3
- import { existsSync } from "fs";
3
+ import { spawnSync } from "child_process";
4
4
 
5
- describe("audio", () => {
5
+ const hasFfmpeg = spawnSync("which", ["ffmpeg"]).status === 0;
6
+
7
+ describe.skipIf(!hasFfmpeg)("audio", () => {
6
8
  test("converts WAV to 16kHz mono Float32Array", async () => {
7
9
  const buffer = await convertToFloat32PCM("fixtures/silence.wav");
8
10
  expect(buffer).toBeInstanceOf(Float32Array);
@@ -1,16 +1,11 @@
1
1
  import { describe, test, expect } from "bun:test";
2
- import { getModelDir, MODEL_FILES, HF_REPOS } from "../models";
2
+ import { getModelDir, MODEL_FILES, HF_REPO } from "../models";
3
3
  import { join } from "path";
4
4
  import { homedir } from "os";
5
5
 
6
6
  describe("models", () => {
7
- test("getModelDir returns correct cache path for v2", () => {
8
- const dir = getModelDir("v2");
9
- expect(dir).toBe(join(homedir(), ".cache", "parakeet", "v2"));
10
- });
11
-
12
- test("getModelDir returns correct cache path for v3", () => {
13
- const dir = getModelDir("v3");
7
+ test("getModelDir returns correct cache path", () => {
8
+ const dir = getModelDir();
14
9
  expect(dir).toBe(join(homedir(), ".cache", "parakeet", "v3"));
15
10
  });
16
11
 
@@ -22,8 +17,7 @@ describe("models", () => {
22
17
  expect(MODEL_FILES).toContain("vocab.txt");
23
18
  });
24
19
 
25
- test("HF_REPOS maps versions to repo IDs", () => {
26
- expect(HF_REPOS.v2).toBe("istupakov/parakeet-tdt-0.6b-v2-onnx");
27
- expect(HF_REPOS.v3).toBe("istupakov/parakeet-tdt-0.6b-v3-onnx");
20
+ test("HF_REPO points to v3 ONNX repo", () => {
21
+ expect(HF_REPO).toBe("istupakov/parakeet-tdt-0.6b-v3-onnx");
28
22
  });
29
23
  });
@@ -38,8 +38,4 @@ describe("tokenizer", () => {
38
38
  expect(text).toBe("cats");
39
39
  });
40
40
 
41
- test("isAsciiDominant returns true for ASCII tokens", async () => {
42
- const tok = await Tokenizer.fromFile("fixtures/test-vocab.txt");
43
- expect(tok.isAsciiDominant([0, 1, 2])).toBe(true);
44
- });
45
41
  });
package/src/audio.ts CHANGED
@@ -30,6 +30,7 @@ export async function convertToFloat32PCM(inputPath: string): Promise<Float32Arr
30
30
  const raw = await Bun.file(tmpPath).arrayBuffer();
31
31
  return new Float32Array(raw);
32
32
  } finally {
33
+ // Best-effort cleanup; file may already be gone
33
34
  try { unlinkSync(tmpPath); } catch {}
34
35
  }
35
36
  }
package/src/cli.ts CHANGED
@@ -28,8 +28,9 @@ async function main(): Promise<void> {
28
28
  try {
29
29
  const text = await transcribe(file, { noCache });
30
30
  if (text) process.stdout.write(text + "\n");
31
- } catch (err: any) {
32
- console.error(`Error: ${err.message}`);
31
+ } catch (err: unknown) {
32
+ const message = err instanceof Error ? err.message : String(err);
33
+ console.error(`Error: ${message}`);
33
34
  process.exit(1);
34
35
  }
35
36
  }
package/src/decoder.ts CHANGED
@@ -2,16 +2,19 @@ import * as ort from "onnxruntime-node";
2
2
  import { join } from "path";
3
3
  import { ensureOrtBackend } from "./ort-backend-fix";
4
4
 
5
+ // TDT allows multiple tokens per encoder frame; cap to prevent runaway decoding
5
6
  const MAX_TOKENS_PER_STEP = 10;
6
7
 
8
+ type F32 = Float32Array<ArrayBufferLike>;
9
+
7
10
  export interface DecoderSession {
8
11
  decode(
9
- encoderFrame: Float32Array,
12
+ encoderFrame: F32,
10
13
  targets: number[],
11
14
  targetLength: number,
12
- state1: Float32Array,
13
- state2: Float32Array
14
- ): Promise<{ output: Float32Array; state1: Float32Array; state2: Float32Array }>;
15
+ state1: F32,
16
+ state2: F32
17
+ ): Promise<{ output: F32; state1: F32; state2: F32 }>;
15
18
  vocabSize: number;
16
19
  blankId: number;
17
20
  stateDims: { layers: number; hidden: number };
@@ -27,8 +30,8 @@ export async function greedyDecode(
27
30
 
28
31
  const tokens: number[] = [];
29
32
  const stateSize = session.stateDims.layers * session.stateDims.hidden;
30
- let state1 = new Float32Array(stateSize);
31
- let state2 = new Float32Array(stateSize);
33
+ let state1: F32 = new Float32Array(stateSize);
34
+ let state2: F32 = new Float32Array(stateSize);
32
35
  let lastToken = session.blankId;
33
36
 
34
37
  let t = 0;
@@ -129,7 +132,3 @@ export function createOnnxDecoderSession(
129
132
  },
130
133
  };
131
134
  }
132
-
133
- export function releaseDecoder(): void {
134
- onnxSession = null;
135
- }
package/src/encoder.ts CHANGED
@@ -26,7 +26,3 @@ export async function encode(
26
26
 
27
27
  return { encoderOutput, encodedLength };
28
28
  }
29
-
30
- export function releaseEncoder(): void {
31
- session = null;
32
- }
package/src/models.ts CHANGED
@@ -2,12 +2,7 @@ import { join } from "path";
2
2
  import { homedir } from "os";
3
3
  import { existsSync, mkdirSync } from "fs";
4
4
 
5
- export type ModelVersion = "v2" | "v3";
6
-
7
- export const HF_REPOS: Record<ModelVersion, string> = {
8
- v2: "istupakov/parakeet-tdt-0.6b-v2-onnx",
9
- v3: "istupakov/parakeet-tdt-0.6b-v3-onnx",
10
- };
5
+ export const HF_REPO = "istupakov/parakeet-tdt-0.6b-v3-onnx";
11
6
 
12
7
  export const MODEL_FILES = [
13
8
  "encoder-model.onnx",
@@ -17,28 +12,26 @@ export const MODEL_FILES = [
17
12
  "vocab.txt",
18
13
  ];
19
14
 
20
- export function getModelDir(version: ModelVersion): string {
21
- return join(homedir(), ".cache", "parakeet", version);
15
+ export function getModelDir(): string {
16
+ return join(homedir(), ".cache", "parakeet", "v3");
22
17
  }
23
18
 
24
- export function isModelCached(version: ModelVersion): boolean {
25
- const dir = getModelDir(version);
19
+ export function isModelCached(): boolean {
20
+ const dir = getModelDir();
26
21
  return MODEL_FILES.every((f) => existsSync(join(dir, f)));
27
22
  }
28
23
 
29
- export async function ensureModel(version: ModelVersion, noCache = false): Promise<string> {
30
- const dir = getModelDir(version);
24
+ export async function ensureModel(noCache = false): Promise<string> {
25
+ const dir = getModelDir();
31
26
 
32
- if (!noCache && isModelCached(version)) {
27
+ if (!noCache && isModelCached()) {
33
28
  return dir;
34
29
  }
35
30
 
36
31
  mkdirSync(dir, { recursive: true });
37
32
 
38
- const repo = HF_REPOS[version];
39
-
40
33
  for (const file of MODEL_FILES) {
41
- const url = `https://huggingface.co/${repo}/resolve/main/${file}`;
34
+ const url = `https://huggingface.co/${HF_REPO}/resolve/main/${file}`;
42
35
  const dest = join(dir, file);
43
36
 
44
37
  if (!noCache && existsSync(dest)) continue;
package/src/preprocess.ts CHANGED
@@ -2,6 +2,8 @@ import * as ort from "onnxruntime-node";
2
2
  import { join } from "path";
3
3
  import { ensureOrtBackend } from "./ort-backend-fix";
4
4
 
5
+ const NORM_EPSILON = 1e-10;
6
+
5
7
  let session: ort.InferenceSession | null = null;
6
8
 
7
9
  export async function initPreprocessor(modelDir: string): Promise<void> {
@@ -41,7 +43,7 @@ export async function preprocess(audio: Float32Array): Promise<{ features: ort.T
41
43
 
42
44
  const mean = sum / actualLength;
43
45
  const variance = sumSq / actualLength - mean * mean;
44
- const std = Math.sqrt(Math.max(variance, 1e-10));
46
+ const std = Math.sqrt(Math.max(variance, NORM_EPSILON));
45
47
 
46
48
  for (let t = 0; t < T; t++) {
47
49
  normalized[f * T + t] = t < actualLength ? (melData[f * T + t] - mean) / std : 0;
@@ -53,7 +55,3 @@ export async function preprocess(audio: Float32Array): Promise<{ features: ort.T
53
55
 
54
56
  return { features: featureTensor, length: outputLength };
55
57
  }
56
-
57
- export function releasePreprocessor(): void {
58
- session = null;
59
- }
package/src/tokenizer.ts CHANGED
@@ -40,20 +40,4 @@ export class Tokenizer {
40
40
  }
41
41
  return pieces.join("").replaceAll("\u2581", " ").trim();
42
42
  }
43
-
44
- isAsciiDominant(tokenIds: number[], threshold = 0.9): boolean {
45
- const nonBlank = tokenIds.filter((id) => id !== this.blankId);
46
- if (nonBlank.length === 0) return false;
47
-
48
- let asciiCount = 0;
49
- for (const id of nonBlank) {
50
- const token = this.idToToken.get(id) ?? "";
51
- const cleaned = token.replaceAll("\u2581", "");
52
- if (cleaned.length > 0 && /^[\x00-\x7F]+$/.test(cleaned)) {
53
- asciiCount++;
54
- }
55
- }
56
-
57
- return asciiCount / nonBlank.length >= threshold;
58
- }
59
43
  }
package/src/transcribe.ts CHANGED
@@ -10,6 +10,16 @@ import {
10
10
  import { Tokenizer } from "./tokenizer";
11
11
  import { join } from "path";
12
12
 
13
+ function transpose2D(data: Float32Array, rows: number, cols: number): Float32Array {
14
+ const out = new Float32Array(cols * rows);
15
+ for (let c = 0; c < cols; c++) {
16
+ for (let r = 0; r < rows; r++) {
17
+ out[c * rows + r] = data[r * cols + c];
18
+ }
19
+ }
20
+ return out;
21
+ }
22
+
13
23
  // Parakeet TDT 0.6B decoder state dimensions (from ONNX model input shapes)
14
24
  const DECODER_LAYERS = 2;
15
25
  const DECODER_HIDDEN = 640;
@@ -18,15 +28,18 @@ export interface TranscribeOptions {
18
28
  noCache?: boolean;
19
29
  }
20
30
 
31
+ // Minimum 0.1s of audio at 16kHz to produce meaningful output
32
+ const MIN_AUDIO_SAMPLES = 1600;
33
+
21
34
  export async function transcribe(audioPath: string, opts: TranscribeOptions = {}): Promise<string> {
22
35
  const audio = await convertToFloat32PCM(audioPath);
23
36
 
24
- if (audio.length < 1600) {
37
+ if (audio.length < MIN_AUDIO_SAMPLES) {
25
38
  return "";
26
39
  }
27
40
 
28
41
  const noCache = opts.noCache ?? false;
29
- const modelDir = await ensureModel("v3", noCache);
42
+ const modelDir = await ensureModel(noCache);
30
43
  const tokenizer = await Tokenizer.fromFile(join(modelDir, "vocab.txt"));
31
44
 
32
45
  await initPreprocessor(modelDir);
@@ -41,13 +54,7 @@ export async function transcribe(audioPath: string, opts: TranscribeOptions = {}
41
54
  const D = dims[1];
42
55
  const T = dims[2];
43
56
 
44
- // Transpose from [1, D, T] to [T, D] so each frame is contiguous
45
- const transposed = new Float32Array(T * D);
46
- for (let t = 0; t < T; t++) {
47
- for (let d = 0; d < D; d++) {
48
- transposed[t * D + d] = encoderData[d * T + t];
49
- }
50
- }
57
+ const transposed = transpose2D(encoderData, D, T);
51
58
 
52
59
  const session = createOnnxDecoderSession(
53
60
  tokenizer.vocabSize,
package/tsconfig.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "strict": true,
8
8
  "skipLibCheck": true,
9
9
  "outDir": "./dist",
10
- "rootDir": "./src"
10
+ "rootDir": "."
11
11
  },
12
12
  "include": ["src/**/*.ts", "tests/**/*.ts"]
13
13
  }