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 { execFile as execFileCb } from "node:child_process";
|
||||
import si from "systeminformation";
|
||||
|
||||
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 {
|
||||
totalGb: number;
|
||||
freeGb: number;
|
||||
|
|
@ -13,6 +21,7 @@ export interface HardwareInfo {
|
|||
unifiedMemory: boolean;
|
||||
hardwareTier: HardwareTier;
|
||||
cpuModel: string | null;
|
||||
voiceCapability: VoiceCapability;
|
||||
}
|
||||
|
||||
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
|
@ -26,6 +35,63 @@ export function _resetHardwareCache(): void {
|
|||
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() {
|
||||
async function detect(): Promise<HardwareInfo> {
|
||||
const now = Date.now();
|
||||
|
|
@ -43,6 +109,9 @@ export function hardwareService() {
|
|||
|
||||
// Apple Silicon detection: darwin platform + CPU brand starting with "Apple"
|
||||
if (platform === "darwin" && cpuModel?.startsWith("Apple")) {
|
||||
const voiceCapability = await withVoiceTimeout(
|
||||
detectVoiceCapability("apple_silicon", freeGb)
|
||||
);
|
||||
const info: HardwareInfo = {
|
||||
totalGb,
|
||||
freeGb,
|
||||
|
|
@ -53,6 +122,7 @@ export function hardwareService() {
|
|||
unifiedMemory: true,
|
||||
hardwareTier: "apple_silicon",
|
||||
cpuModel,
|
||||
voiceCapability,
|
||||
};
|
||||
cache = info;
|
||||
cacheExpiry = now + CACHE_TTL_MS;
|
||||
|
|
@ -84,6 +154,10 @@ export function hardwareService() {
|
|||
hardwareTier = "cpu_only";
|
||||
}
|
||||
|
||||
const voiceCapability = await withVoiceTimeout(
|
||||
detectVoiceCapability(hardwareTier, freeGb)
|
||||
);
|
||||
|
||||
const info: HardwareInfo = {
|
||||
totalGb,
|
||||
freeGb,
|
||||
|
|
@ -94,6 +168,7 @@ export function hardwareService() {
|
|||
unifiedMemory: false,
|
||||
hardwareTier,
|
||||
cpuModel,
|
||||
voiceCapability,
|
||||
};
|
||||
|
||||
cache = info;
|
||||
|
|
@ -103,3 +178,16 @@ export function hardwareService() {
|
|||
|
||||
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