chore(lint): remove explicit any in server (129 warnings)

- session.ts: catch(error: any) -> catch(error) with Error coercion at use
- daemon-client transport: any -> unknown in listener types
- sherpa/onnx loaders: introduce structural native types
- pocket-tts-onnx: typed ONNX session inputs/outputs/tensors
- tests: any -> unknown + named stub types
This commit is contained in:
Mohamed Boudra
2026-04-24 02:40:02 +07:00
parent 032de17537
commit 58d92f501b
22 changed files with 299 additions and 155 deletions

View File

@@ -22,15 +22,15 @@ export interface WebSocketLike {
send: (data: string | Uint8Array | ArrayBuffer) => void;
close: (code?: number, reason?: string) => void;
binaryType?: string;
on?: (event: string, listener: (...args: any[]) => void) => void;
off?: (event: string, listener: (...args: any[]) => void) => void;
removeListener?: (event: string, listener: (...args: any[]) => void) => void;
addEventListener?: (event: string, listener: (event: any) => void) => void;
removeEventListener?: (event: string, listener: (event: any) => void) => void;
onopen?: ((event: any) => void) | null;
onclose?: ((event: any) => void) | null;
onerror?: ((event: any) => void) | null;
onmessage?: ((event: any) => void) | null;
on?: (event: string, listener: (...args: unknown[]) => void) => void;
off?: (event: string, listener: (...args: unknown[]) => void) => void;
removeListener?: (event: string, listener: (...args: unknown[]) => void) => void;
addEventListener?: (event: string, listener: (event: unknown) => void) => void;
removeEventListener?: (event: string, listener: (event: unknown) => void) => void;
onopen?: ((event: unknown) => void) | null;
onclose?: ((event: unknown) => void) | null;
onerror?: ((event: unknown) => void) | null;
onmessage?: ((event: unknown) => void) | null;
}
export interface TransportLogger {

View File

@@ -89,12 +89,12 @@ describe("daemon-client transport helpers", () => {
});
test("createWebSocketTransportFactory binds and unbinds event listeners", () => {
const listeners = new Map<string, (...args: any[]) => void>();
const listeners = new Map<string, (...args: unknown[]) => void>();
const ws = {
readyState: 1,
send: vi.fn(),
close: vi.fn(),
addEventListener: vi.fn((event: string, handler: (...args: any[]) => void) => {
addEventListener: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
listeners.set(event, handler);
}),
removeEventListener: vi.fn((event: string) => {

View File

@@ -8,7 +8,7 @@ export function defaultWebSocketFactory(
url: string,
_options?: { headers?: Record<string, string> },
): WebSocketLike {
const globalWs = (globalThis as { WebSocket?: any }).WebSocket;
const globalWs = (globalThis as { WebSocket?: new (url: string) => WebSocketLike }).WebSocket;
if (!globalWs) {
throw new Error("WebSocket is not available in this runtime");
}
@@ -102,7 +102,7 @@ function bindTemporaryEarlyCloseErrorHandler(ws: WebSocketLike): () => void {
export function bindWsHandler(
ws: WebSocketLike,
event: "open" | "close" | "error" | "message",
handler: (...args: any[]) => void,
handler: (...args: unknown[]) => void,
): () => void {
if (typeof ws.addEventListener === "function") {
ws.addEventListener(event, handler);
@@ -125,11 +125,12 @@ export function bindWsHandler(
};
}
const prop = `on${event}` as "onopen" | "onclose" | "onerror" | "onmessage";
const previous = (ws as any)[prop];
(ws as any)[prop] = handler;
const wsRecord = ws as unknown as Record<string, unknown>;
const previous = wsRecord[prop];
wsRecord[prop] = handler;
return () => {
if ((ws as any)[prop] === handler) {
(ws as any)[prop] = previous ?? null;
if (wsRecord[prop] === handler) {
wsRecord[prop] = previous ?? null;
}
};
}

View File

@@ -159,7 +159,10 @@ describe("agent metadata generator auto-title", () => {
deps: {
generateStructuredAgentResponseWithFallback: generateStructured,
renameCurrentBranch,
workspaceGitService: workspaceGitService as any,
workspaceGitService: workspaceGitService as unknown as Pick<
import("../workspace-git-service.js").WorkspaceGitService,
"getSnapshot"
>,
},
});
@@ -198,7 +201,10 @@ describe("agent metadata generator auto-title", () => {
deps: {
generateStructuredAgentResponseWithFallback: generateStructured,
renameCurrentBranch,
workspaceGitService: workspaceGitService as any,
workspaceGitService: workspaceGitService as unknown as Pick<
import("../workspace-git-service.js").WorkspaceGitService,
"getSnapshot"
>,
},
});

View File

@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { AGENT_LIFECYCLE_STATUSES, type AgentLifecycleStatus } from "./agent-manager.js";
import { toAgentPayload, toStoredAgentRecord, type ManagedAgent } from "./agent-projections.js";
import type { AgentSession } from "./agent-sdk-types.js";
import type {
AgentFeature,
AgentPermissionRequest,
@@ -40,7 +41,8 @@ function createManagedAgent(overrides: ManagedAgentOverrides = {}): ManagedAgent
...restOverrides
} = overrides;
const sessionValue = lifecycle === "closed" ? null : (restOverrides.session ?? ({} as any));
const sessionValue =
lifecycle === "closed" ? null : (restOverrides.session ?? ({} as AgentSession));
const activeForegroundTurnIdValue =
restOverrides.activeForegroundTurnId ?? (lifecycle === "running" ? "test-turn-id" : null);
const lastErrorValue =
@@ -250,7 +252,7 @@ describe("toAgentPayload", () => {
expect(payload.lastUsage).toEqual(agent.lastUsage);
expect(payload.lastUsage).not.toBe(agent.lastUsage);
expect(payload.lastError).toBe("boom");
expect((payload as any).session).toBeUndefined();
expect((payload as unknown as { session?: unknown }).session).toBeUndefined();
payload.availableModes[0].label = "Changed";
expect(agent.availableModes[0].label).toBe("Planning");
@@ -310,7 +312,7 @@ describe("toAgentPayload", () => {
persistence: {
provider: "codex",
sessionId: "persist-99",
nativeHandle: { id: "native" } as any,
nativeHandle: { id: "native" } as unknown,
metadata: { restored: new Date("2025-03-01T00:00:00.000Z"), empty: {} },
},
});

View File

@@ -71,6 +71,10 @@ it("does not notify archived callers", async () => {
expect(agentStorage.get).toHaveBeenCalledWith("caller-agent");
});
expect((agentManager as any).streamAgent).not.toHaveBeenCalled();
expect((agentManager as any).replaceAgentRun).not.toHaveBeenCalled();
expect(
(agentManager as unknown as { streamAgent: ReturnType<typeof vi.fn> }).streamAgent,
).not.toHaveBeenCalled();
expect(
(agentManager as unknown as { replaceAgentRun: ReturnType<typeof vi.fn> }).replaceAgentRun,
).not.toHaveBeenCalled();
});

View File

@@ -1,5 +1,5 @@
import express from "express";
import { createServer as createHTTPServer } from "http";
import { createServer as createHTTPServer, type IncomingMessage, type ServerResponse } from "http";
import { createReadStream, unlinkSync, existsSync } from "fs";
import { stat } from "fs/promises";
import { randomUUID } from "node:crypto";
@@ -644,7 +644,11 @@ export async function createPaseoDaemon(
transport = await createAgentMcpTransport(callerAgentId);
}
await transport.handleRequest(req as any, res as any, req.body);
await transport.handleRequest(
req as unknown as IncomingMessage,
res as unknown as ServerResponse,
req.body,
);
} catch (err) {
logger.error({ err }, "Failed to handle Agent MCP request");
if (!res.headersSent) {

View File

@@ -10,7 +10,9 @@ vi.mock("./checkout-git-utils.js", () => ({
toCheckoutError: toCheckoutErrorMock,
}));
import type pino from "pino";
import { CheckoutDiffManager } from "./checkout-diff-manager.js";
import type { WorkspaceGitService } from "./workspace-git-service.js";
describe("CheckoutDiffManager", () => {
beforeEach(() => {
@@ -54,9 +56,9 @@ describe("CheckoutDiffManager", () => {
};
const manager = new CheckoutDiffManager({
logger: logger as any,
logger: logger as unknown as pino.Logger,
paseoHome: "/tmp/paseo-test",
workspaceGitService: workspaceGitService as any,
workspaceGitService: workspaceGitService as unknown as WorkspaceGitService,
});
return {

View File

@@ -26,7 +26,7 @@ function findTimelineToolCall(
if (msg.payload.agentId !== agentId) {
continue;
}
const event = msg.payload.event as any;
const event = msg.payload.event as { type?: string; item?: AgentTimelineItem };
if (event?.type !== "timeline") {
continue;
}
@@ -57,7 +57,7 @@ async function waitForTimelineToolCall(
const msg = messages[i];
if (msg?.type !== "agent_stream") continue;
if (msg.payload.agentId !== agentId) continue;
const event = msg.payload.event as any;
const event = msg.payload.event as { type?: string; item?: AgentTimelineItem };
if (event?.type !== "timeline") continue;
const item = event.item as AgentTimelineItem;
if (item?.type !== "tool_call") continue;

View File

@@ -13,7 +13,7 @@ const wsMock = vi.hoisted(() => {
readyState = MockWebSocket.CONNECTING;
sent: string[] = [];
terminateCalls = 0;
private listeners = new Map<string, Array<(...args: any[]) => void>>();
private listeners = new Map<string, Array<(...args: unknown[]) => void>>();
constructor(url: string, options?: unknown) {
this.url = url;
@@ -25,15 +25,15 @@ const wsMock = vi.hoisted(() => {
MockWebSocket.instances = [];
}
on(event: string, listener: (...args: any[]) => void) {
on(event: string, listener: (...args: unknown[]) => void) {
const handlers = this.listeners.get(event) ?? [];
handlers.push(listener);
this.listeners.set(event, handlers);
return this;
}
once(event: string, listener: (...args: any[]) => void) {
const wrapped = (...args: any[]) => {
once(event: string, listener: (...args: unknown[]) => void) {
const wrapped = (...args: unknown[]) => {
this.off(event, wrapped);
listener(...args);
};
@@ -71,7 +71,7 @@ const wsMock = vi.hoisted(() => {
this.emit("error", err);
}
private off(event: string, listener: (...args: any[]) => void) {
private off(event: string, listener: (...args: unknown[]) => void) {
const handlers = this.listeners.get(event) ?? [];
this.listeners.set(
event,
@@ -79,7 +79,7 @@ const wsMock = vi.hoisted(() => {
);
}
private emit(event: string, ...args: any[]) {
private emit(event: string, ...args: unknown[]) {
const handlers = this.listeners.get(event) ?? [];
for (const handler of [...handlers]) {
handler(...args);
@@ -92,6 +92,7 @@ const wsMock = vi.hoisted(() => {
vi.mock("ws", () => ({ default: wsMock.MockWebSocket }));
import type pino from "pino";
import { startRelayTransport } from "./relay-transport";
function createMockLogger() {
@@ -128,7 +129,7 @@ describe("relay-transport control lifecycle", () => {
test("logs relay_control_connected only after first valid control message", () => {
const logger = createMockLogger();
const controller = startRelayTransport({
logger: logger as any,
logger: logger as unknown as pino.Logger,
attachSocket: async () => {},
relayEndpoint: "relay.paseo.sh:443",
serverId: "srv_test",
@@ -150,7 +151,7 @@ describe("relay-transport control lifecycle", () => {
vi.useFakeTimers();
const logger = createMockLogger();
const controller = startRelayTransport({
logger: logger as any,
logger: logger as unknown as pino.Logger,
attachSocket: async () => {},
relayEndpoint: "relay.paseo.sh:443",
serverId: "srv_test",
@@ -172,7 +173,7 @@ describe("relay-transport control lifecycle", () => {
vi.useFakeTimers();
const logger = createMockLogger();
const controller = startRelayTransport({
logger: logger as any,
logger: logger as unknown as pino.Logger,
attachSocket: async () => {},
relayEndpoint: "relay.paseo.sh:443",
serverId: "srv_test",
@@ -193,7 +194,7 @@ describe("relay-transport control lifecycle", () => {
const logger = createMockLogger();
const attachSocket = vi.fn(async () => {});
const controller = startRelayTransport({
logger: logger as any,
logger: logger as unknown as pino.Logger,
attachSocket,
relayEndpoint: "relay.paseo.sh:443",
serverId: "srv_test",

View File

@@ -1642,7 +1642,7 @@ export class Session {
);
try {
await this.dispatchInboundMessage(msg);
} catch (error: any) {
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
this.sessionLogger.error({ err }, "Error handling message");
@@ -2131,7 +2131,7 @@ export class Session {
try {
await this.agentManager.closeAgent(agentId);
} catch (error: any) {
} catch (error) {
this.sessionLogger.warn(
{ err: error, agentId },
`Failed to close agent ${agentId} during delete`,
@@ -2145,7 +2145,7 @@ export class Session {
try {
await this.agentStorage.remove(agentId);
await this.agentManager.deleteCommittedTimeline(agentId);
} catch (error: any) {
} catch (error) {
this.sessionLogger.error({ err: error, agentId }, `Failed to fully delete agent ${agentId}`);
}
@@ -2281,7 +2281,7 @@ export class Session {
for (const terminalId of msg.terminalIds) {
try {
terminals.push(this.killTerminalForClose(terminalId));
} catch (error: any) {
} catch (error) {
this.sessionLogger.warn(
{ err: error, terminalId, requestId: msg.requestId },
"Failed to kill terminal during close_items batch",
@@ -2358,7 +2358,7 @@ export class Session {
type: "update_agent_response",
payload: { requestId, agentId, accepted: true, error: null },
});
} catch (error: any) {
} catch (error) {
this.sessionLogger.error(
{ err: error, agentId, requestId },
"session: update_agent_request error",
@@ -2369,7 +2369,7 @@ export class Session {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to update agent: ${error.message}`,
content: `Failed to update agent: ${(error as Error).message}`,
},
});
this.emit({
@@ -2378,7 +2378,9 @@ export class Session {
requestId,
agentId,
accepted: false,
error: error?.message ? String(error.message) : "Failed to update agent",
error: (error as Error | undefined)?.message
? String((error as Error).message)
: "Failed to update agent",
},
});
}
@@ -2943,7 +2945,7 @@ export class Session {
{ agentId: snapshot.id, provider: snapshot.provider },
`Created agent ${snapshot.id} (${snapshot.provider})`,
);
} catch (error: any) {
} catch (error) {
const wireError = toWorktreeWireError(error);
this.sessionLogger.error({ err: error }, "Failed to create agent");
if (requestId) {
@@ -3052,7 +3054,7 @@ export class Session {
},
});
}
} catch (error: any) {
} catch (error) {
this.sessionLogger.error({ err: error }, "Failed to resume agent");
this.emit({
type: "activity_log",
@@ -3060,7 +3062,7 @@ export class Session {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to resume agent: ${error.message}`,
content: `Failed to resume agent: ${(error as Error).message}`,
},
});
}
@@ -3113,7 +3115,7 @@ export class Session {
},
});
}
} catch (error: any) {
} catch (error) {
this.sessionLogger.error({ err: error, agentId }, `Failed to refresh agent ${agentId}`);
this.emit({
type: "activity_log",
@@ -3121,7 +3123,7 @@ export class Session {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to refresh agent: ${error.message}`,
content: `Failed to refresh agent: ${(error as Error).message}`,
},
});
}
@@ -3782,7 +3784,7 @@ export class Session {
type: "set_agent_mode_response",
payload: { requestId, agentId, accepted: true, error: null },
});
} catch (error: any) {
} catch (error) {
this.sessionLogger.error(
{ err: error, agentId, modeId, requestId },
"session: set_agent_mode_request error",
@@ -3793,7 +3795,7 @@ export class Session {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to set agent mode: ${error.message}`,
content: `Failed to set agent mode: ${(error as Error).message}`,
},
});
this.emit({
@@ -3802,7 +3804,9 @@ export class Session {
requestId,
agentId,
accepted: false,
error: error?.message ? String(error.message) : "Failed to set agent mode",
error: (error as Error | undefined)?.message
? String((error as Error).message)
: "Failed to set agent mode",
},
});
}
@@ -3825,7 +3829,7 @@ export class Session {
type: "set_agent_model_response",
payload: { requestId, agentId, accepted: true, error: null },
});
} catch (error: any) {
} catch (error) {
this.sessionLogger.error(
{ err: error, agentId, modelId, requestId },
"session: set_agent_model_request error",
@@ -3836,7 +3840,7 @@ export class Session {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to set agent model: ${error.message}`,
content: `Failed to set agent model: ${(error as Error).message}`,
},
});
this.emit({
@@ -3845,7 +3849,9 @@ export class Session {
requestId,
agentId,
accepted: false,
error: error?.message ? String(error.message) : "Failed to set agent model",
error: (error as Error | undefined)?.message
? String((error as Error).message)
: "Failed to set agent model",
},
});
}
@@ -3872,7 +3878,7 @@ export class Session {
type: "set_agent_feature_response",
payload: { requestId, agentId, accepted: true, error: null },
});
} catch (error: any) {
} catch (error) {
this.sessionLogger.error(
{ err: error, agentId, featureId, value, requestId },
"session: set_agent_feature_request error",
@@ -3883,7 +3889,7 @@ export class Session {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to set agent feature: ${error.message}`,
content: `Failed to set agent feature: ${(error as Error).message}`,
},
});
this.emit({
@@ -3892,7 +3898,9 @@ export class Session {
requestId,
agentId,
accepted: false,
error: error?.message ? String(error.message) : "Failed to set agent feature",
error: (error as Error | undefined)?.message
? String((error as Error).message)
: "Failed to set agent feature",
},
});
}
@@ -3918,7 +3926,7 @@ export class Session {
type: "set_agent_thinking_response",
payload: { requestId, agentId, accepted: true, error: null },
});
} catch (error: any) {
} catch (error) {
this.sessionLogger.error(
{ err: error, agentId, thinkingOptionId, requestId },
"session: set_agent_thinking_request error",
@@ -3929,7 +3937,7 @@ export class Session {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to set agent thinking option: ${error.message}`,
content: `Failed to set agent thinking option: ${(error as Error).message}`,
},
});
this.emit({
@@ -3938,7 +3946,9 @@ export class Session {
requestId,
agentId,
accepted: false,
error: error?.message ? String(error.message) : "Failed to set agent thinking option",
error: (error as Error | undefined)?.message
? String((error as Error).message)
: "Failed to set agent thinking option",
},
});
}
@@ -3973,7 +3983,7 @@ export class Session {
},
});
}
} catch (error: any) {
} catch (error) {
this.sessionLogger.error({ err: error, agentIds }, "Failed to clear agent attention");
// Don't throw - this is not critical
}
@@ -4072,14 +4082,14 @@ export class Session {
requestId,
},
});
} catch (error: any) {
} catch (error) {
this.sessionLogger.error({ err: error, agentId, draftConfig }, "Failed to list commands");
this.emit({
type: "list_commands_response",
payload: {
agentId,
commands: [],
error: error.message,
error: (error as Error).message,
requestId,
},
});
@@ -4110,7 +4120,7 @@ export class Session {
);
this.startAgentStream(agentId, result.followUpPrompt);
}
} catch (error: any) {
} catch (error) {
this.sessionLogger.error(
{ err: error, agentId, requestId },
"Failed to respond to permission",
@@ -4121,7 +4131,7 @@ export class Session {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Failed to respond to permission: ${error.message}`,
content: `Failed to respond to permission: ${(error as Error).message}`,
},
});
throw error;
@@ -5195,7 +5205,7 @@ export class Session {
},
});
}
} catch (error: any) {
} catch (error) {
this.sessionLogger.error(
{ err: error, cwd, path: requestedPath },
`Failed to fulfill file explorer request for workspace ${cwd}`,
@@ -5208,7 +5218,7 @@ export class Session {
mode,
directory: null,
file: null,
error: error.message,
error: (error as Error).message,
requestId,
},
});
@@ -5234,13 +5244,13 @@ export class Session {
requestId,
},
});
} catch (error: any) {
} catch (error) {
this.emit({
type: "project_icon_response",
payload: {
cwd,
icon: null,
error: error.message,
error: (error as Error).message,
requestId,
},
});
@@ -5302,7 +5312,7 @@ export class Session {
requestId,
},
});
} catch (error: any) {
} catch (error) {
this.sessionLogger.error(
{ err: error, cwd, path: requestedPath },
`Failed to issue download token for workspace ${cwd}`,
@@ -5316,7 +5326,7 @@ export class Session {
fileName: null,
mimeType: null,
size: null,
error: error.message,
error: (error as Error).message,
requestId,
},
});
@@ -7746,7 +7756,7 @@ export class Session {
format: result.format,
debugRecordingPath: result.debugRecordingPath,
});
} catch (error: any) {
} catch (error) {
this.setPhase("idle");
this.clearSpeechInProgress("transcription error");
await this.flushPendingAudioSegments("transcription error");
@@ -7756,7 +7766,7 @@ export class Session {
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Transcription error: ${error.message}`,
content: `Transcription error: ${(error as Error).message}`,
},
});
throw error;
@@ -8816,7 +8826,7 @@ export class Session {
requestId: msg.requestId,
},
});
} catch (error: any) {
} catch (error) {
this.sessionLogger.error({ err: error, cwd: msg.cwd }, "Failed to list terminals");
this.emit({
type: "list_terminals_response",
@@ -8887,13 +8897,13 @@ export class Session {
requestId: msg.requestId,
},
});
} catch (error: any) {
} catch (error) {
this.sessionLogger.error({ err: error, cwd: msg.cwd }, "Failed to create terminal");
this.emit({
type: "create_terminal_response",
payload: {
terminal: null,
error: error.message,
error: (error as Error).message,
requestId: msg.requestId,
},
});
@@ -9081,7 +9091,7 @@ export class Session {
requestId: msg.requestId,
},
});
} catch (error: any) {
} catch (error) {
this.sessionLogger.error(
{ err: error, terminalId: msg.terminalId },
"Failed to capture terminal",

View File

@@ -1,13 +1,14 @@
import { describe, expect, test } from "vitest";
import { resolveWaitForFinishError } from "./session.js";
import type { AgentSnapshotPayload } from "./messages.js";
describe("resolveWaitForFinishError", () => {
test("returns the agent error when the wait result is an error", () => {
expect(
resolveWaitForFinishError({
status: "error",
final: { lastError: "invalid_json_schema" } as any,
final: { lastError: "invalid_json_schema" } as unknown as AgentSnapshotPayload,
}),
).toBe("invalid_json_schema");
});
@@ -16,7 +17,7 @@ describe("resolveWaitForFinishError", () => {
expect(
resolveWaitForFinishError({
status: "error",
final: {} as any,
final: {} as unknown as AgentSnapshotPayload,
}),
).toBe("Agent failed");
});
@@ -25,7 +26,7 @@ describe("resolveWaitForFinishError", () => {
expect(
resolveWaitForFinishError({
status: "idle",
final: { lastError: "should not surface" } as any,
final: { lastError: "should not surface" } as unknown as AgentSnapshotPayload,
}),
).toBeNull();
});

View File

@@ -52,17 +52,23 @@ function getSessionInputMeta(
session: OrtSession,
inputName: string,
): { type?: string; dims?: Array<number | string | null> } | undefined {
const metaAny = (session as any).inputMetadata as unknown;
const metaAny = (session as unknown as { inputMetadata?: unknown }).inputMetadata;
if (Array.isArray(metaAny)) {
const entry = metaAny.find(
(m) => m && typeof m === "object" && (m as any).name === inputName,
) as any;
(m) => m && typeof m === "object" && (m as { name?: string }).name === inputName,
) as { type?: string; shape?: Array<number | string | null> } | undefined;
if (!entry) return undefined;
return { type: entry.type, dims: entry.shape };
}
if (metaAny && typeof metaAny === "object" && inputName in (metaAny as any)) {
const entry = (metaAny as any)[inputName] as any;
if (metaAny && typeof metaAny === "object" && inputName in metaAny) {
const entry = (metaAny as Record<string, unknown>)[inputName] as
| {
type?: string;
dimensions?: Array<number | string | null>;
shape?: Array<number | string | null>;
}
| undefined;
return { type: entry?.type, dims: entry?.dimensions ?? entry?.shape };
}
@@ -108,10 +114,19 @@ async function loadOrt(): Promise<OrtModule> {
async function loadSentencePiece(tokenizerModelPath: string): Promise<SentencePieceProcessor> {
const mod = await import("@sctg/sentencepiece-js");
const modRecord = mod as unknown as {
SentencePieceProcessor?: new () => SentencePieceProcessor;
default?:
| (new () => SentencePieceProcessor)
| { SentencePieceProcessor?: new () => SentencePieceProcessor };
};
const defaultValue = modRecord.default;
const Processor =
(mod as any).SentencePieceProcessor ??
(mod as any).default?.SentencePieceProcessor ??
(mod as any).default;
modRecord.SentencePieceProcessor ??
(defaultValue && typeof defaultValue === "object" && "SentencePieceProcessor" in defaultValue
? defaultValue.SentencePieceProcessor
: undefined) ??
(typeof defaultValue === "function" ? defaultValue : undefined);
if (!Processor) {
throw new Error("Failed to load SentencePiece processor from @sctg/sentencepiece-js");
@@ -166,7 +181,8 @@ function createZeroTensorForInput(
function initState(session: OrtSession, ort: OrtModule): Record<string, OrtTensor> {
const out: Record<string, OrtTensor> = {};
for (const name of (session as any).inputNames as string[]) {
const inputNames = (session as unknown as { inputNames: string[] }).inputNames;
for (const name of inputNames) {
if (name.startsWith("state_")) {
out[name] = createZeroTensorForInput(ort, session, name);
}
@@ -188,7 +204,7 @@ function updateStateFromOutputs(
}
function tensorDataFloat32(t: OrtTensor): Float32Array {
const data = (t as any).data;
const data = (t as unknown as { data: unknown }).data;
if (data instanceof Float32Array) return data;
if (Array.isArray(data)) return Float32Array.from(data as number[]);
throw new Error("Unexpected tensor data type (expected Float32Array)");
@@ -346,10 +362,11 @@ class PocketTtsOnnxEngine {
const audioTensor = new ort.Tensor("float32", floatAudio, [1, 1, floatAudio.length]);
const encoded = await mimiEncoder.run({ audio: audioTensor });
const firstOutName = (mimiEncoder as any).outputNames?.[0] as string | undefined;
const firstOutName = (mimiEncoder as unknown as { outputNames?: string[] }).outputNames?.[0];
const encodedRecord = encoded as unknown as Record<string, OrtTensor>;
const voiceEmb = firstOutName
? (encoded as any)[firstOutName]
: (Object.values(encoded)[0] as any);
? encodedRecord[firstOutName]
: (Object.values(encodedRecord)[0] as OrtTensor | undefined);
if (!voiceEmb) {
throw new Error("PocketTTS mimi_encoder: missing output");
}
@@ -382,9 +399,15 @@ class PocketTtsOnnxEngine {
}
private async runTextConditioner(tokenIds: OrtTensor): Promise<OrtTensor> {
const out = await this.textConditioner.run({ token_ids: tokenIds } as any);
const firstOutName = (this.textConditioner as any).outputNames?.[0] as string | undefined;
const t = firstOutName ? (out as any)[firstOutName] : (Object.values(out)[0] as any);
const out = await this.textConditioner.run({
token_ids: tokenIds,
} as unknown as Record<string, OrtTensor>);
const firstOutName = (this.textConditioner as unknown as { outputNames?: string[] })
.outputNames?.[0];
const outRecord = out as unknown as Record<string, OrtTensor>;
const t = firstOutName
? outRecord[firstOutName]
: (Object.values(outRecord)[0] as OrtTensor | undefined);
if (!t) throw new Error("PocketTTS text_conditioner: missing output");
return t;
}
@@ -401,16 +424,16 @@ class PocketTtsOnnxEngine {
sequence: emptySeq,
text_embeddings: this.voiceEmbeddings,
...state,
} as any);
updateStateFromOutputs(state, resVoice as any);
} as unknown as Record<string, OrtTensor>);
updateStateFromOutputs(state, resVoice as unknown as Record<string, OrtTensor>);
// Text conditioning pass
const resText = await this.flowLmMain.run({
sequence: emptySeq,
text_embeddings: textEmbeddings,
...state,
} as any);
updateStateFromOutputs(state, resText as any);
} as unknown as Record<string, OrtTensor>);
updateStateFromOutputs(state, resText as unknown as Record<string, OrtTensor>);
// Autoregressive generation
const curr = new Float32Array(32);
@@ -425,18 +448,19 @@ class PocketTtsOnnxEngine {
sequence: currTensor,
text_embeddings: emptyText,
...state,
} as any);
} as unknown as Record<string, OrtTensor>);
const outputNames = (this.flowLmMain as any).outputNames as string[] | undefined;
const conditioningName = outputNames?.[0] ?? Object.keys(resStep)[0]!;
const eosName = outputNames?.[1] ?? Object.keys(resStep)[1]!;
const outputNames = (this.flowLmMain as unknown as { outputNames?: string[] }).outputNames;
const resStepRecord = resStep as unknown as Record<string, OrtTensor>;
const conditioningName = outputNames?.[0] ?? Object.keys(resStepRecord)[0]!;
const eosName = outputNames?.[1] ?? Object.keys(resStepRecord)[1]!;
const conditioning = (resStep as any)[conditioningName] as OrtTensor;
const eos = (resStep as any)[eosName] as OrtTensor;
const conditioning = resStepRecord[conditioningName];
const eos = resStepRecord[eosName];
if (!conditioning || !eos) {
throw new Error("PocketTTS flow_lm_main: missing conditioning/EOS outputs");
}
updateStateFromOutputs(state, resStep as any);
updateStateFromOutputs(state, resStepRecord);
const eosData = tensorDataFloat32(eos);
if (eosData[0]! > -4.0 && eosStep === null) {
@@ -462,9 +486,12 @@ class PocketTtsOnnxEngine {
s: st.s,
t: st.t,
x: xTensor,
} as any);
const first = (this.flowLmFlow as any).outputNames?.[0] as string | undefined;
const flowTensor = first ? (flowOut as any)[first] : (Object.values(flowOut)[0] as any);
} as unknown as Record<string, OrtTensor>);
const first = (this.flowLmFlow as unknown as { outputNames?: string[] }).outputNames?.[0];
const flowOutRecord = flowOut as unknown as Record<string, OrtTensor>;
const flowTensor = first
? flowOutRecord[first]
: (Object.values(flowOutRecord)[0] as OrtTensor | undefined);
if (!flowTensor) throw new Error("PocketTTS flow_lm_flow: missing output");
const delta = tensorDataFloat32(flowTensor);
for (let i = 0; i < x.length; i += 1) {
@@ -489,11 +516,18 @@ class PocketTtsOnnxEngine {
}
const latent = new ort.Tensor("float32", flattened, [1, frameCount, 32]);
const out = await this.mimiDecoder.run({ latent, ...state } as any);
updateStateFromOutputs(state, out as any);
const out = await this.mimiDecoder.run({
latent,
...state,
} as unknown as Record<string, OrtTensor>);
const outRecord = out as unknown as Record<string, OrtTensor>;
updateStateFromOutputs(state, outRecord);
const firstOutName = (this.mimiDecoder as any).outputNames?.[0] as string | undefined;
const audioTensor = firstOutName ? (out as any)[firstOutName] : (Object.values(out)[0] as any);
const firstOutName = (this.mimiDecoder as unknown as { outputNames?: string[] })
.outputNames?.[0];
const audioTensor = firstOutName
? outRecord[firstOutName]
: (Object.values(outRecord)[0] as OrtTensor | undefined);
if (!audioTensor) {
throw new Error("PocketTTS mimi_decoder: missing audio output");
}

View File

@@ -28,8 +28,22 @@ export interface SherpaOfflineRecognizerConfig {
maxActivePaths?: number;
}
interface SherpaOfflineRecognizerNative {
config?: { featConfig?: { sampleRate?: number } };
createStream: () => unknown;
decode: (stream: unknown) => void;
getResult: (stream: unknown) => { text?: string } | string | undefined;
free?: () => void;
}
interface SherpaOfflineStreamNative {
acceptWaveform: ((arg: { samples: Float32Array; sampleRate: number }) => void) &
((sampleRate: number, samples: Float32Array) => void);
free?: () => void;
}
export class SherpaOfflineRecognizerEngine {
public readonly recognizer: any;
public readonly recognizer: SherpaOfflineRecognizerNative;
public readonly sampleRate: number;
private readonly logger: pino.Logger;
@@ -68,7 +82,11 @@ export class SherpaOfflineRecognizerEngine {
maxActivePaths: config.maxActivePaths ?? 4,
};
this.recognizer = new sherpa.OfflineRecognizer(recognizerConfig);
this.recognizer = new (
sherpa as unknown as {
OfflineRecognizer: new (config: unknown) => SherpaOfflineRecognizerNative;
}
).OfflineRecognizer(recognizerConfig);
const sr = this.recognizer?.config?.featConfig?.sampleRate;
this.sampleRate =
typeof sr === "number" && Number.isFinite(sr) && sr > 0
@@ -81,11 +99,15 @@ export class SherpaOfflineRecognizerEngine {
);
}
createStream(): any {
return this.recognizer.createStream();
createStream(): SherpaOfflineStreamNative {
return this.recognizer.createStream() as SherpaOfflineStreamNative;
}
acceptWaveform(stream: any, sampleRate: number, samples: Float32Array): void {
acceptWaveform(
stream: SherpaOfflineStreamNative,
sampleRate: number,
samples: Float32Array,
): void {
if (!stream || typeof stream.acceptWaveform !== "function") {
throw new Error("Unexpected sherpa offline stream: missing acceptWaveform()");
}

View File

@@ -74,8 +74,23 @@ function buildModelConfig(model: SherpaOnlineRecognizerModel): object {
};
}
export interface SherpaOnlineStreamNative {
acceptWaveform: (sampleRate: number, samples: Float32Array) => void;
free?: () => void;
}
export interface SherpaOnlineRecognizerNative {
config?: { featConfig?: { sampleRate?: number } };
createStream: () => SherpaOnlineStreamNative;
isReady: (stream: SherpaOnlineStreamNative) => boolean;
decode: (stream: SherpaOnlineStreamNative) => void;
getResult: (stream: SherpaOnlineStreamNative) => { text?: string } | string | undefined;
reset?: (stream: SherpaOnlineStreamNative) => void;
free?: () => void;
}
export class SherpaOnlineRecognizerEngine {
public readonly recognizer: any;
public readonly recognizer: SherpaOnlineRecognizerNative;
public readonly sampleRate: number;
private readonly logger: pino.Logger;
@@ -115,7 +130,9 @@ export class SherpaOnlineRecognizerEngine {
rule3MinUtteranceLength: config.rule3MinUtteranceLength ?? 20,
};
this.recognizer = sherpa.createOnlineRecognizer(recognizerConfig);
this.recognizer = sherpa.createOnlineRecognizer(
recognizerConfig,
) as SherpaOnlineRecognizerNative;
const sr = this.recognizer?.config?.featConfig?.sampleRate;
this.sampleRate =
typeof sr === "number" && Number.isFinite(sr) && sr > 0 ? sr : featConfig.sampleRate;
@@ -126,7 +143,7 @@ export class SherpaOnlineRecognizerEngine {
);
}
createStream(): any {
createStream(): SherpaOnlineStreamNative {
return this.recognizer.createStream();
}

View File

@@ -1,9 +1,9 @@
import { createRequire } from "node:module";
export interface SherpaOnnxModule {
createOnlineRecognizer: (config: any) => any;
createOfflineRecognizer: (config: any) => any;
createOfflineTts: (config: any) => any;
createOnlineRecognizer: (config: unknown) => unknown;
createOfflineRecognizer: (config: unknown) => unknown;
createOfflineTts: (config: unknown) => unknown;
}
let cached: SherpaOnnxModule | null = null;

View File

@@ -147,7 +147,11 @@ export class SherpaParakeetRealtimeTranscriptionSession
this.engine.acceptWaveform(stream, this.engine.sampleRate, floatSamples);
this.engine.recognizer.decode(stream);
const result = this.engine.recognizer.getResult(stream);
return String(result?.text ?? result ?? "").trim();
return String(
(typeof result === "object" && result && "text" in result ? result.text : undefined) ??
result ??
"",
).trim();
} finally {
try {
stream.free?.();

View File

@@ -53,14 +53,14 @@ export class SherpaOnnxParakeetSTT implements SpeechToTextProvider {
},
appendPcm16(chunk: Buffer) {
if (!connected) {
(emitter as any).emit("error", new Error("STT session not connected"));
emitter.emit("error", new Error("STT session not connected"));
return;
}
pcm16 = pcm16.length === 0 ? chunk : Buffer.concat([pcm16, chunk]);
},
commit: () => {
if (!connected) {
(emitter as any).emit("error", new Error("STT session not connected"));
emitter.emit("error", new Error("STT session not connected"));
return;
}
@@ -70,7 +70,7 @@ export class SherpaOnnxParakeetSTT implements SpeechToTextProvider {
previousSegmentId = committedId;
segmentId = uuidv4();
pcm16 = Buffer.alloc(0);
(emitter as any).emit("committed", { segmentId: committedId, previousSegmentId: prev });
emitter.emit("committed", { segmentId: committedId, previousSegmentId: prev });
void (async () => {
try {
@@ -78,7 +78,7 @@ export class SherpaOnnxParakeetSTT implements SpeechToTextProvider {
committedPcm16,
`audio/pcm;rate=${requiredSampleRate}`,
);
(emitter as any).emit("transcript", {
emitter.emit("transcript", {
segmentId: committedId,
transcript: rt.text,
isFinal: true,
@@ -88,7 +88,7 @@ export class SherpaOnnxParakeetSTT implements SpeechToTextProvider {
isLowConfidence: rt.isLowConfidence,
});
} catch (err) {
(emitter as any).emit("error", err);
emitter.emit("error", err);
} finally {
logger.debug({ bytes: committedPcm16.length }, "Parakeet session reset");
}
@@ -102,8 +102,8 @@ export class SherpaOnnxParakeetSTT implements SpeechToTextProvider {
connected = false;
pcm16 = Buffer.alloc(0);
},
on(event: any, handler: any) {
emitter.on(event, handler);
on(event: "committed" | "transcript" | "error", handler: (payload: never) => void) {
emitter.on(event, handler as (...args: unknown[]) => void);
return undefined;
},
};
@@ -151,7 +151,11 @@ export class SherpaOnnxParakeetSTT implements SpeechToTextProvider {
this.engine.acceptWaveform(stream, inputRate, floatSamples);
this.engine.recognizer.decode(stream);
const result = this.engine.recognizer.getResult(stream);
const text = String(result?.text ?? result ?? "").trim();
const text = String(
(typeof result === "object" && result && "text" in result ? result.text : undefined) ??
result ??
"",
).trim();
const duration = Date.now() - start;
this.logger.debug({ duration, textLength: text.length }, "Parakeet transcription complete");
return { text, duration, ...(text.length === 0 ? { isLowConfidence: true } : {}) };

View File

@@ -3,14 +3,17 @@ import { v4 as uuidv4 } from "uuid";
import type { StreamingTranscriptionSession } from "../../../speech-provider.js";
import { pcm16lePeakAbs, pcm16leToFloat32 } from "../../../audio.js";
import { SherpaOnlineRecognizerEngine } from "./sherpa-online-recognizer.js";
import {
SherpaOnlineRecognizerEngine,
type SherpaOnlineStreamNative,
} from "./sherpa-online-recognizer.js";
export class SherpaRealtimeTranscriptionSession
extends EventEmitter
implements StreamingTranscriptionSession
{
private readonly engine: SherpaOnlineRecognizerEngine;
private stream: any | null = null;
private stream: SherpaOnlineStreamNative | null = null;
private connected = false;
public readonly requiredSampleRate: number;
@@ -55,7 +58,12 @@ export class SherpaRealtimeTranscriptionSession
this.engine.recognizer.decode(this.stream);
}
const text = String(this.engine.recognizer.getResult(this.stream)?.text ?? "").trim();
const rawResult = this.engine.recognizer.getResult(this.stream);
const text = String(
(typeof rawResult === "object" && rawResult && "text" in rawResult
? rawResult.text
: undefined) ?? "",
).trim();
if (text !== this.lastPartialText) {
this.lastPartialText = text;
this.emit("transcript", {
@@ -88,7 +96,12 @@ export class SherpaRealtimeTranscriptionSession
this.engine.recognizer.decode(this.stream);
}
const finalText = String(this.engine.recognizer.getResult(this.stream)?.text ?? "").trim();
const rawFinal = this.engine.recognizer.getResult(this.stream);
const finalText = String(
(typeof rawFinal === "object" && rawFinal && "text" in rawFinal
? rawFinal.text
: undefined) ?? "",
).trim();
const segmentId = this.currentSegmentId;
const previousSegmentId = this.previousSegmentId;
@@ -98,7 +111,7 @@ export class SherpaRealtimeTranscriptionSession
this.previousSegmentId = segmentId;
this.currentSegmentId = uuidv4();
this.lastPartialText = "";
this.engine.recognizer.reset(this.stream);
this.engine.recognizer.reset?.(this.stream);
} catch (err) {
this.emit("error", err instanceof Error ? err : new Error(String(err)));
}
@@ -109,7 +122,7 @@ export class SherpaRealtimeTranscriptionSession
return;
}
try {
this.engine.recognizer.reset(this.stream);
this.engine.recognizer.reset?.(this.stream);
this.currentSegmentId = uuidv4();
this.lastPartialText = "";
} catch (err) {

View File

@@ -56,25 +56,25 @@ export class SherpaOnnxSTT implements SpeechToTextProvider {
},
appendPcm16(chunk: Buffer) {
if (!connected) {
(emitter as any).emit("error", new Error("STT session not connected"));
emitter.emit("error", new Error("STT session not connected"));
return;
}
pcm16 = pcm16.length === 0 ? chunk : Buffer.concat([pcm16, chunk]);
},
commit: () => {
if (!connected) {
(emitter as any).emit("error", new Error("STT session not connected"));
emitter.emit("error", new Error("STT session not connected"));
return;
}
const committedId = segmentId;
const prev = previousSegmentId;
(emitter as any).emit("committed", { segmentId: committedId, previousSegmentId: prev });
emitter.emit("committed", { segmentId: committedId, previousSegmentId: prev });
void (async () => {
try {
const rt = await this.transcribeAudio(pcm16, `audio/pcm;rate=${requiredSampleRate}`);
(emitter as any).emit("transcript", {
emitter.emit("transcript", {
segmentId: committedId,
transcript: rt.text,
isFinal: true,
@@ -84,7 +84,7 @@ export class SherpaOnnxSTT implements SpeechToTextProvider {
isLowConfidence: rt.isLowConfidence,
});
} catch (err) {
(emitter as any).emit("error", err);
emitter.emit("error", err);
} finally {
previousSegmentId = committedId;
segmentId = uuidv4();
@@ -100,8 +100,8 @@ export class SherpaOnnxSTT implements SpeechToTextProvider {
connected = false;
pcm16 = Buffer.alloc(0);
},
on(event: any, handler: any) {
emitter.on(event, handler);
on(event: "committed" | "transcript" | "error", handler: (payload: never) => void) {
emitter.on(event, handler as (...args: unknown[]) => void);
return undefined;
},
};
@@ -164,7 +164,12 @@ export class SherpaOnnxSTT implements SpeechToTextProvider {
this.engine.recognizer.decode(stream);
}
const text = String(this.engine.recognizer.getResult(stream)?.text ?? "").trim();
const rawResult = this.engine.recognizer.getResult(stream);
const text = String(
(typeof rawResult === "object" && rawResult && "text" in rawResult
? rawResult.text
: undefined) ?? "",
).trim();
const duration = Date.now() - start;
this.logger.debug({ duration, textLength: text.length }, "Sherpa transcription complete");
return { text, duration, ...(text.length === 0 ? { isLowConfidence: true } : {}) };

View File

@@ -23,8 +23,19 @@ function assertFileExists(filePath: string, label: string): void {
}
}
interface SherpaOfflineTtsNative {
sampleRate?: number;
generate: (args: {
text: string;
sid: number;
speed: number;
enableExternalBuffer: boolean;
}) => { samples?: Float32Array | number[]; sampleRate?: number } | undefined;
free?: () => void;
}
export class SherpaOnnxTTS implements TextToSpeechProvider {
private readonly tts: any;
private readonly tts: SherpaOfflineTtsNative;
private readonly speakerId: number;
private readonly speed: number;
private readonly logger: pino.Logger;
@@ -81,7 +92,9 @@ export class SherpaOnnxTTS implements TextToSpeechProvider {
maxNumSentences: 1,
};
this.tts = new sherpa.OfflineTts(offlineTtsConfig);
this.tts = new (
sherpa as unknown as { OfflineTts: new (config: unknown) => SherpaOfflineTtsNative }
).OfflineTts(offlineTtsConfig);
this.logger.info(
{ preset: config.preset, modelDir: config.modelDir },
"Sherpa offline TTS initialized",

View File

@@ -2,6 +2,7 @@ import { execSync } from "node:child_process";
import { mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import type pino from "pino";
import { describe, expect, test, vi, afterEach } from "vitest";
import {
createPersistedProjectRecord,
@@ -69,7 +70,7 @@ function createTestLogger() {
warn: vi.fn(),
error: vi.fn(),
};
return logger as any;
return logger as unknown as pino.Logger;
}
function createWorkspaceGitServiceStub(