@drakulavich/kesha-voice-kit 1.2.0 → 1.2.2

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/kesha-voice-kit",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Open-source voice toolkit for Apple Silicon. Speech-to-text, language detection. 25 languages.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -4,7 +4,8 @@ import { defineCommand, runMain } from "citty";
4
4
  import { detect } from "tinyld";
5
5
  import { transcribe } from "./lib";
6
6
  import { downloadEngine } from "./engine-install";
7
- import { detectAudioLanguageEngine, detectTextLanguageEngine } from "./engine";
7
+ import { detectAudioLanguageEngine, detectTextLanguageEngine, getEngineBinPath } from "./engine";
8
+ import { readStarSeen, shouldShowStarPrompt, writeStarSeen } from "./star";
8
9
  import type { LangDetectResult } from "./engine";
9
10
  import { log } from "./log";
10
11
  import { say, SayError } from "./say";
@@ -50,6 +51,24 @@ function resolveBackendFlag(coreml: boolean, onnx: boolean): string | undefined
50
51
  }
51
52
 
52
53
  async function askForStar() {
54
+ // Gate on major-or-minor bump only — patch releases and re-installs of the
55
+ // same version shouldn't nag. First-ever install (no marker) still prompts.
56
+ const currentVersion = typeof pkg.version === "string" ? pkg.version : null;
57
+ if (!currentVersion) return;
58
+ const binPath = getEngineBinPath();
59
+ const seen = readStarSeen(binPath);
60
+ if (!shouldShowStarPrompt(currentVersion, seen)) {
61
+ return;
62
+ }
63
+ // Record the version up front so a single run never prompts twice, even
64
+ // if the gh subprocess below throws.
65
+ try {
66
+ writeStarSeen(binPath, currentVersion);
67
+ } catch {
68
+ // Non-fatal — falling through to the prompt is still OK, just means we
69
+ // may nag again on the next install if the write failed for IO reasons.
70
+ }
71
+
53
72
  const gh = Bun.which("gh");
54
73
  if (!gh) {
55
74
  log.info("\nIf you enjoy Kesha Voice Kit, consider starring the repo:");
@@ -1,11 +1,35 @@
1
1
  import { dirname } from "path";
2
- import { existsSync, mkdirSync, chmodSync } from "fs";
2
+ import { existsSync, mkdirSync, chmodSync, readFileSync, writeFileSync } from "fs";
3
3
  import { getEngineBinPath, getEngineCapabilities } from "./engine";
4
4
  import { log } from "./log";
5
5
  import { streamResponseToFile } from "./progress";
6
6
 
7
7
  const GITHUB_REPO = "drakulavich/kesha-voice-kit";
8
8
 
9
+ /**
10
+ * Version-marker file written next to the engine binary on download (#151).
11
+ * Lets `kesha install` detect a stale cached engine after a CLI upgrade and
12
+ * re-download without requiring `--no-cache`.
13
+ */
14
+ export function getVersionMarkerPath(binPath: string): string {
15
+ return `${binPath}.version`;
16
+ }
17
+
18
+ /** Read the recorded version for the installed binary, or null if missing / empty. */
19
+ export function readInstalledEngineVersion(binPath: string): string | null {
20
+ try {
21
+ const v = readFileSync(getVersionMarkerPath(binPath), "utf-8").trim();
22
+ return v.length > 0 ? v : null;
23
+ } catch {
24
+ // Missing, unreadable, or permission-denied → treat as no marker so we re-download.
25
+ return null;
26
+ }
27
+ }
28
+
29
+ export function writeInstalledEngineVersion(binPath: string, version: string): void {
30
+ writeFileSync(getVersionMarkerPath(binPath), `${version}\n`);
31
+ }
32
+
9
33
  function getEngineBinaryName(): string {
10
34
  const platform = process.platform;
11
35
  const arch = process.arch;
@@ -28,21 +52,31 @@ export async function downloadEngine(
28
52
  options: InstallOptions = {},
29
53
  ): Promise<string> {
30
54
  const binPath = getEngineBinPath();
55
+ const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
56
+ // The engine version is tracked separately from the CLI version so
57
+ // CLI-only patch releases don't require cutting a new GitHub release
58
+ // + Rust rebuild. Fall back to the CLI version for backwards compat.
59
+ const engineVersion =
60
+ typeof pkg.keshaEngine?.version === "string"
61
+ ? pkg.keshaEngine.version
62
+ : typeof pkg.version === "string"
63
+ ? pkg.version
64
+ : "unknown";
31
65
 
32
- if (!noCache && existsSync(binPath)) {
33
- log.success("Engine binary already installed.");
66
+ const installedVersion = readInstalledEngineVersion(binPath);
67
+ const cacheValid =
68
+ !noCache && existsSync(binPath) && installedVersion === engineVersion;
69
+
70
+ if (cacheValid) {
71
+ log.success(`Engine binary already installed (v${engineVersion}).`);
34
72
  } else {
73
+ // Log why we're downloading — helps diagnose surprising re-downloads.
74
+ if (existsSync(binPath) && installedVersion && installedVersion !== engineVersion) {
75
+ log.progress(
76
+ `Upgrading engine v${installedVersion} → v${engineVersion}...`,
77
+ );
78
+ }
35
79
  const binaryName = getEngineBinaryName();
36
- const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
37
- // The engine version is tracked separately from the CLI version so
38
- // CLI-only patch releases don't require cutting a new GitHub release
39
- // + Rust rebuild. Fall back to the CLI version for backwards compat.
40
- const engineVersion =
41
- typeof pkg.keshaEngine?.version === "string"
42
- ? pkg.keshaEngine.version
43
- : typeof pkg.version === "string"
44
- ? pkg.version
45
- : "unknown";
46
80
  const url = `https://github.com/${GITHUB_REPO}/releases/download/v${engineVersion}/${binaryName}`;
47
81
 
48
82
  mkdirSync(dirname(binPath), { recursive: true });
@@ -64,7 +98,8 @@ export async function downloadEngine(
64
98
 
65
99
  await streamResponseToFile(res, binPath, "kesha-engine binary");
66
100
  chmodSync(binPath, 0o755);
67
- log.success("Engine binary downloaded.");
101
+ writeInstalledEngineVersion(binPath, engineVersion);
102
+ log.success(`Engine binary downloaded (v${engineVersion}).`);
68
103
  }
69
104
 
70
105
  if (backend) {
package/src/star.ts ADDED
@@ -0,0 +1,57 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "fs";
2
+
3
+ /**
4
+ * Version-bump gate for the "star the repo" prompt in `kesha install`.
5
+ *
6
+ * Prompts are valuable on first install and on meaningful upgrades (new
7
+ * features) but annoying on patch-only bumps. This module persists the
8
+ * last-seen version to a marker file next to the engine binary and only
9
+ * returns true on a major-or-minor bump.
10
+ */
11
+ export function starSeenPath(binPath: string): string {
12
+ return `${binPath}.star-seen`;
13
+ }
14
+
15
+ export function readStarSeen(binPath: string): string | null {
16
+ try {
17
+ const v = readFileSync(starSeenPath(binPath), "utf-8").trim();
18
+ return v.length > 0 ? v : null;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ export function writeStarSeen(binPath: string, version: string): void {
25
+ writeFileSync(starSeenPath(binPath), `${version}\n`);
26
+ }
27
+
28
+ function parseMajorMinor(v: string): [number, number] | null {
29
+ const parts = v.split(".");
30
+ if (parts.length < 2) return null;
31
+ const major = Number(parts[0]);
32
+ const minor = Number(parts[1]);
33
+ if (!Number.isFinite(major) || !Number.isFinite(minor)) return null;
34
+ return [major, minor];
35
+ }
36
+
37
+ /**
38
+ * Returns true iff `current` represents a major-or-minor bump over `seen`.
39
+ * - `seen === null` → true (first install, always prompt once).
40
+ * - Same or downgraded version → false.
41
+ * - Patch-only bump → false (annoying on every install).
42
+ * - Unparseable either side → false (don't nag when we can't reason).
43
+ */
44
+ export function shouldShowStarPrompt(current: string, seen: string | null): boolean {
45
+ if (seen === null) return true;
46
+ const c = parseMajorMinor(current);
47
+ const s = parseMajorMinor(seen);
48
+ if (!c || !s) return false;
49
+ if (c[0] > s[0]) return true;
50
+ if (c[0] === s[0] && c[1] > s[1]) return true;
51
+ return false;
52
+ }
53
+
54
+ /** True when a star-seen marker already exists for this install. */
55
+ export function hasStarMarker(binPath: string): boolean {
56
+ return existsSync(starSeenPath(binPath));
57
+ }