@drakulavich/parakeet-cli 0.8.0 → 0.8.1

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/parakeet.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env bun
2
- import { runMain } from "citty";
3
- import { mainCommand } from "../src/cli.ts";
4
- runMain(mainCommand);
2
+ import { runCli } from "../src/cli.ts";
3
+
4
+ await runCli();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/parakeet-cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Fast local speech-to-text CLI. CoreML on Apple Silicon, ONNX on CPU. 25 languages.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -19,20 +19,52 @@ export function checkLanguageMismatch(expected: string | undefined, detected: st
19
19
  return `warning: expected language "${expected}" but detected "${detected}"`;
20
20
  }
21
21
 
22
+ export interface InstallOptions {
23
+ coreml: boolean;
24
+ onnx: boolean;
25
+ noCache: boolean;
26
+ }
27
+
28
+ interface InstallCommandArgs {
29
+ coreml: boolean;
30
+ onnx: boolean;
31
+ "no-cache": boolean;
32
+ }
33
+
34
+ interface MainCommandArgs {
35
+ _: string[];
36
+ json: boolean;
37
+ lang?: string;
38
+ }
39
+
22
40
  const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
23
41
 
24
- async function performInstall(options: { coreml: boolean; onnx: boolean; noCache: boolean }) {
25
- const { coreml, onnx, noCache } = options;
42
+ export function resolveInstallBackend(options: InstallOptions, macArm64 = isMacArm64()): "coreml" | "onnx" {
43
+ const { coreml, onnx } = options;
44
+
45
+ if (coreml && onnx) {
46
+ throw new Error('Choose only one backend: "--coreml" or "--onnx".');
47
+ }
48
+
49
+ if (coreml) {
50
+ if (!macArm64) {
51
+ throw new Error("CoreML backend is only available on macOS Apple Silicon.");
52
+ }
53
+ return "coreml";
54
+ }
55
+
56
+ if (onnx) {
57
+ return "onnx";
58
+ }
59
+
60
+ return macArm64 ? "coreml" : "onnx";
61
+ }
62
+
63
+ async function performInstall(options: InstallOptions) {
64
+ const { noCache } = options;
26
65
  try {
27
- if (coreml) {
28
- if (!isMacArm64()) {
29
- log.error("CoreML backend is only available on macOS Apple Silicon.");
30
- process.exit(1);
31
- }
32
- await downloadCoreML(noCache);
33
- } else if (onnx) {
34
- await downloadModel(noCache);
35
- } else if (isMacArm64()) {
66
+ const backend = resolveInstallBackend(options);
67
+ if (backend === "coreml") {
36
68
  await downloadCoreML(noCache);
37
69
  } else {
38
70
  await downloadModel(noCache);
@@ -66,18 +98,29 @@ export const installCommand = defineCommand({
66
98
  default: false,
67
99
  },
68
100
  },
69
- async run({ args }) {
101
+ async run({ args }: { args: InstallCommandArgs }) {
70
102
  await performInstall({ coreml: args.coreml, onnx: args.onnx, noCache: args["no-cache"] });
71
103
  },
72
104
  });
73
105
 
106
+ export const statusCommand = defineCommand({
107
+ meta: {
108
+ name: "status",
109
+ description: "Show backend installation status",
110
+ },
111
+ async run() {
112
+ await showStatus();
113
+ },
114
+ });
115
+
74
116
  export const mainCommand = defineCommand({
75
117
  meta: {
76
118
  name: "parakeet",
77
119
  version: pkg.version,
78
120
  description:
79
121
  "Fast local speech-to-text. 25 languages. CoreML on Apple Silicon, ONNX on CPU.\n" +
80
- " Run 'parakeet install [--coreml | --onnx] [--no-cache]' to download models.",
122
+ " Run 'parakeet install [--coreml | --onnx] [--no-cache]' to download models.\n" +
123
+ " Run 'parakeet status' to inspect installed backends.",
81
124
  },
82
125
  args: {
83
126
  json: {
@@ -90,25 +133,8 @@ export const mainCommand = defineCommand({
90
133
  description: "Expected language code (ISO 639-1), warn if mismatch",
91
134
  },
92
135
  },
93
- async run({ args }) {
94
- const positional = args._ as string[];
95
-
96
- // Manual subcommand routing: "parakeet install [flags]"
97
- if (positional[0] === "install") {
98
- const argv = process.argv;
99
- const coreml = argv.includes("--coreml");
100
- const onnx = argv.includes("--onnx");
101
- const noCache = argv.includes("--no-cache");
102
- await performInstall({ coreml, onnx, noCache });
103
- return;
104
- }
105
-
106
- if (positional[0] === "status") {
107
- await showStatus();
108
- return;
109
- }
110
-
111
- const files = positional;
136
+ async run({ args }: { args: MainCommandArgs }) {
137
+ const files = args._;
112
138
 
113
139
  if (files.length === 0) {
114
140
  log.info("Usage: parakeet <audio_file> [audio_file ...]\n parakeet install [--coreml | --onnx] [--no-cache]\n parakeet status");
@@ -144,6 +170,22 @@ export const mainCommand = defineCommand({
144
170
  },
145
171
  });
146
172
 
173
+ export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
174
+ const [firstArg, ...restArgs] = rawArgs;
175
+
176
+ if (firstArg === "install") {
177
+ await runMain(installCommand, { rawArgs: restArgs });
178
+ return;
179
+ }
180
+
181
+ if (firstArg === "status") {
182
+ await runMain(statusCommand, { rawArgs: restArgs });
183
+ return;
184
+ }
185
+
186
+ await runMain(mainCommand, { rawArgs });
187
+ }
188
+
147
189
  export type TranscribeResult = { file: string; text: string; lang: string };
148
190
 
149
191
  export function formatTextOutput(results: TranscribeResult[]): string {
@@ -160,5 +202,5 @@ export function formatJsonOutput(results: TranscribeResult[]): string {
160
202
  }
161
203
 
162
204
  if (import.meta.main) {
163
- runMain(mainCommand);
205
+ await runCli();
164
206
  }
@@ -85,6 +85,18 @@ export function getCoreMLLatestDownloadURL(): string {
85
85
  return `https://github.com/${GITHUB_REPO}/releases/latest/download/${COREML_BINARY_NAME}`;
86
86
  }
87
87
 
88
+ export function isUnreleasedVersion(version: string): boolean {
89
+ return version === "0.0.0" || version.includes("-");
90
+ }
91
+
92
+ export function getCoreMLBinaryDownloadCandidates(version: string): string[] {
93
+ const versionUrl = getCoreMLDownloadURL(version);
94
+ if (isUnreleasedVersion(version)) {
95
+ return [getCoreMLLatestDownloadURL(), versionUrl];
96
+ }
97
+ return [versionUrl];
98
+ }
99
+
88
100
  export function getCoreMLInstallState(opts?: {
89
101
  binPath?: string;
90
102
  exists?: (path: string) => boolean;
@@ -165,24 +177,21 @@ export function classifyCoreMLInstallProbe(exitCode: number, stdout: string): Co
165
177
  }
166
178
 
167
179
  async function fetchCoreMLBinary(): Promise<Response> {
168
- const latestUrl = getCoreMLLatestDownloadURL();
169
- let res = await fetch(latestUrl, { redirect: "follow" });
170
-
171
- if (res.ok) {
172
- return res;
173
- }
174
-
175
180
  const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
176
- const versionUrl = getCoreMLDownloadURL(pkg.version);
177
- res = await fetch(versionUrl, { redirect: "follow" });
181
+ const version = typeof pkg.version === "string" ? pkg.version : "unknown";
178
182
 
179
- if (!res.ok) {
180
- throw new Error(
181
- `Failed to download CoreML binary (HTTP ${res.status})\n No release found matching ${COREML_BINARY_NAME}\n Fix: Check https://github.com/drakulavich/parakeet-cli/releases for available versions\n Or install the ONNX backend instead: parakeet install --onnx`,
182
- );
183
+ let lastStatus: number | null = null;
184
+ for (const url of getCoreMLBinaryDownloadCandidates(version)) {
185
+ const res = await fetch(url, { redirect: "follow" });
186
+ if (res.ok) {
187
+ return res;
188
+ }
189
+ lastStatus = res.status;
183
190
  }
184
191
 
185
- return res;
192
+ throw new Error(
193
+ `Failed to download CoreML binary${lastStatus ? ` (HTTP ${lastStatus})` : ""}\n Requested package version: ${version}\n Fix: Check https://github.com/drakulavich/parakeet-cli/releases for available versions\n Or install the ONNX backend instead: parakeet install --onnx`,
194
+ );
186
195
  }
187
196
 
188
197
  export function getCoreMLInstallStatus(
package/src/status.ts CHANGED
@@ -4,6 +4,9 @@ import { getCoreMLInstallState, getCoreMLInstallStatus, getCoreMLSupportDir, typ
4
4
  import { log } from "./log";
5
5
  import pc from "picocolors";
6
6
 
7
+ export type StatusCoreMLState = CoreMLInstallState | "n/a" | "probe-failed";
8
+ export type StatusPlatform = "mac-arm64" | "other";
9
+
7
10
  export function formatStatusLine(
8
11
  label: string,
9
12
  path: string | null,
@@ -18,17 +21,25 @@ export function formatStatusLine(
18
21
 
19
22
  export interface StatusInfo {
20
23
  onnx: boolean;
21
- coreml: CoreMLInstallState | "n/a";
24
+ coreml: StatusCoreMLState;
22
25
  ffmpeg: boolean;
26
+ platform: StatusPlatform;
23
27
  }
24
28
 
25
29
  export function collectSuggestions(info: StatusInfo): string[] {
26
30
  const suggestions: string[] = [];
27
31
 
28
- if (info.coreml === "missing" || info.coreml === "stale-binary") {
29
- suggestions.push(`Run "parakeet install --coreml" to install the CoreML backend.`);
30
- } else if (info.coreml === "binary-only") {
31
- suggestions.push(`Run "parakeet install --coreml" to download CoreML models.`);
32
+ if (info.platform === "mac-arm64") {
33
+ if (info.coreml === "missing") {
34
+ suggestions.push(`Run "parakeet install --coreml" to install the CoreML backend.`);
35
+ } else if (info.coreml === "binary-only") {
36
+ suggestions.push(`Run "parakeet install --coreml" to download CoreML models.`);
37
+ } else if (info.coreml === "stale-binary") {
38
+ suggestions.push(`Run "parakeet install --coreml --no-cache" to refresh the incompatible CoreML binary.`);
39
+ } else if (info.coreml === "probe-failed") {
40
+ suggestions.push(`Run "parakeet install --coreml --no-cache" to refresh the CoreML backend and restore status checks.`);
41
+ }
42
+ return suggestions;
32
43
  }
33
44
 
34
45
  if (!info.onnx) {
@@ -71,29 +82,67 @@ function defaultDeps(): StatusDeps {
71
82
  };
72
83
  }
73
84
 
85
+ function getCoreMLBinaryDisplay(state: StatusCoreMLState): { installed: boolean; missingLabel: string } {
86
+ switch (state) {
87
+ case "ready":
88
+ case "binary-only":
89
+ return { installed: true, missingLabel: "not installed" };
90
+ case "stale-binary":
91
+ return { installed: false, missingLabel: "stale binary" };
92
+ case "probe-failed":
93
+ return { installed: false, missingLabel: "probe failed" };
94
+ case "missing":
95
+ case "n/a":
96
+ return { installed: false, missingLabel: "not installed" };
97
+ }
98
+ }
99
+
100
+ function getCoreMLModelsDisplay(state: StatusCoreMLState): { installed: boolean; missingLabel: string } {
101
+ switch (state) {
102
+ case "ready":
103
+ return { installed: true, missingLabel: "not installed" };
104
+ case "stale-binary":
105
+ return { installed: false, missingLabel: "reinstall required" };
106
+ case "probe-failed":
107
+ return { installed: false, missingLabel: "status unknown" };
108
+ case "binary-only":
109
+ case "missing":
110
+ case "n/a":
111
+ return { installed: false, missingLabel: "not installed" };
112
+ }
113
+ }
114
+
74
115
  export async function showStatus(deps?: Partial<StatusDeps>): Promise<void> {
75
116
  const d = { ...defaultDeps(), ...deps };
76
117
 
77
118
  const isMac = d.isMacArm64();
119
+ const platform: StatusPlatform = isMac ? "mac-arm64" : "other";
78
120
 
79
121
  // CoreML status
80
- let coremlState: CoreMLInstallState | "n/a" = "n/a";
122
+ let coremlState: StatusCoreMLState = "n/a";
123
+ let coremlProbeError: string | null = null;
81
124
  if (isMac) {
82
125
  const binPath = d.getCoreMLBinPath();
83
126
  try {
84
127
  coremlState = d.getCoreMLState(binPath);
85
- } catch {
86
- coremlState = "missing";
128
+ } catch (error: unknown) {
129
+ coremlState = "probe-failed";
130
+ coremlProbeError = error instanceof Error ? error.message : String(error);
87
131
  }
88
132
 
89
133
  log.info("CoreML (macOS Apple Silicon):");
90
- const binInstalled = coremlState !== "missing";
91
- log.info(formatStatusLine("Binary", binInstalled ? binPath : null, binInstalled));
134
+ const binaryDisplay = getCoreMLBinaryDisplay(coremlState);
135
+ log.info(formatStatusLine("Binary", coremlState === "missing" ? null : binPath, binaryDisplay.installed, binaryDisplay.missingLabel));
92
136
 
93
- const modelsInstalled = coremlState === "ready";
137
+ const modelsDisplay = getCoreMLModelsDisplay(coremlState);
94
138
  const modelDir = d.getCoreMLSupportDir();
95
- log.info(formatStatusLine("Models", modelsInstalled ? modelDir : null, modelsInstalled));
139
+ const modelsPath = (coremlState === "ready" || coremlState === "stale-binary" || coremlState === "probe-failed") ? modelDir : null;
140
+ log.info(formatStatusLine("Models", modelsPath, modelsDisplay.installed, modelsDisplay.missingLabel));
96
141
  log.info("");
142
+
143
+ if (coremlProbeError) {
144
+ log.warn(`CoreML status probe failed: ${coremlProbeError}`);
145
+ }
97
146
  }
98
147
 
99
148
  // ONNX status
@@ -117,6 +166,7 @@ export async function showStatus(deps?: Partial<StatusDeps>): Promise<void> {
117
166
  onnx: onnxInstalled,
118
167
  coreml: coremlState,
119
168
  ffmpeg: !!ffmpegPath,
169
+ platform,
120
170
  });
121
171
 
122
172
  for (const suggestion of suggestions) {