feat(39-02): voice capability probe in hardware service
- Add VoiceCapability interface with whisperAvailable, piperAvailable, voiceTierSufficient - Extend HardwareInfo with voiceCapability field - Add detectVoiceCapability() probing whisper-cpp/whisper and piper with 2s timeout each - voiceTierSufficient: true for apple_silicon/gpu, or cpu_only with >= 4GB free RAM - Wrap voice probe in 3s timeout to avoid slowing hardware detection - Route automatically includes voiceCapability via existing HardwareInfo return
This commit is contained in:
parent
cc05befdb0
commit
3673fa03a1
1 changed files with 88 additions and 0 deletions
|
|
@ -1,8 +1,16 @@
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
import { execFile as execFileCb } from "node:child_process";
|
||||||
import si from "systeminformation";
|
import si from "systeminformation";
|
||||||
|
|
||||||
export type HardwareTier = "gpu" | "apple_silicon" | "cpu_only";
|
export type HardwareTier = "gpu" | "apple_silicon" | "cpu_only";
|
||||||
|
|
||||||
|
export interface VoiceCapability {
|
||||||
|
whisperAvailable: boolean;
|
||||||
|
piperAvailable: boolean;
|
||||||
|
/** true if hardware tier >= apple_silicon OR (cpu_only with >= 4GB free RAM) */
|
||||||
|
voiceTierSufficient: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HardwareInfo {
|
export interface HardwareInfo {
|
||||||
totalGb: number;
|
totalGb: number;
|
||||||
freeGb: number;
|
freeGb: number;
|
||||||
|
|
@ -13,6 +21,7 @@ export interface HardwareInfo {
|
||||||
unifiedMemory: boolean;
|
unifiedMemory: boolean;
|
||||||
hardwareTier: HardwareTier;
|
hardwareTier: HardwareTier;
|
||||||
cpuModel: string | null;
|
cpuModel: string | null;
|
||||||
|
voiceCapability: VoiceCapability;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
@ -26,6 +35,63 @@ export function _resetHardwareCache(): void {
|
||||||
cacheExpiry = 0;
|
cacheExpiry = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Promisified execFile wrapper for consistent mocking in tests. */
|
||||||
|
function execFileAsync(
|
||||||
|
cmd: string,
|
||||||
|
args: string[],
|
||||||
|
opts: { timeout?: number }
|
||||||
|
): Promise<{ stdout: string; stderr: string }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
execFileCb(cmd, args, opts as never, (err, stdout, stderr) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
stdout: Buffer.isBuffer(stdout) ? stdout.toString() : String(stdout ?? ""),
|
||||||
|
stderr: Buffer.isBuffer(stderr) ? stderr.toString() : String(stderr ?? ""),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Probe for Whisper and Piper binary availability. */
|
||||||
|
async function detectVoiceCapability(
|
||||||
|
hardwareTier: HardwareTier,
|
||||||
|
freeGb: number
|
||||||
|
): Promise<VoiceCapability> {
|
||||||
|
// Probe whisper-cpp first, fall back to whisper (openai-whisper)
|
||||||
|
let whisperAvailable = false;
|
||||||
|
try {
|
||||||
|
await execFileAsync("whisper-cpp", ["--help"], { timeout: 2000 });
|
||||||
|
whisperAvailable = true;
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
await execFileAsync("whisper", ["--help"], { timeout: 2000 });
|
||||||
|
whisperAvailable = true;
|
||||||
|
} catch {
|
||||||
|
whisperAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probe piper
|
||||||
|
let piperAvailable = false;
|
||||||
|
try {
|
||||||
|
await execFileAsync("piper", ["--help"], { timeout: 2000 });
|
||||||
|
piperAvailable = true;
|
||||||
|
} catch {
|
||||||
|
piperAvailable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// voiceTierSufficient: true for apple_silicon/gpu, or cpu_only with >= 4GB free
|
||||||
|
const voiceTierSufficient =
|
||||||
|
hardwareTier === "apple_silicon" ||
|
||||||
|
hardwareTier === "gpu" ||
|
||||||
|
(hardwareTier === "cpu_only" && freeGb >= 4);
|
||||||
|
|
||||||
|
return { whisperAvailable, piperAvailable, voiceTierSufficient };
|
||||||
|
}
|
||||||
|
|
||||||
export function hardwareService() {
|
export function hardwareService() {
|
||||||
async function detect(): Promise<HardwareInfo> {
|
async function detect(): Promise<HardwareInfo> {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
@ -43,6 +109,9 @@ export function hardwareService() {
|
||||||
|
|
||||||
// Apple Silicon detection: darwin platform + CPU brand starting with "Apple"
|
// Apple Silicon detection: darwin platform + CPU brand starting with "Apple"
|
||||||
if (platform === "darwin" && cpuModel?.startsWith("Apple")) {
|
if (platform === "darwin" && cpuModel?.startsWith("Apple")) {
|
||||||
|
const voiceCapability = await withVoiceTimeout(
|
||||||
|
detectVoiceCapability("apple_silicon", freeGb)
|
||||||
|
);
|
||||||
const info: HardwareInfo = {
|
const info: HardwareInfo = {
|
||||||
totalGb,
|
totalGb,
|
||||||
freeGb,
|
freeGb,
|
||||||
|
|
@ -53,6 +122,7 @@ export function hardwareService() {
|
||||||
unifiedMemory: true,
|
unifiedMemory: true,
|
||||||
hardwareTier: "apple_silicon",
|
hardwareTier: "apple_silicon",
|
||||||
cpuModel,
|
cpuModel,
|
||||||
|
voiceCapability,
|
||||||
};
|
};
|
||||||
cache = info;
|
cache = info;
|
||||||
cacheExpiry = now + CACHE_TTL_MS;
|
cacheExpiry = now + CACHE_TTL_MS;
|
||||||
|
|
@ -84,6 +154,10 @@ export function hardwareService() {
|
||||||
hardwareTier = "cpu_only";
|
hardwareTier = "cpu_only";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const voiceCapability = await withVoiceTimeout(
|
||||||
|
detectVoiceCapability(hardwareTier, freeGb)
|
||||||
|
);
|
||||||
|
|
||||||
const info: HardwareInfo = {
|
const info: HardwareInfo = {
|
||||||
totalGb,
|
totalGb,
|
||||||
freeGb,
|
freeGb,
|
||||||
|
|
@ -94,6 +168,7 @@ export function hardwareService() {
|
||||||
unifiedMemory: false,
|
unifiedMemory: false,
|
||||||
hardwareTier,
|
hardwareTier,
|
||||||
cpuModel,
|
cpuModel,
|
||||||
|
voiceCapability,
|
||||||
};
|
};
|
||||||
|
|
||||||
cache = info;
|
cache = info;
|
||||||
|
|
@ -103,3 +178,16 @@ export function hardwareService() {
|
||||||
|
|
||||||
return { detect };
|
return { detect };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Wrap voice capability probe with 3s timeout; return safe defaults on failure. */
|
||||||
|
async function withVoiceTimeout(
|
||||||
|
probe: Promise<VoiceCapability>
|
||||||
|
): Promise<VoiceCapability> {
|
||||||
|
const timeout = new Promise<VoiceCapability>((resolve) =>
|
||||||
|
setTimeout(
|
||||||
|
() => resolve({ whisperAvailable: false, piperAvailable: false, voiceTierSufficient: false }),
|
||||||
|
3000
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return Promise.race([probe, timeout]);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue