@juspay/neurolink 12.48.1 → 12.49.0

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.
Files changed (107) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/browser/neurolink.min.js +616 -595
  3. package/dist/cli/commands/serve.d.ts +5 -3
  4. package/dist/cli/commands/serve.js +113 -20
  5. package/dist/cli/commands/server.js +13 -6
  6. package/dist/cli/commands/transcribe.d.ts +24 -0
  7. package/dist/cli/commands/transcribe.js +718 -0
  8. package/dist/cli/factories/commandFactory.d.ts +10 -0
  9. package/dist/cli/factories/commandFactory.js +13 -0
  10. package/dist/cli/parser.js +3 -0
  11. package/dist/cli/utils/serverUtils.d.ts +28 -0
  12. package/dist/cli/utils/serverUtils.js +71 -0
  13. package/dist/factories/mediaHandlerCatalog.js +11 -9
  14. package/dist/factories/sttDescriptors.d.ts +63 -0
  15. package/dist/factories/sttDescriptors.js +286 -0
  16. package/dist/index.d.ts +4 -1
  17. package/dist/index.js +7 -1
  18. package/dist/neurolink.d.ts +68 -1
  19. package/dist/neurolink.js +202 -4
  20. package/dist/processors/media/AudioProcessor.d.ts +15 -56
  21. package/dist/processors/media/AudioProcessor.js +105 -276
  22. package/dist/processors/media/VideoProcessor.d.ts +3 -5
  23. package/dist/processors/media/VideoProcessor.js +52 -51
  24. package/dist/server/abstract/baseServerAdapter.d.ts +28 -1
  25. package/dist/server/abstract/baseServerAdapter.js +84 -4
  26. package/dist/server/adapters/expressAdapter.d.ts +1 -0
  27. package/dist/server/adapters/expressAdapter.js +50 -10
  28. package/dist/server/adapters/fastifyAdapter.d.ts +1 -0
  29. package/dist/server/adapters/fastifyAdapter.js +51 -8
  30. package/dist/server/adapters/honoAdapter.d.ts +1 -4
  31. package/dist/server/adapters/honoAdapter.js +95 -8
  32. package/dist/server/adapters/koaAdapter.d.ts +7 -0
  33. package/dist/server/adapters/koaAdapter.js +71 -10
  34. package/dist/server/index.d.ts +3 -2
  35. package/dist/server/index.js +3 -2
  36. package/dist/server/routes/index.d.ts +1 -0
  37. package/dist/server/routes/index.js +3 -0
  38. package/dist/server/routes/transcribeRoutes.d.ts +23 -0
  39. package/dist/server/routes/transcribeRoutes.js +506 -0
  40. package/dist/server/utils/rawBody.d.ts +32 -0
  41. package/dist/server/utils/rawBody.js +76 -0
  42. package/dist/server/utils/validation.d.ts +192 -0
  43. package/dist/server/utils/validation.js +100 -0
  44. package/dist/server/websocket/transcribeWebSocket.d.ts +42 -0
  45. package/dist/server/websocket/transcribeWebSocket.js +264 -0
  46. package/dist/types/cli.d.ts +47 -0
  47. package/dist/types/index.d.ts +1 -0
  48. package/dist/types/index.js +1 -0
  49. package/dist/types/providers.d.ts +7 -1
  50. package/dist/types/server.d.ts +117 -1
  51. package/dist/types/stt.d.ts +160 -7
  52. package/dist/types/transcribe.d.ts +506 -0
  53. package/dist/types/transcribe.js +29 -0
  54. package/dist/utils/speakerTurns.d.ts +28 -0
  55. package/dist/utils/speakerTurns.js +93 -0
  56. package/dist/utils/sttProcessor.d.ts +20 -2
  57. package/dist/utils/sttProcessor.js +73 -5
  58. package/dist/voice/correction/dictionary.d.ts +49 -0
  59. package/dist/voice/correction/dictionary.js +134 -0
  60. package/dist/voice/correction/guard.d.ts +35 -0
  61. package/dist/voice/correction/guard.js +161 -0
  62. package/dist/voice/correction/index.d.ts +47 -0
  63. package/dist/voice/correction/index.js +211 -0
  64. package/dist/voice/correction/rewrite.d.ts +92 -0
  65. package/dist/voice/correction/rewrite.js +322 -0
  66. package/dist/voice/errors.d.ts +15 -0
  67. package/dist/voice/errors.js +45 -0
  68. package/dist/voice/index.d.ts +20 -2
  69. package/dist/voice/index.js +75 -9
  70. package/dist/voice/providers/AzureSTT.d.ts +10 -7
  71. package/dist/voice/providers/AzureSTT.js +23 -75
  72. package/dist/voice/providers/DeepgramSTT.d.ts +17 -2
  73. package/dist/voice/providers/DeepgramSTT.js +109 -13
  74. package/dist/voice/providers/ElevenLabsSTT.d.ts +12 -2
  75. package/dist/voice/providers/ElevenLabsSTT.js +34 -8
  76. package/dist/voice/providers/GoogleSTT.d.ts +10 -8
  77. package/dist/voice/providers/GoogleSTT.js +21 -75
  78. package/dist/voice/providers/OpenAISTT.d.ts +30 -9
  79. package/dist/voice/providers/OpenAISTT.js +196 -21
  80. package/dist/voice/providers/WhistleSTT.d.ts +87 -0
  81. package/dist/voice/providers/WhistleSTT.js +313 -0
  82. package/dist/voice/streaming/chunkedStreamAdapter.d.ts +19 -0
  83. package/dist/voice/streaming/chunkedStreamAdapter.js +519 -0
  84. package/dist/voice/streaming/endpointer.d.ts +81 -0
  85. package/dist/voice/streaming/endpointer.js +247 -0
  86. package/dist/voice/streaming/localAgreement.d.ts +30 -0
  87. package/dist/voice/streaming/localAgreement.js +44 -0
  88. package/dist/voice/sttEnvCredentials.d.ts +12 -0
  89. package/dist/voice/sttEnvCredentials.js +12 -0
  90. package/dist/voice/transcribe.d.ts +47 -0
  91. package/dist/voice/transcribe.js +519 -0
  92. package/dist/voice/transcribeStream.d.ts +4 -0
  93. package/dist/voice/transcribeStream.js +584 -0
  94. package/dist/voice/whistle/assets.d.ts +47 -0
  95. package/dist/voice/whistle/assets.js +270 -0
  96. package/dist/voice/whistle/audio.d.ts +32 -0
  97. package/dist/voice/whistle/audio.js +267 -0
  98. package/dist/voice/whistle/engine.d.ts +32 -0
  99. package/dist/voice/whistle/engine.js +193 -0
  100. package/dist/voice/whistle/worker.d.ts +18 -0
  101. package/dist/voice/whistle/worker.js +294 -0
  102. package/docs-site/static/search-index.json +69 -10
  103. package/models/whistle/README.md +18 -0
  104. package/models/whistle/needle.js +2 -0
  105. package/models/whistle/needle.wasm +0 -0
  106. package/models/whistle/whistle.cact +0 -0
  107. package/package.json +7 -3
@@ -300,6 +300,11 @@ export declare class CLICommandFactory {
300
300
  type: "string";
301
301
  description: string;
302
302
  };
303
+ sttDiarize: {
304
+ type: "boolean";
305
+ default: boolean;
306
+ description: string;
307
+ };
303
308
  inputAudio: {
304
309
  type: "string";
305
310
  description: string;
@@ -1223,6 +1228,11 @@ export declare const commonOptions: {
1223
1228
  type: "string";
1224
1229
  description: string;
1225
1230
  };
1231
+ sttDiarize: {
1232
+ type: "boolean";
1233
+ default: boolean;
1234
+ description: string;
1235
+ };
1226
1236
  inputAudio: {
1227
1237
  type: "string";
1228
1238
  description: string;
@@ -435,6 +435,11 @@ export class CLICommandFactory {
435
435
  type: "string",
436
436
  description: "Audio language code for STT (e.g., en-US)",
437
437
  },
438
+ sttDiarize: {
439
+ type: "boolean",
440
+ default: false,
441
+ description: "Label speakers in the transcript (providers that diarize: deepgram, google-stt, elevenlabs-stt, a diarizing OpenAI-compatible endpoint)",
442
+ },
438
443
  inputAudio: {
439
444
  type: "string",
440
445
  description: "Path to audio file for STT transcription",
@@ -818,6 +823,7 @@ export class CLICommandFactory {
818
823
  // --input-audio without --stt get an actionable error from yargs
819
824
  // instead of silently skipping STT.
820
825
  .implies("sttProvider", "stt")
826
+ .implies("sttDiarize", "stt")
821
827
  .implies("inputAudio", "stt"));
822
828
  }
823
829
  // Helper method to process CLI images with smart auto-detection
@@ -980,6 +986,7 @@ export class CLICommandFactory {
980
986
  stt: argv.stt,
981
987
  sttProvider: argv.sttProvider,
982
988
  sttLanguage: argv.sttLanguage,
989
+ sttDiarize: argv.sttDiarize,
983
990
  inputAudio: argv.inputAudio,
984
991
  // Video generation options (Veo 3.1)
985
992
  outputMode: argv.outputMode,
@@ -2850,6 +2857,9 @@ export class CLICommandFactory {
2850
2857
  enabled: true,
2851
2858
  provider: enhancedOptions.sttProvider,
2852
2859
  language: enhancedOptions.sttLanguage,
2860
+ ...(enhancedOptions.sttDiarize
2861
+ ? { speakerDiarization: true }
2862
+ : {}),
2853
2863
  ...(inputAudioBuffer && { audio: inputAudioBuffer }),
2854
2864
  ...(inputAudioFormat && { format: inputAudioFormat }),
2855
2865
  }
@@ -3117,6 +3127,9 @@ export class CLICommandFactory {
3117
3127
  enabled: true,
3118
3128
  provider: enhancedOptions.sttProvider,
3119
3129
  language: enhancedOptions.sttLanguage,
3130
+ ...(enhancedOptions.sttDiarize
3131
+ ? { speakerDiarization: true }
3132
+ : {}),
3120
3133
  ...(streamSttAudio && { audio: streamSttAudio }),
3121
3134
  ...(streamSttFormat && { format: streamSttFormat }),
3122
3135
  };
@@ -31,6 +31,7 @@ import { proxyExposeCommand } from "./commands/proxyExpose.js";
31
31
  import { proxyReplayCommand } from "./commands/proxyReplay.js";
32
32
  import { EvaluateCommandFactory } from "./commands/evaluate.js";
33
33
  import { DecideCommandFactory } from "./commands/decide.js";
34
+ import { TranscribeCommandFactory } from "./commands/transcribe.js";
34
35
  import { TaskCommandFactory } from "./commands/task.js";
35
36
  import { AutoresearchCommandFactory } from "./commands/autoresearch.js";
36
37
  import { voiceServerCommand } from "./commands/voiceServer.js";
@@ -172,6 +173,8 @@ export function initializeCliParser() {
172
173
  .command(CLICommandFactory.createStreamCommand())
173
174
  // Decide Command - typed, calibrated judgements (no free text)
174
175
  .command(DecideCommandFactory.createDecideCommand())
176
+ // Transcribe Command - audio in, text out (the transcribe inference type)
177
+ .command(TranscribeCommandFactory.createTranscribeCommand())
175
178
  // Batch Processing Command - Using CLICommandFactory
176
179
  .command(CLICommandFactory.createBatchCommand())
177
180
  // Provider Command Group - Using CLICommandFactory
@@ -2,6 +2,22 @@
2
2
  * Server Utilities for NeuroLink CLI
3
3
  * Shared utility functions for server management commands (serve.ts and server.ts)
4
4
  */
5
+ import type { MiddlewareDefinition } from "../../types/index.js";
6
+ export declare const DEFAULT_SERVER_PORT = 3000;
7
+ /** Comma-separated API keys; when set, every route except health needs one. */
8
+ export declare const SERVER_API_KEY_ENV = "NEUROLINK_SERVER_API_KEY";
9
+ /**
10
+ * Resolve the listen port: --port, then the config file, then the PORT
11
+ * environment variable (what container platforms set), then 3000.
12
+ */
13
+ export declare function resolveServerPort(cliPort?: number, filePort?: number): number;
14
+ export declare function readServerApiKeys(): string[];
15
+ /**
16
+ * Require one of `apiKeys` as `Authorization: Bearer <key>` or `X-API-Key`.
17
+ * Health probes stay open so orchestrators can reach them, and the dev
18
+ * playground header cannot bypass a key the operator configured.
19
+ */
20
+ export declare function createServerApiKeyMiddleware(apiKeys: string[], basePath: string): MiddlewareDefinition;
5
21
  /**
6
22
  * Get the base directory for NeuroLink state files
7
23
  * @returns Path to ~/.neurolink directory
@@ -29,6 +45,18 @@ export declare function ensureStateDir(): void;
29
45
  * @returns true if the process is running, false otherwise
30
46
  */
31
47
  export declare function isProcessRunning(pid: number): boolean;
48
+ /**
49
+ * Whether a saved server state belongs to a different, still-running process.
50
+ * A state naming our own PID is stale: a restarted container is PID 1 again
51
+ * and would otherwise refuse to start, believing it is already running.
52
+ */
53
+ export declare function isOtherServerRunning(pid: number): boolean;
54
+ /**
55
+ * Remove this process's state file however it exits. A graceful stop can end
56
+ * the process before its signal handler gets to clean up (once the listener
57
+ * closes, the event loop may drain), so this runs from the "exit" event.
58
+ */
59
+ export declare function clearStateOnExit(clearState: () => void): void;
32
60
  /**
33
61
  * Format a duration in milliseconds to a human-readable uptime string
34
62
  * @param ms - Duration in milliseconds
@@ -2,9 +2,64 @@
2
2
  * Server Utilities for NeuroLink CLI
3
3
  * Shared utility functions for server management commands (serve.ts and server.ts)
4
4
  */
5
+ import { timingSafeEqual } from "crypto";
5
6
  import fs from "fs";
6
7
  import os from "os";
7
8
  import path from "path";
9
+ import { ConfigurationError } from "../../server/errors.js";
10
+ import { createAuthMiddleware } from "../../server/middleware/auth.js";
11
+ // ============================================
12
+ // Listen Address & Access Control
13
+ // ============================================
14
+ export const DEFAULT_SERVER_PORT = 3000;
15
+ /** Comma-separated API keys; when set, every route except health needs one. */
16
+ export const SERVER_API_KEY_ENV = "NEUROLINK_SERVER_API_KEY";
17
+ /**
18
+ * Resolve the listen port: --port, then the config file, then the PORT
19
+ * environment variable (what container platforms set), then 3000.
20
+ */
21
+ export function resolveServerPort(cliPort, filePort) {
22
+ const envPort = process.env.PORT?.trim();
23
+ const port = cliPort ?? filePort ?? (envPort ? Number(envPort) : DEFAULT_SERVER_PORT);
24
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
25
+ throw new ConfigurationError(`Invalid port "${cliPort ?? filePort ?? envPort}": expected an integer between 0 and 65535`);
26
+ }
27
+ return port;
28
+ }
29
+ export function readServerApiKeys() {
30
+ return (process.env[SERVER_API_KEY_ENV] ?? "")
31
+ .split(",")
32
+ .map((key) => key.trim())
33
+ .filter((key) => key.length > 0);
34
+ }
35
+ /**
36
+ * Constant-time for equal lengths; a length mismatch returns early, which
37
+ * reveals only the key's length, never its content.
38
+ */
39
+ const matchesKey = (key, token) => key.length === token.length && timingSafeEqual(key, token);
40
+ /**
41
+ * Require one of `apiKeys` as `Authorization: Bearer <key>` or `X-API-Key`.
42
+ * Health probes stay open so orchestrators can reach them, and the dev
43
+ * playground header cannot bypass a key the operator configured.
44
+ */
45
+ export function createServerApiKeyMiddleware(apiKeys, basePath) {
46
+ const keys = apiKeys.map((key) => Buffer.from(key, "utf8"));
47
+ return createAuthMiddleware({
48
+ type: "custom",
49
+ extractToken: (ctx) => {
50
+ const bearer = /^Bearer\s+(.+)$/i.exec(ctx.headers["authorization"] ?? "");
51
+ return bearer?.[1] ?? ctx.headers["x-api-key"] ?? null;
52
+ },
53
+ validate: async (token) => {
54
+ const candidate = Buffer.from(token, "utf8");
55
+ return keys.some((key) => matchesKey(key, candidate))
56
+ ? { id: "api-key" }
57
+ : null;
58
+ },
59
+ skipPaths: [`${basePath.replace(/\/+$/, "")}/health`],
60
+ skipDevPlayground: false,
61
+ });
62
+ }
8
63
  // ============================================
9
64
  // State Directory Management
10
65
  // ============================================
@@ -56,6 +111,22 @@ export function isProcessRunning(pid) {
56
111
  return code === "EPERM";
57
112
  }
58
113
  }
114
+ /**
115
+ * Whether a saved server state belongs to a different, still-running process.
116
+ * A state naming our own PID is stale: a restarted container is PID 1 again
117
+ * and would otherwise refuse to start, believing it is already running.
118
+ */
119
+ export function isOtherServerRunning(pid) {
120
+ return pid !== process.pid && isProcessRunning(pid);
121
+ }
122
+ /**
123
+ * Remove this process's state file however it exits. A graceful stop can end
124
+ * the process before its signal handler gets to clean up (once the listener
125
+ * closes, the event loop may drain), so this runs from the "exit" event.
126
+ */
127
+ export function clearStateOnExit(clearState) {
128
+ process.once("exit", clearState);
129
+ }
59
130
  // ============================================
60
131
  // Time Formatting
61
132
  // ============================================
@@ -1,3 +1,4 @@
1
+ import { STT_PROVIDER_DESCRIPTORS } from "./sttDescriptors.js";
1
2
  /**
2
3
  * Static catalog of every shipped media-handler provider, across all six
3
4
  * ecosystems (TTS, STT, Realtime, Video, Avatar, Music). Pure data — no
@@ -20,15 +21,16 @@ export const MEDIA_HANDLER_CATALOG = [
20
21
  { kind: "tts", name: "sixtydb" },
21
22
  { kind: "tts", name: "cartesia" },
22
23
  // --- STT ---
23
- { kind: "stt", name: "whisper", aliases: ["openai-stt"] },
24
- { kind: "stt", name: "deepgram" },
25
- { kind: "stt", name: "google-stt" },
26
- { kind: "stt", name: "azure-stt" },
27
- // "elevenlabs" is reused across kinds on purpose — TTS primary, STT alias —
28
- // the same way "vertex" is a TTS alias and the Video primary. Each kind has
29
- // its own registry, so `stt: { provider: "elevenlabs" }` and
30
- // `tts: { provider: "elevenlabs" }` resolve to different handlers.
31
- { kind: "stt", name: "elevenlabs-stt", aliases: ["scribe", "elevenlabs"] },
24
+ // Derived from STT_PROVIDER_DESCRIPTORS (names, aliases and precedence
25
+ // order) so the catalog and the descriptors cannot drift. "elevenlabs" is
26
+ // reused across kinds on purpose — TTS primary, STT alias — the same way
27
+ // "vertex" is a TTS alias and the Video primary; each kind has its own
28
+ // registry.
29
+ ...STT_PROVIDER_DESCRIPTORS.map((descriptor) => ({
30
+ kind: "stt",
31
+ name: descriptor.name,
32
+ ...(descriptor.aliases ? { aliases: descriptor.aliases } : {}),
33
+ })),
32
34
  // --- Realtime ---
33
35
  { kind: "realtime", name: "openai-realtime" },
34
36
  { kind: "realtime", name: "gemini-live" },
@@ -0,0 +1,63 @@
1
+ import type { STTCredentials, STTEndpointConfig, STTProviderDescriptor } from "../types/index.js";
2
+ /**
3
+ * Every shipped speech-to-text provider — the single source of truth for its
4
+ * name, aliases, credentials slice, env vars and capabilities. Pure data, like
5
+ * `PROVIDER_DESCRIPTORS` for text providers; the handler classes are wired to
6
+ * these names in `voice/index.ts`, and `MEDIA_HANDLER_CATALOG`'s STT rows are
7
+ * derived from this list so the two cannot drift.
8
+ *
9
+ * **Order is precedence.** `resolveDefaultSTTProvider()` returns the first
10
+ * entry that is configured, so reordering changes which engine every
11
+ * `transcribe()` / `generate({ stt })` without an explicit provider uses.
12
+ * `whistle` is last on purpose: it runs on this machine and needs no key, so
13
+ * it is what a caller gets when nothing else is configured.
14
+ */
15
+ export declare const STT_PROVIDER_DESCRIPTORS: readonly STTProviderDescriptor[];
16
+ /** The provider used when nothing else is configured. */
17
+ export declare const DEFAULT_LOCAL_STT_PROVIDER = "whistle";
18
+ /**
19
+ * Canonical descriptor name for a provider name or alias (case-insensitive),
20
+ * or `undefined` when it is not a shipped STT provider — a caller may still
21
+ * have registered a handler of its own under that name.
22
+ */
23
+ export declare function resolveSTTProviderName(name: string | undefined): string | undefined;
24
+ /** The descriptor for a provider name or alias, if it is a shipped one. */
25
+ export declare function getSTTProviderDescriptor(name: string): STTProviderDescriptor | undefined;
26
+ /**
27
+ * Whether a provider can run with these credentials and the environment. The
28
+ * local engine needs no configuration and always counts. Unknown names are
29
+ * not configured.
30
+ */
31
+ export declare function isSTTProviderConfigured(name: string, credentials?: STTCredentials): boolean;
32
+ /** Configured providers, in precedence order (the local engine last). */
33
+ export declare function listConfiguredSTTProviders(credentials?: STTCredentials): string[];
34
+ /**
35
+ * Named OpenAI-compatible endpoints: `credentials.stt.endpoints` over the
36
+ * `NEUROLINK_STT_ENDPOINTS` JSON object. Keys are lowercased; one that
37
+ * collides with a shipped provider name or alias is dropped with a warning,
38
+ * as is an entry without a `baseURL`. Malformed JSON is reported once and
39
+ * ignored.
40
+ */
41
+ export declare function resolveSTTEndpoints(credentials?: STTCredentials): Record<string, STTEndpointConfig>;
42
+ /**
43
+ * The per-call endpoints alone (`credentials.stt.endpoints`), validated
44
+ * exactly as the env map is: names lower-cased, a name that collides with a
45
+ * shipped provider or alias dropped, an entry without a `baseURL` dropped.
46
+ * Every per-call lookup goes through this so a request can neither replace a
47
+ * shipped handler nor build an endpoint the env form would have refused.
48
+ */
49
+ export declare function resolveCallSTTEndpoints(credentials?: STTCredentials): Record<string, STTEndpointConfig>;
50
+ /**
51
+ * The provider a transcription without an explicit one goes to:
52
+ * `NEUROLINK_STT_PROVIDER` when it names a known provider or alias, else the
53
+ * first configured provider in descriptor order, else the built-in local
54
+ * engine. Never throws, never returns an empty string.
55
+ */
56
+ export declare function resolveDefaultSTTProvider(credentials?: STTCredentials): string;
57
+ /**
58
+ * The variables that would configure each hosted STT provider, one clause per
59
+ * provider in precedence order, e.g. "OPENAI_STT_API_KEY or OPENAI_API_KEY for
60
+ * whisper, or DEEPGRAM_API_KEY for deepgram". Derived from the descriptors so
61
+ * a new provider appears in every "nothing is configured" message.
62
+ */
63
+ export declare function describeSTTProviderKeys(): string;
@@ -0,0 +1,286 @@
1
+ import { logger } from "../utils/logger.js";
2
+ /**
3
+ * Every shipped speech-to-text provider — the single source of truth for its
4
+ * name, aliases, credentials slice, env vars and capabilities. Pure data, like
5
+ * `PROVIDER_DESCRIPTORS` for text providers; the handler classes are wired to
6
+ * these names in `voice/index.ts`, and `MEDIA_HANDLER_CATALOG`'s STT rows are
7
+ * derived from this list so the two cannot drift.
8
+ *
9
+ * **Order is precedence.** `resolveDefaultSTTProvider()` returns the first
10
+ * entry that is configured, so reordering changes which engine every
11
+ * `transcribe()` / `generate({ stt })` without an explicit provider uses.
12
+ * `whistle` is last on purpose: it runs on this machine and needs no key, so
13
+ * it is what a caller gets when nothing else is configured.
14
+ */
15
+ export const STT_PROVIDER_DESCRIPTORS = [
16
+ {
17
+ name: "whisper",
18
+ aliases: ["openai-stt", "openai-compatible"],
19
+ label: "OpenAI-compatible /audio/transcriptions",
20
+ credentialsKey: "whisper",
21
+ envVars: ["OPENAI_STT_API_KEY", "OPENAI_API_KEY"],
22
+ baseUrlEnv: "OPENAI_STT_BASE_URL",
23
+ defaultModel: "whisper-1",
24
+ capabilities: {
25
+ streaming: "chunked",
26
+ // OpenAI's own endpoint does not diarize; compatible servers may, and
27
+ // the handler only sends the switch to those.
28
+ diarization: true,
29
+ languageDetect: true,
30
+ prompt: true,
31
+ wordTimestamps: true,
32
+ local: false,
33
+ },
34
+ },
35
+ {
36
+ name: "deepgram",
37
+ label: "Deepgram",
38
+ credentialsKey: "deepgram",
39
+ envVars: ["DEEPGRAM_API_KEY"],
40
+ baseUrlEnv: "DEEPGRAM_BASE_URL",
41
+ defaultModel: "nova-2",
42
+ capabilities: {
43
+ streaming: "native",
44
+ diarization: true,
45
+ languageDetect: true,
46
+ prompt: false,
47
+ wordTimestamps: true,
48
+ local: false,
49
+ },
50
+ },
51
+ {
52
+ name: "elevenlabs-stt",
53
+ // "elevenlabs" is reused across kinds on purpose — TTS primary, STT
54
+ // alias. Each kind has its own registry, so `stt: { provider:
55
+ // "elevenlabs" }` and `tts: { provider: "elevenlabs" }` resolve to
56
+ // different handlers.
57
+ aliases: ["scribe", "elevenlabs"],
58
+ label: "ElevenLabs Scribe",
59
+ credentialsKey: "elevenlabs",
60
+ envVars: ["ELEVENLABS_API_KEY"],
61
+ baseUrlEnv: "ELEVENLABS_STT_BASE_URL",
62
+ defaultModel: "scribe_v2",
63
+ capabilities: {
64
+ streaming: "chunked",
65
+ diarization: true,
66
+ languageDetect: true,
67
+ prompt: false,
68
+ wordTimestamps: true,
69
+ local: false,
70
+ },
71
+ },
72
+ {
73
+ name: "google-stt",
74
+ label: "Google Cloud Speech-to-Text",
75
+ credentialsKey: "google",
76
+ // GoogleSTT accepts the AI Studio key aliases and a service-account file.
77
+ envVars: [
78
+ "GOOGLE_API_KEY",
79
+ "GOOGLE_AI_API_KEY",
80
+ "GEMINI_API_KEY",
81
+ "GOOGLE_APPLICATION_CREDENTIALS",
82
+ ],
83
+ defaultModel: "latest_long",
84
+ capabilities: {
85
+ streaming: "chunked",
86
+ diarization: true,
87
+ languageDetect: false,
88
+ prompt: false,
89
+ wordTimestamps: true,
90
+ local: false,
91
+ },
92
+ },
93
+ {
94
+ name: "azure-stt",
95
+ label: "Azure Speech",
96
+ credentialsKey: "azure",
97
+ envVars: ["AZURE_SPEECH_KEY"],
98
+ capabilities: {
99
+ streaming: "chunked",
100
+ diarization: false,
101
+ languageDetect: false,
102
+ prompt: false,
103
+ wordTimestamps: true,
104
+ local: false,
105
+ },
106
+ },
107
+ {
108
+ name: "whistle",
109
+ aliases: ["cactus-whistle", "local"],
110
+ label: "Whistle (built-in, local)",
111
+ credentialsKey: "whistle",
112
+ envVars: [],
113
+ capabilities: {
114
+ streaming: "chunked",
115
+ diarization: false,
116
+ languageDetect: true,
117
+ prompt: false,
118
+ wordTimestamps: true,
119
+ local: true,
120
+ languages: ["en", "de", "fr", "es", "it", "nl", "pl"],
121
+ },
122
+ },
123
+ ];
124
+ /** The provider used when nothing else is configured. */
125
+ export const DEFAULT_LOCAL_STT_PROVIDER = "whistle";
126
+ const STT_DESCRIPTORS_BY_NAME = new Map(STT_PROVIDER_DESCRIPTORS.map((d) => [d.name, d]));
127
+ /** Lower-cased name or alias → canonical descriptor name. */
128
+ const STT_ALIAS_INDEX = new Map(STT_PROVIDER_DESCRIPTORS.flatMap((d) => [
129
+ [d.name.toLowerCase(), d.name],
130
+ ...(d.aliases ?? []).map((alias) => [alias.toLowerCase(), d.name]),
131
+ ]));
132
+ /**
133
+ * Canonical descriptor name for a provider name or alias (case-insensitive),
134
+ * or `undefined` when it is not a shipped STT provider — a caller may still
135
+ * have registered a handler of its own under that name.
136
+ */
137
+ export function resolveSTTProviderName(name) {
138
+ if (!name) {
139
+ return undefined;
140
+ }
141
+ return STT_ALIAS_INDEX.get(name.trim().toLowerCase());
142
+ }
143
+ /** The descriptor for a provider name or alias, if it is a shipped one. */
144
+ export function getSTTProviderDescriptor(name) {
145
+ const canonical = resolveSTTProviderName(name);
146
+ return canonical ? STT_DESCRIPTORS_BY_NAME.get(canonical) : undefined;
147
+ }
148
+ function hasValue(value) {
149
+ return typeof value === "string" && value.trim().length > 0;
150
+ }
151
+ /**
152
+ * Whether a credentials slice configures its provider: a key, a service
153
+ * account file, or — for the OpenAI-compatible handler, whose self-hosted
154
+ * servers often need no key — a base URL.
155
+ */
156
+ function sliceConfigures(descriptor, credentials) {
157
+ const slice = credentials?.[descriptor.credentialsKey];
158
+ if (!slice) {
159
+ return false;
160
+ }
161
+ if (hasValue(slice.apiKey) || hasValue(slice.credentialsPath)) {
162
+ return true;
163
+ }
164
+ return descriptor.credentialsKey === "whisper" && hasValue(slice.baseURL);
165
+ }
166
+ /**
167
+ * Whether a provider can run with these credentials and the environment. The
168
+ * local engine needs no configuration and always counts. Unknown names are
169
+ * not configured.
170
+ */
171
+ export function isSTTProviderConfigured(name, credentials) {
172
+ const descriptor = getSTTProviderDescriptor(name);
173
+ if (!descriptor) {
174
+ return false;
175
+ }
176
+ if (descriptor.capabilities.local) {
177
+ return true;
178
+ }
179
+ if (sliceConfigures(descriptor, credentials)) {
180
+ return true;
181
+ }
182
+ return descriptor.envVars.some((envVar) => hasValue(process.env[envVar]));
183
+ }
184
+ /** Configured providers, in precedence order (the local engine last). */
185
+ export function listConfiguredSTTProviders(credentials) {
186
+ return [
187
+ ...STT_PROVIDER_DESCRIPTORS.filter((d) => isSTTProviderConfigured(d.name, credentials)).map((d) => d.name),
188
+ ...Object.keys(resolveSTTEndpoints(credentials)),
189
+ ];
190
+ }
191
+ /**
192
+ * Named OpenAI-compatible endpoints: `credentials.stt.endpoints` over the
193
+ * `NEUROLINK_STT_ENDPOINTS` JSON object. Keys are lowercased; one that
194
+ * collides with a shipped provider name or alias is dropped with a warning,
195
+ * as is an entry without a `baseURL`. Malformed JSON is reported once and
196
+ * ignored.
197
+ */
198
+ export function resolveSTTEndpoints(credentials) {
199
+ const out = {};
200
+ const raw = process.env.NEUROLINK_STT_ENDPOINTS?.trim();
201
+ if (raw) {
202
+ try {
203
+ Object.assign(out, validateSTTEndpoints("NEUROLINK_STT_ENDPOINTS", JSON.parse(raw)));
204
+ }
205
+ catch (error) {
206
+ logger.warn(`[STT] NEUROLINK_STT_ENDPOINTS is not valid JSON; ignoring it (${error instanceof Error ? error.message : String(error)})`);
207
+ }
208
+ }
209
+ Object.assign(out, resolveCallSTTEndpoints(credentials));
210
+ return out;
211
+ }
212
+ /**
213
+ * The per-call endpoints alone (`credentials.stt.endpoints`), validated
214
+ * exactly as the env map is: names lower-cased, a name that collides with a
215
+ * shipped provider or alias dropped, an entry without a `baseURL` dropped.
216
+ * Every per-call lookup goes through this so a request can neither replace a
217
+ * shipped handler nor build an endpoint the env form would have refused.
218
+ */
219
+ export function resolveCallSTTEndpoints(credentials) {
220
+ return validateSTTEndpoints("credentials.stt.endpoints", credentials?.endpoints);
221
+ }
222
+ function validateSTTEndpoints(source, entries) {
223
+ const out = {};
224
+ if (typeof entries !== "object" || entries === null) {
225
+ return out;
226
+ }
227
+ for (const [rawName, cfg] of Object.entries(entries)) {
228
+ const name = rawName.trim().toLowerCase();
229
+ if (!name) {
230
+ continue;
231
+ }
232
+ if (STT_ALIAS_INDEX.has(name)) {
233
+ logger.warn(`[STT] ${source}: endpoint "${rawName}" collides with a shipped provider name; ignoring it`);
234
+ continue;
235
+ }
236
+ const baseURL = typeof cfg === "object" && cfg !== null && "baseURL" in cfg
237
+ ? cfg.baseURL
238
+ : undefined;
239
+ if (typeof baseURL !== "string" || !baseURL.trim()) {
240
+ logger.warn(`[STT] ${source}: endpoint "${rawName}" has no baseURL; ignoring it`);
241
+ continue;
242
+ }
243
+ const c = cfg;
244
+ out[name] = {
245
+ baseURL: baseURL.trim(),
246
+ ...(typeof c.apiKey === "string" ? { apiKey: c.apiKey } : {}),
247
+ ...(typeof c.model === "string" ? { model: c.model } : {}),
248
+ ...(typeof c.timeoutMs === "number" ? { timeoutMs: c.timeoutMs } : {}),
249
+ };
250
+ }
251
+ return out;
252
+ }
253
+ /**
254
+ * The provider a transcription without an explicit one goes to:
255
+ * `NEUROLINK_STT_PROVIDER` when it names a known provider or alias, else the
256
+ * first configured provider in descriptor order, else the built-in local
257
+ * engine. Never throws, never returns an empty string.
258
+ */
259
+ export function resolveDefaultSTTProvider(credentials) {
260
+ const fromEnv = process.env.NEUROLINK_STT_PROVIDER?.trim();
261
+ if (fromEnv) {
262
+ const canonical = resolveSTTProviderName(fromEnv);
263
+ if (canonical) {
264
+ return canonical;
265
+ }
266
+ const endpoint = fromEnv.toLowerCase();
267
+ if (endpoint in resolveSTTEndpoints(credentials)) {
268
+ return endpoint;
269
+ }
270
+ logger.warn(`[STT] NEUROLINK_STT_PROVIDER="${fromEnv}" is not a known STT provider; ` +
271
+ `ignoring it. Known: ${STT_PROVIDER_DESCRIPTORS.map((d) => d.name).join(", ")}`);
272
+ }
273
+ const configured = STT_PROVIDER_DESCRIPTORS.find((d) => !d.capabilities.local && isSTTProviderConfigured(d.name, credentials));
274
+ return configured?.name ?? DEFAULT_LOCAL_STT_PROVIDER;
275
+ }
276
+ /**
277
+ * The variables that would configure each hosted STT provider, one clause per
278
+ * provider in precedence order, e.g. "OPENAI_STT_API_KEY or OPENAI_API_KEY for
279
+ * whisper, or DEEPGRAM_API_KEY for deepgram". Derived from the descriptors so
280
+ * a new provider appears in every "nothing is configured" message.
281
+ */
282
+ export function describeSTTProviderKeys() {
283
+ return STT_PROVIDER_DESCRIPTORS.filter((d) => d.envVars.length > 0)
284
+ .map((d) => `${d.envVars.join(" or ")} for ${d.name}`)
285
+ .join(", or ");
286
+ }
package/dist/index.d.ts CHANGED
@@ -70,7 +70,10 @@ export { AVATAR_ERROR_CODES, AvatarError } from "./utils/avatarProcessor.js";
70
70
  export { MusicProcessor } from "./utils/musicProcessor.js";
71
71
  export { MUSIC_ERROR_CODES, MusicError } from "./utils/musicProcessor.js";
72
72
  export { STTProcessor } from "./utils/sttProcessor.js";
73
- export { AzureTTS, AzureTTSHandler, CartesiaTTS, CartesiaTTSHandler, ElevenLabsTTS, ElevenLabsTTSHandler, FishAudioTTS, FishAudioTTSHandler, SixtyDBTTS, SixtyDBTTSHandler, GoogleTTSHandler, OpenAITTS, OpenAITTSHandler, AzureSTT, AzureSTTHandler, DeepgramSTT, DeepgramSTTHandler, ElevenLabsSTT, ElevenLabsSTTHandler, GoogleSTT, GoogleSTTHandler, OpenAISTT, OpenAISTTHandler, WhisperSTT, WhisperSTTHandler, BaseRealtimeHandler, GeminiLive, GeminiLiveHandler, OpenAIRealtime, OpenAIRealtimeHandler, RealtimeProcessor, RealtimeError, STTError, VoiceError, registerDefaultRealtimeHandlers, registerDefaultSTTHandlers, registerDefaultTTSHandlers, } from "./voice/index.js";
73
+ export { describeSTTProviderKeys, isSTTProviderConfigured, resolveDefaultSTTProvider, STT_PROVIDER_DESCRIPTORS, } from "./factories/sttDescriptors.js";
74
+ export { buildSpeakerTurns, listSpeakers } from "./utils/speakerTurns.js";
75
+ export { correctTranscript } from "./voice/correction/index.js";
76
+ export { AzureTTS, AzureTTSHandler, CartesiaTTS, CartesiaTTSHandler, ElevenLabsTTS, ElevenLabsTTSHandler, FishAudioTTS, FishAudioTTSHandler, SixtyDBTTS, SixtyDBTTSHandler, GoogleTTSHandler, OpenAITTS, OpenAITTSHandler, AzureSTT, AzureSTTHandler, DeepgramSTT, DeepgramSTTHandler, ElevenLabsSTT, ElevenLabsSTTHandler, GoogleSTT, GoogleSTTHandler, OpenAISTT, OpenAISTTHandler, WhisperSTT, WhisperSTTHandler, WhistleSTT, WhistleSTTHandler, BaseRealtimeHandler, GeminiLive, GeminiLiveHandler, OpenAIRealtime, OpenAIRealtimeHandler, RealtimeProcessor, RealtimeError, STTError, VoiceError, registerDefaultRealtimeHandlers, registerDefaultSTTHandlers, registerDefaultTTSHandlers, } from "./voice/index.js";
74
77
  export { BeatovenMusic, BeatovenMusicHandler, ElevenLabsMusic, ElevenLabsMusicHandler, LyriaMusic, LyriaMusicHandler, registerDefaultMusicHandlers, ReplicateMusic, ReplicateMusicHandler, } from "./music/index.js";
75
78
  export { DIDAvatar, DIDAvatarHandler, HeyGenAvatar, HeyGenAvatarHandler, registerDefaultAvatarHandlers, ReplicateAvatar, ReplicateAvatarHandler, } from "./avatar/index.js";
76
79
  export { isVertexVideoConfigured, KlingVideoHandler, registerDefaultVideoHandlers, ReplicateVideoHandler, RunwayVideoHandler, VertexVideoHandler, } from "./adapters/video/index.js";
package/dist/index.js CHANGED
@@ -120,6 +120,12 @@ export { MusicProcessor } from "./utils/musicProcessor.js";
120
120
  export { MUSIC_ERROR_CODES, MusicError } from "./utils/musicProcessor.js";
121
121
  // STT / Realtime processors (registries for speech-to-text + live voice)
122
122
  export { STTProcessor } from "./utils/sttProcessor.js";
123
+ // The `transcribe` inference type: STT provider descriptors (precedence
124
+ // order = default-provider order), provider-neutral speaker turns, and the
125
+ // transcript correction layer `neurolink.transcribe()` runs.
126
+ export { describeSTTProviderKeys, isSTTProviderConfigured, resolveDefaultSTTProvider, STT_PROVIDER_DESCRIPTORS, } from "./factories/sttDescriptors.js";
127
+ export { buildSpeakerTurns, listSpeakers } from "./utils/speakerTurns.js";
128
+ export { correctTranscript } from "./voice/correction/index.js";
123
129
  // STT_ERROR_CODES is surfaced via the types barrel (export * from "./types/index.js")
124
130
  // STTError is re-exported below from the voice/index.js barrel
125
131
  // ============================================================================
@@ -134,7 +140,7 @@ export {
134
140
  // TTS
135
141
  AzureTTS, AzureTTSHandler, CartesiaTTS, CartesiaTTSHandler, ElevenLabsTTS, ElevenLabsTTSHandler, FishAudioTTS, FishAudioTTSHandler, SixtyDBTTS, SixtyDBTTSHandler, GoogleTTSHandler, OpenAITTS, OpenAITTSHandler,
136
142
  // STT
137
- AzureSTT, AzureSTTHandler, DeepgramSTT, DeepgramSTTHandler, ElevenLabsSTT, ElevenLabsSTTHandler, GoogleSTT, GoogleSTTHandler, OpenAISTT, OpenAISTTHandler, WhisperSTT, WhisperSTTHandler,
143
+ AzureSTT, AzureSTTHandler, DeepgramSTT, DeepgramSTTHandler, ElevenLabsSTT, ElevenLabsSTTHandler, GoogleSTT, GoogleSTTHandler, OpenAISTT, OpenAISTTHandler, WhisperSTT, WhisperSTTHandler, WhistleSTT, WhistleSTTHandler,
138
144
  // Realtime
139
145
  BaseRealtimeHandler, GeminiLive, GeminiLiveHandler, OpenAIRealtime, OpenAIRealtimeHandler, RealtimeProcessor,
140
146
  // Voice error classes (RealtimeError + STTError + VoiceError)