use-voice-control 0.1.85 → 0.1.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -21
- package/bin/use-voice-control.mjs +20 -0
- package/dist/cli.js +374 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/read-aloud.d.ts +14 -0
- package/dist/client.js +132 -157
- package/dist/client.js.map +1 -1
- package/dist/core/kokoro-node.d.ts +51 -0
- package/dist/core/kokoro.d.ts +6 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +33 -76
- package/dist/index.js.map +1 -1
- package/dist/kokoro-node-DJ_Rxp_N.js +144 -0
- package/dist/kokoro-node-DJ_Rxp_N.js.map +1 -0
- package/dist/markdown.js +213 -0
- package/dist/markdown.js.map +1 -0
- package/dist/node/cli.d.ts +52 -0
- package/dist/node/document.d.ts +45 -0
- package/dist/node/index.d.ts +18 -0
- package/dist/node/render.d.ts +30 -0
- package/dist/node.js +27 -0
- package/dist/node.js.map +1 -0
- package/dist/react.js +13 -11
- package/dist/react.js.map +1 -1
- package/dist/semantic-split-CXhk-k1F.js +44 -0
- package/dist/semantic-split-CXhk-k1F.js.map +1 -0
- package/dist/types/types.d.ts +14 -1
- package/dist/utils/markdown-to-speech.d.ts +76 -0
- package/dist/utils/wav.d.ts +27 -0
- package/package.json +17 -4
- package/speech/client/index.ts +10 -0
- package/speech/client/read-aloud.ts +27 -1
- package/speech/core/kokoro-node.ts +164 -0
- package/speech/core/kokoro.ts +13 -66
- package/speech/index.ts +14 -0
- package/speech/node/cli.ts +509 -0
- package/speech/node/document.ts +126 -0
- package/speech/node/index.ts +64 -0
- package/speech/node/render.ts +79 -0
- package/speech/react/useReadAloud.ts +2 -0
- package/speech/types/types.ts +34 -5
- package/speech/utils/markdown-to-speech.ts +426 -0
- package/speech/utils/wav.ts +89 -0
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sources":["../speech/utils/semantic-split.js","../speech/client/read-aloud.ts","../speech/client/live-transcriber.ts"],"sourcesContent":["/**\n * @fileoverview Splits long text into TTS-sized chunks along paragraph, sentence, comma, and word boundaries so speech synthesis can process it incrementally.\n *\n * Exports `splitTextSmart` (primary chunker, falling back to `splitLongSentence` for\n * oversized sentences) and `splitLongSentence`. Also retains an older, unused\n * `splitTextSmartOld` variant that lacks the long-sentence fallback.\n */\nexport function splitTextSmart(text, maxChunkLength = 500) {\n const paragraphChunks = text.split(/\\n\\s*\\n/);\n const finalChunks = [];\n\n for (let para of paragraphChunks) {\n if (para.length <= maxChunkLength) {\n finalChunks.push(para.trim());\n continue;\n }\n\n const sentenceRegex = /(?<=[.?!])(?=\\s+[\"“”'a-z])/gi;\n const sentences = para.split(sentenceRegex);\n\n let chunk = '';\n for (let sentence of sentences) {\n sentence = sentence.trim();\n\n if (sentence.length > maxChunkLength) {\n // Sentence too long — fallback split\n const subChunks = splitLongSentence(sentence, maxChunkLength);\n for (let sub of subChunks) {\n if ((chunk + ' ' + sub).length > maxChunkLength) {\n if (chunk) finalChunks.push(chunk.trim());\n chunk = sub;\n } else {\n chunk += (chunk ? ' ' : '') + sub;\n }\n }\n continue;\n }\n\n if ((chunk + ' ' + sentence).length > maxChunkLength) {\n if (chunk) finalChunks.push(chunk.trim());\n chunk = sentence;\n } else {\n chunk += (chunk ? ' ' : '') + sentence;\n }\n }\n if (chunk) finalChunks.push(chunk.trim());\n }\n\n return finalChunks;\n}\n\nexport function splitLongSentence(sentence, maxLen) {\n const chunks = [];\n let current = '';\n\n const commaParts = sentence.split(/,\\s*/);\n for (let part of commaParts) {\n if ((current + ', ' + part).length > maxLen) {\n if (current) chunks.push(current.trim());\n if (part.length > maxLen) {\n const words = part.split(/\\s+/);\n let wordChunk = '';\n for (let word of words) {\n if ((wordChunk + ' ' + word).length > maxLen) {\n if (wordChunk) chunks.push(wordChunk.trim());\n wordChunk = word;\n } else {\n wordChunk += (wordChunk ? ' ' : '') + word;\n }\n }\n if (wordChunk) chunks.push(wordChunk.trim());\n current = '';\n } else {\n current = part;\n }\n } else {\n current += (current ? ', ' : '') + part;\n }\n }\n if (current) chunks.push(current.trim());\n\n return chunks;\n}\n\n\nfunction splitTextSmartOld(text, maxChunkLength = 500) {\n const paragraphChunks = text.split(/\\n\\s*\\n/); // Step 1: split on double returns\n const finalChunks = [];\n\n for (let para of paragraphChunks) {\n if (para.length <= maxChunkLength) {\n finalChunks.push(para.trim());\n continue;\n }\n\n // Step 2: Further split on sentence boundaries if too long\n const sentenceRegex = /(?<=[.?!])(?=\\s+[\"“”'a-z])/gi;\n const sentences = para.split(sentenceRegex);\n\n let chunk = '';\n for (let sentence of sentences) {\n sentence = sentence.trim();\n if ((chunk + ' ' + sentence).length > maxChunkLength) {\n if (chunk) finalChunks.push(chunk.trim());\n chunk = sentence;\n } else {\n chunk += (chunk ? ' ' : '') + sentence;\n }\n }\n if (chunk) finalChunks.push(chunk.trim());\n }\n\n return finalChunks;\n}\n","/**\n * @fileoverview Browser-side \"read aloud\" playback engine.\n *\n * `ReadAloudController` takes a block of text, chunks it along sentence/paragraph\n * boundaries with `splitTextSmart`, synthesizes each chunk (Kokoro by default, via\n * the package's `/api/speech/tts` route) and plays the chunks back-to-back while\n * pre-fetching the next one, so playback starts after the first short chunk rather\n * than after the whole document. Pause/resume/stop are supported throughout, and\n * each chunk is reported to `onChunk` so callers can highlight what is being spoken.\n *\n * When no TTS endpoint is reachable — a host app that has not mounted the route, or\n * an offline build — the controller falls back to the browser's built-in\n * `speechSynthesis` rather than failing, so the feature still works everywhere.\n */\nimport type { TTSProvider } from \"../types/types\";\nimport { splitTextSmart } from \"../utils/semantic-split.js\";\n\nexport type ReadAloudState = \"idle\" | \"loading\" | \"speaking\" | \"paused\";\n\nexport interface ReadAloudChunk {\n /** The text of the chunk currently being spoken. */\n text: string;\n /** Zero-based position of this chunk in the queue. */\n index: number;\n /** Total number of chunks queued for this utterance. */\n total: number;\n}\n\n/** Turns a chunk of text into playable audio. Return `null` to defer to `speechSynthesis`. */\nexport type SynthesizeFn = (\n text: string,\n signal: AbortSignal\n) => Promise<Blob | null>;\n\nexport interface ReadAloudOptions {\n /** TTS provider passed to the endpoint. Default `kokoro`. */\n provider?: TTSProvider;\n /** Provider-specific voice id. Default `af_heart`. */\n voice?: string;\n /** HTTP route that synthesizes text. Default `/api/speech/tts`. */\n endpoint?: string;\n /**\n * Target chunk size in characters. Smaller starts talking sooner but makes the\n * seams between chunks more audible. Default 240.\n */\n maxChunkLength?: number;\n /** Override synthesis entirely (tests, a bring-your-own-TTS host app). */\n synthesize?: SynthesizeFn;\n /** Called as each chunk starts playing. */\n onChunk?: (chunk: ReadAloudChunk) => void;\n /** Called on every state transition. */\n onStateChange?: (state: ReadAloudState) => void;\n /** Called when playback ends, either by running out of chunks or by `stop()`. */\n onEnd?: (reason: \"finished\" | \"stopped\") => void;\n /** Called when synthesis or playback fails irrecoverably. */\n onError?: (error: Error) => void;\n}\n\nconst DEFAULT_ENDPOINT = \"/api/speech/tts\";\nconst DEFAULT_VOICE = \"af_heart\";\nconst DEFAULT_MAX_CHUNK = 240;\n\n/** Resolves once `signal` aborts. Used to race playback against `stop()`. */\nfunction abortPromise(signal: AbortSignal): Promise<void> {\n if (signal.aborted) return Promise.resolve();\n return new Promise((resolve) => {\n signal.addEventListener(\"abort\", () => resolve(), { once: true });\n });\n}\n\nexport class ReadAloudController {\n private options: ReadAloudOptions;\n private state: ReadAloudState = \"idle\";\n private abortController: AbortController | null = null;\n private audio: HTMLAudioElement | null = null;\n private objectUrl: string | null = null;\n /** Set once the endpoint has proved unreachable, so later chunks skip the retry. */\n private endpointUnavailable = false;\n\n constructor(options: ReadAloudOptions = {}) {\n this.options = options;\n }\n\n /** Replace the options (voice, callbacks, …) without discarding playback state. */\n setOptions(options: ReadAloudOptions): void {\n this.options = options;\n }\n\n getState(): ReadAloudState {\n return this.state;\n }\n\n isActive(): boolean {\n return this.state === \"speaking\" || this.state === \"paused\" || this.state === \"loading\";\n }\n\n /**\n * Speak `text`, cancelling anything already playing. Resolves when playback\n * finishes or is stopped — it never rejects; failures go to `onError`.\n */\n async speak(text: string): Promise<void> {\n this.stop();\n\n const maxChunkLength = this.options.maxChunkLength ?? DEFAULT_MAX_CHUNK;\n const chunks = splitTextSmart(text ?? \"\", maxChunkLength)\n .map((chunk) => chunk.trim())\n .filter((chunk) => chunk.length > 0);\n\n if (chunks.length === 0) return;\n\n const abortController = new AbortController();\n this.abortController = abortController;\n const { signal } = abortController;\n this.setState(\"loading\");\n\n try {\n // Kick off the first chunk, then always keep exactly one chunk in flight\n // ahead of the one playing.\n let upcoming = this.synthesize(chunks[0], signal);\n\n for (let index = 0; index < chunks.length; index += 1) {\n const current = upcoming;\n upcoming =\n index + 1 < chunks.length\n ? this.synthesize(chunks[index + 1], signal)\n : Promise.resolve(null);\n\n const blob = await current;\n if (signal.aborted) return;\n\n // Announce the state before the chunk so a listener reacting to\n // `onChunk` sees the controller already speaking.\n this.setState(\"speaking\");\n this.options.onChunk?.({ text: chunks[index], index, total: chunks.length });\n if (signal.aborted) return;\n await this.playChunk(blob, chunks[index], signal);\n if (signal.aborted) return;\n }\n\n this.cleanup();\n this.setState(\"idle\");\n this.options.onEnd?.(\"finished\");\n } catch (error) {\n if (signal.aborted) return;\n this.cleanup();\n this.setState(\"idle\");\n this.options.onError?.(\n error instanceof Error ? error : new Error(String(error))\n );\n this.options.onEnd?.(\"stopped\");\n } finally {\n if (this.abortController === abortController) {\n this.abortController = null;\n }\n }\n }\n\n pause(): void {\n if (this.state !== \"speaking\") return;\n if (this.audio) {\n this.audio.pause();\n } else if (typeof speechSynthesis !== \"undefined\") {\n speechSynthesis.pause();\n }\n this.setState(\"paused\");\n }\n\n resume(): void {\n if (this.state !== \"paused\") return;\n if (this.audio) {\n void this.audio.play().catch(() => undefined);\n } else if (typeof speechSynthesis !== \"undefined\") {\n speechSynthesis.resume();\n }\n this.setState(\"speaking\");\n }\n\n /** Stop playback and drop any queued chunks. Safe to call when already idle. */\n stop(): void {\n const wasActive = this.isActive();\n this.abortController?.abort();\n this.abortController = null;\n this.cleanup();\n if (wasActive) {\n this.setState(\"idle\");\n this.options.onEnd?.(\"stopped\");\n }\n }\n\n private setState(state: ReadAloudState): void {\n if (this.state === state) return;\n this.state = state;\n this.options.onStateChange?.(state);\n }\n\n private cleanup(): void {\n if (this.audio) {\n this.audio.pause();\n this.audio.src = \"\";\n this.audio = null;\n }\n if (this.objectUrl) {\n URL.revokeObjectURL(this.objectUrl);\n this.objectUrl = null;\n }\n if (typeof speechSynthesis !== \"undefined\") {\n speechSynthesis.cancel();\n }\n }\n\n private synthesize(text: string, signal: AbortSignal): Promise<Blob | null> {\n const promise = this.options.synthesize\n ? this.options.synthesize(text, signal)\n : this.fetchAudio(text, signal);\n // Pre-fetched chunks are awaited a beat later; mark them handled now so a\n // stop() mid-flight does not surface as an unhandled rejection.\n promise.catch(() => undefined);\n return promise;\n }\n\n private async fetchAudio(text: string, signal: AbortSignal): Promise<Blob | null> {\n if (this.endpointUnavailable) return null;\n\n const endpoint = this.options.endpoint ?? DEFAULT_ENDPOINT;\n try {\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n text,\n provider: this.options.provider ?? \"kokoro\",\n voice: this.options.voice ?? DEFAULT_VOICE,\n }),\n signal,\n });\n\n if (!response.ok) throw new Error(`TTS failed: ${response.status}`);\n\n const contentType = response.headers.get(\"Content-Type\") || \"audio/wav\";\n return new Blob([await response.arrayBuffer()], { type: contentType });\n } catch (error) {\n if (signal.aborted) throw error;\n // No usable endpoint in this host app — remember it and let every\n // remaining chunk go straight to the browser's own synthesizer.\n this.endpointUnavailable = true;\n return null;\n }\n }\n\n private playChunk(\n blob: Blob | null,\n text: string,\n signal: AbortSignal\n ): Promise<void> {\n return blob\n ? this.playAudioBlob(blob, signal)\n : this.playWithSpeechSynthesis(text, signal);\n }\n\n private async playAudioBlob(blob: Blob, signal: AbortSignal): Promise<void> {\n const url = URL.createObjectURL(blob);\n const audio = new Audio(url);\n this.audio = audio;\n this.objectUrl = url;\n\n const finished = new Promise<void>((resolve, reject) => {\n audio.addEventListener(\"ended\", () => resolve(), { once: true });\n audio.addEventListener(\n \"error\",\n () => reject(new Error(\"Audio playback failed\")),\n { once: true }\n );\n });\n\n try {\n await audio.play();\n await Promise.race([finished, abortPromise(signal)]);\n } finally {\n if (this.audio === audio) this.audio = null;\n if (this.objectUrl === url) this.objectUrl = null;\n audio.pause();\n URL.revokeObjectURL(url);\n }\n }\n\n private async playWithSpeechSynthesis(\n text: string,\n signal: AbortSignal\n ): Promise<void> {\n if (typeof speechSynthesis === \"undefined\" || typeof SpeechSynthesisUtterance === \"undefined\") {\n throw new Error(\"No speech synthesis available in this browser\");\n }\n\n const utterance = new SpeechSynthesisUtterance(text);\n const finished = new Promise<void>((resolve, reject) => {\n utterance.onend = () => resolve();\n // `cancel()` fires an error event; a stop is not a failure.\n utterance.onerror = () =>\n signal.aborted ? resolve() : reject(new Error(\"Speech synthesis failed\"));\n });\n\n speechSynthesis.speak(utterance);\n await Promise.race([finished, abortPromise(signal)]);\n }\n}\n","/**\n * @fileoverview Browser-side live dictation engine.\n *\n * `LiveTranscriber` streams microphone audio to a recognizer and reports two kinds\n * of text: `onPartial`, the in-progress guess that keeps changing while a phrase is\n * being spoken, and `onCommit`, a phrase the recognizer has settled on. Callers use\n * the first to type text into an input as it is being said, and the second to\n * finalize it.\n *\n * Two engines are supported. The browser's own `SpeechRecognition` is preferred when\n * present because it needs no model download; otherwise the package's bundled\n * Moonshine model runs the recognition entirely on-device. Chromium's recognizer\n * stops itself after a pause, so a session that is still meant to be listening is\n * restarted automatically.\n */\n\nexport type TranscriberEngine = \"auto\" | \"webspeech\" | \"moonshine\";\n\nexport interface LiveTranscriberOptions {\n /** Which recognizer to use. Default `auto` (browser first, Moonshine as fallback). */\n engine?: TranscriberEngine;\n /** BCP-47 language tag for the browser recognizer. Default `en-US`. */\n language?: string;\n /** Moonshine model name. Default `model/small`. */\n model?: string;\n /** Fires continuously with the current in-progress phrase. */\n onPartial?: (text: string) => void;\n /** Fires once per phrase the recognizer has settled on. */\n onCommit?: (text: string) => void;\n /** Fires whenever the microphone starts or stops. */\n onStateChange?: (listening: boolean) => void;\n onError?: (error: Error) => void;\n}\n\ntype SpeechRecognitionCtor = new () => any;\n\nfunction getSpeechRecognition(): SpeechRecognitionCtor | null {\n if (typeof window === \"undefined\") return null;\n return (\n (window as any).SpeechRecognition ||\n (window as any).webkitSpeechRecognition ||\n null\n );\n}\n\n/** True when this browser can dictate at all (either engine). */\nexport function isTranscriptionSupported(): boolean {\n if (typeof window === \"undefined\") return false;\n if (getSpeechRecognition()) return true;\n return !!navigator.mediaDevices?.getUserMedia;\n}\n\nexport class LiveTranscriber {\n private options: LiveTranscriberOptions;\n private listening = false;\n /** Distinguishes a deliberate `stop()` from Chromium's idle auto-stop. */\n private stopRequested = false;\n private recognition: any = null;\n private moonshine: any = null;\n\n constructor(options: LiveTranscriberOptions = {}) {\n this.options = options;\n }\n\n setOptions(options: LiveTranscriberOptions): void {\n this.options = options;\n }\n\n isListening(): boolean {\n return this.listening;\n }\n\n async start(): Promise<void> {\n if (this.listening) return;\n this.stopRequested = false;\n\n const engine = this.options.engine ?? \"auto\";\n const Recognition = getSpeechRecognition();\n\n try {\n if (engine !== \"moonshine\" && Recognition) {\n this.startWebSpeech(Recognition);\n return;\n }\n if (engine === \"webspeech\") {\n throw new Error(\"Speech recognition is not available in this browser\");\n }\n await this.startMoonshine();\n } catch (error) {\n this.setListening(false);\n this.options.onError?.(\n error instanceof Error ? error : new Error(String(error))\n );\n }\n }\n\n async stop(): Promise<void> {\n this.stopRequested = true;\n\n if (this.recognition) {\n try {\n this.recognition.stop();\n } catch {\n /* already stopped */\n }\n this.recognition = null;\n }\n\n if (this.moonshine) {\n try {\n await this.moonshine.stop?.();\n } catch {\n /* already stopped */\n }\n this.moonshine = null;\n }\n\n this.setListening(false);\n }\n\n async toggle(): Promise<void> {\n if (this.listening) {\n await this.stop();\n } else {\n await this.start();\n }\n }\n\n private setListening(listening: boolean): void {\n if (this.listening === listening) return;\n this.listening = listening;\n this.options.onStateChange?.(listening);\n }\n\n private startWebSpeech(Recognition: SpeechRecognitionCtor): void {\n const recognition = new Recognition();\n recognition.continuous = true;\n recognition.interimResults = true;\n recognition.lang = this.options.language ?? \"en-US\";\n\n recognition.onstart = () => this.setListening(true);\n\n recognition.onresult = (event: any) => {\n let interim = \"\";\n for (let i = event.resultIndex; i < event.results.length; i += 1) {\n const result = event.results[i];\n const text = String(result[0]?.transcript ?? \"\").trim();\n if (!text) continue;\n if (result.isFinal) {\n // No `onPartial(\"\")` first: a commit supersedes the interim phrase, and\n // clearing it separately would make consumers that write the interim\n // into a document erase and re-insert the same words.\n this.options.onCommit?.(text);\n } else {\n interim += `${interim ? \" \" : \"\"}${text}`;\n }\n }\n if (interim) this.options.onPartial?.(interim);\n };\n\n recognition.onerror = (event: any) => {\n const code = event?.error;\n // `no-speech` and `aborted` are routine during a long dictation session;\n // `onend` restarts the recognizer for us.\n if (code === \"no-speech\" || code === \"aborted\") return;\n this.stopRequested = true;\n this.options.onError?.(new Error(`Speech recognition error: ${code}`));\n };\n\n recognition.onend = () => {\n if (this.stopRequested || this.recognition !== recognition) {\n this.recognition = null;\n this.setListening(false);\n return;\n }\n // Chromium ends the session after a pause — start a fresh one so the user\n // can keep dictating without touching the button again.\n try {\n recognition.start();\n } catch {\n this.recognition = null;\n this.setListening(false);\n }\n };\n\n this.recognition = recognition;\n recognition.start();\n }\n\n private async startMoonshine(): Promise<void> {\n const Moonshine = await import(\"@moonshine-ai/moonshine-js\");\n\n const transcriber = new Moonshine.MicrophoneTranscriber(\n this.options.model ?? \"model/small\",\n {\n onTranscriptionUpdated: (text: string) => {\n this.options.onPartial?.(String(text ?? \"\").trim());\n },\n onTranscriptionCommitted: (text: string) => {\n const committed = String(text ?? \"\").trim();\n if (committed) this.options.onCommit?.(committed);\n else this.options.onPartial?.(\"\");\n },\n },\n false // streaming mode\n );\n\n this.moonshine = transcriber;\n await transcriber.start();\n if (this.stopRequested) {\n await this.stop();\n return;\n }\n this.setListening(true);\n }\n}\n"],"names":["splitTextSmart","text","maxChunkLength","paragraphChunks","finalChunks","para","sentenceRegex","sentences","chunk","sentence","subChunks","splitLongSentence","sub","maxLen","chunks","current","commaParts","part","words","wordChunk","word","DEFAULT_ENDPOINT","DEFAULT_VOICE","DEFAULT_MAX_CHUNK","abortPromise","signal","resolve","ReadAloudController","options","__publicField","_a","_b","_c","_d","_e","_f","_g","_h","abortController","upcoming","index","blob","error","wasActive","state","promise","endpoint","response","contentType","url","audio","finished","reject","utterance","getSpeechRecognition","isTranscriptionSupported","LiveTranscriber","engine","Recognition","listening","recognition","event","interim","i","result","code","Moonshine","transcriber","committed"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","sources":["../speech/client/read-aloud.ts","../speech/client/live-transcriber.ts"],"sourcesContent":["/**\n * @fileoverview Browser-side \"read aloud\" playback engine.\n *\n * `ReadAloudController` takes a block of text, chunks it along sentence/paragraph\n * boundaries with `splitTextSmart`, synthesizes each chunk (Kokoro by default, via\n * the package's `/api/speech/tts` route) and plays the chunks back-to-back while\n * pre-fetching the next one, so playback starts after the first short chunk rather\n * than after the whole document. Pause/resume/stop are supported throughout, and\n * each chunk is reported to `onChunk` so callers can highlight what is being spoken.\n *\n * When no TTS endpoint is reachable — a host app that has not mounted the route, or\n * an offline build — the controller falls back to the browser's built-in\n * `speechSynthesis` rather than failing, so the feature still works everywhere.\n */\nimport type { TTSProvider } from \"../types/types\";\nimport {\n looksLikeMarkdown,\n markdownToSpeech,\n type MarkdownToSpeechOptions,\n} from \"../utils/markdown-to-speech\";\nimport { splitTextSmart } from \"../utils/semantic-split.js\";\n\nexport type ReadAloudState = \"idle\" | \"loading\" | \"speaking\" | \"paused\";\n\nexport interface ReadAloudChunk {\n /** The text of the chunk currently being spoken. */\n text: string;\n /** Zero-based position of this chunk in the queue. */\n index: number;\n /** Total number of chunks queued for this utterance. */\n total: number;\n}\n\n/** Turns a chunk of text into playable audio. Return `null` to defer to `speechSynthesis`. */\nexport type SynthesizeFn = (\n text: string,\n signal: AbortSignal\n) => Promise<Blob | null>;\n\nexport interface ReadAloudOptions {\n /** TTS provider passed to the endpoint. Default `kokoro`. */\n provider?: TTSProvider;\n /** Provider-specific voice id. Default `af_heart`. */\n voice?: string;\n /** HTTP route that synthesizes text. Default `/api/speech/tts`. */\n endpoint?: string;\n /**\n * Target chunk size in characters. Smaller starts talking sooner but makes the\n * seams between chunks more audible. Default 240.\n */\n maxChunkLength?: number;\n /**\n * How to read the text handed to `speak()`. `auto` (default) converts text\n * that looks like Markdown so \"##\" and \"**\" are not read out; `markdown`\n * always converts; `text` never does.\n */\n format?: \"auto\" | \"markdown\" | \"text\";\n /** Markdown conversion options, when the text is read as Markdown. */\n markdown?: MarkdownToSpeechOptions;\n /** Override synthesis entirely (tests, a bring-your-own-TTS host app). */\n synthesize?: SynthesizeFn;\n /** Called as each chunk starts playing. */\n onChunk?: (chunk: ReadAloudChunk) => void;\n /** Called on every state transition. */\n onStateChange?: (state: ReadAloudState) => void;\n /** Called when playback ends, either by running out of chunks or by `stop()`. */\n onEnd?: (reason: \"finished\" | \"stopped\") => void;\n /** Called when synthesis or playback fails irrecoverably. */\n onError?: (error: Error) => void;\n}\n\nconst DEFAULT_ENDPOINT = \"/api/speech/tts\";\nconst DEFAULT_VOICE = \"af_heart\";\nconst DEFAULT_MAX_CHUNK = 240;\n\n/** Resolves once `signal` aborts. Used to race playback against `stop()`. */\nfunction abortPromise(signal: AbortSignal): Promise<void> {\n if (signal.aborted) return Promise.resolve();\n return new Promise((resolve) => {\n signal.addEventListener(\"abort\", () => resolve(), { once: true });\n });\n}\n\nexport class ReadAloudController {\n private options: ReadAloudOptions;\n private state: ReadAloudState = \"idle\";\n private abortController: AbortController | null = null;\n private audio: HTMLAudioElement | null = null;\n private objectUrl: string | null = null;\n /** Set once the endpoint has proved unreachable, so later chunks skip the retry. */\n private endpointUnavailable = false;\n\n constructor(options: ReadAloudOptions = {}) {\n this.options = options;\n }\n\n /** Replace the options (voice, callbacks, …) without discarding playback state. */\n setOptions(options: ReadAloudOptions): void {\n this.options = options;\n }\n\n getState(): ReadAloudState {\n return this.state;\n }\n\n isActive(): boolean {\n return this.state === \"speaking\" || this.state === \"paused\" || this.state === \"loading\";\n }\n\n /**\n * Speak `text`, cancelling anything already playing. Resolves when playback\n * finishes or is stopped — it never rejects; failures go to `onError`.\n */\n async speak(text: string): Promise<void> {\n this.stop();\n\n const maxChunkLength = this.options.maxChunkLength ?? DEFAULT_MAX_CHUNK;\n const chunks = splitTextSmart(this.toSpeakableText(text ?? \"\"), maxChunkLength)\n .map((chunk) => chunk.trim())\n .filter((chunk) => chunk.length > 0);\n\n if (chunks.length === 0) return;\n\n const abortController = new AbortController();\n this.abortController = abortController;\n const { signal } = abortController;\n this.setState(\"loading\");\n\n try {\n // Kick off the first chunk, then always keep exactly one chunk in flight\n // ahead of the one playing.\n let upcoming = this.synthesize(chunks[0], signal);\n\n for (let index = 0; index < chunks.length; index += 1) {\n const current = upcoming;\n upcoming =\n index + 1 < chunks.length\n ? this.synthesize(chunks[index + 1], signal)\n : Promise.resolve(null);\n\n const blob = await current;\n if (signal.aborted) return;\n\n // Announce the state before the chunk so a listener reacting to\n // `onChunk` sees the controller already speaking.\n this.setState(\"speaking\");\n this.options.onChunk?.({ text: chunks[index], index, total: chunks.length });\n if (signal.aborted) return;\n await this.playChunk(blob, chunks[index], signal);\n if (signal.aborted) return;\n }\n\n this.cleanup();\n this.setState(\"idle\");\n this.options.onEnd?.(\"finished\");\n } catch (error) {\n if (signal.aborted) return;\n this.cleanup();\n this.setState(\"idle\");\n this.options.onError?.(\n error instanceof Error ? error : new Error(String(error))\n );\n this.options.onEnd?.(\"stopped\");\n } finally {\n if (this.abortController === abortController) {\n this.abortController = null;\n }\n }\n }\n\n pause(): void {\n if (this.state !== \"speaking\") return;\n if (this.audio) {\n this.audio.pause();\n } else if (typeof speechSynthesis !== \"undefined\") {\n speechSynthesis.pause();\n }\n this.setState(\"paused\");\n }\n\n resume(): void {\n if (this.state !== \"paused\") return;\n if (this.audio) {\n void this.audio.play().catch(() => undefined);\n } else if (typeof speechSynthesis !== \"undefined\") {\n speechSynthesis.resume();\n }\n this.setState(\"speaking\");\n }\n\n /** Stop playback and drop any queued chunks. Safe to call when already idle. */\n stop(): void {\n const wasActive = this.isActive();\n this.abortController?.abort();\n this.abortController = null;\n this.cleanup();\n if (wasActive) {\n this.setState(\"idle\");\n this.options.onEnd?.(\"stopped\");\n }\n }\n\n /**\n * Converts Markdown to spoken words before chunking, so a document read out\n * of an editor does not have its syntax read back to the listener.\n */\n private toSpeakableText(text: string): string {\n const format = this.options.format ?? \"auto\";\n if (format === \"text\") return text;\n if (format === \"markdown\" || looksLikeMarkdown(text)) {\n return markdownToSpeech(text, this.options.markdown);\n }\n return text;\n }\n\n private setState(state: ReadAloudState): void {\n if (this.state === state) return;\n this.state = state;\n this.options.onStateChange?.(state);\n }\n\n private cleanup(): void {\n if (this.audio) {\n this.audio.pause();\n this.audio.src = \"\";\n this.audio = null;\n }\n if (this.objectUrl) {\n URL.revokeObjectURL(this.objectUrl);\n this.objectUrl = null;\n }\n if (typeof speechSynthesis !== \"undefined\") {\n speechSynthesis.cancel();\n }\n }\n\n private synthesize(text: string, signal: AbortSignal): Promise<Blob | null> {\n const promise = this.options.synthesize\n ? this.options.synthesize(text, signal)\n : this.fetchAudio(text, signal);\n // Pre-fetched chunks are awaited a beat later; mark them handled now so a\n // stop() mid-flight does not surface as an unhandled rejection.\n promise.catch(() => undefined);\n return promise;\n }\n\n private async fetchAudio(text: string, signal: AbortSignal): Promise<Blob | null> {\n if (this.endpointUnavailable) return null;\n\n const endpoint = this.options.endpoint ?? DEFAULT_ENDPOINT;\n try {\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n text,\n provider: this.options.provider ?? \"kokoro\",\n voice: this.options.voice ?? DEFAULT_VOICE,\n }),\n signal,\n });\n\n if (!response.ok) throw new Error(`TTS failed: ${response.status}`);\n\n const contentType = response.headers.get(\"Content-Type\") || \"audio/wav\";\n return new Blob([await response.arrayBuffer()], { type: contentType });\n } catch (error) {\n if (signal.aborted) throw error;\n // No usable endpoint in this host app — remember it and let every\n // remaining chunk go straight to the browser's own synthesizer.\n this.endpointUnavailable = true;\n return null;\n }\n }\n\n private playChunk(\n blob: Blob | null,\n text: string,\n signal: AbortSignal\n ): Promise<void> {\n return blob\n ? this.playAudioBlob(blob, signal)\n : this.playWithSpeechSynthesis(text, signal);\n }\n\n private async playAudioBlob(blob: Blob, signal: AbortSignal): Promise<void> {\n const url = URL.createObjectURL(blob);\n const audio = new Audio(url);\n this.audio = audio;\n this.objectUrl = url;\n\n const finished = new Promise<void>((resolve, reject) => {\n audio.addEventListener(\"ended\", () => resolve(), { once: true });\n audio.addEventListener(\n \"error\",\n () => reject(new Error(\"Audio playback failed\")),\n { once: true }\n );\n });\n\n try {\n await audio.play();\n await Promise.race([finished, abortPromise(signal)]);\n } finally {\n if (this.audio === audio) this.audio = null;\n if (this.objectUrl === url) this.objectUrl = null;\n audio.pause();\n URL.revokeObjectURL(url);\n }\n }\n\n private async playWithSpeechSynthesis(\n text: string,\n signal: AbortSignal\n ): Promise<void> {\n if (typeof speechSynthesis === \"undefined\" || typeof SpeechSynthesisUtterance === \"undefined\") {\n throw new Error(\"No speech synthesis available in this browser\");\n }\n\n const utterance = new SpeechSynthesisUtterance(text);\n const finished = new Promise<void>((resolve, reject) => {\n utterance.onend = () => resolve();\n // `cancel()` fires an error event; a stop is not a failure.\n utterance.onerror = () =>\n signal.aborted ? resolve() : reject(new Error(\"Speech synthesis failed\"));\n });\n\n speechSynthesis.speak(utterance);\n await Promise.race([finished, abortPromise(signal)]);\n }\n}\n","/**\n * @fileoverview Browser-side live dictation engine.\n *\n * `LiveTranscriber` streams microphone audio to a recognizer and reports two kinds\n * of text: `onPartial`, the in-progress guess that keeps changing while a phrase is\n * being spoken, and `onCommit`, a phrase the recognizer has settled on. Callers use\n * the first to type text into an input as it is being said, and the second to\n * finalize it.\n *\n * Two engines are supported. The browser's own `SpeechRecognition` is preferred when\n * present because it needs no model download; otherwise the package's bundled\n * Moonshine model runs the recognition entirely on-device. Chromium's recognizer\n * stops itself after a pause, so a session that is still meant to be listening is\n * restarted automatically.\n */\n\nexport type TranscriberEngine = \"auto\" | \"webspeech\" | \"moonshine\";\n\nexport interface LiveTranscriberOptions {\n /** Which recognizer to use. Default `auto` (browser first, Moonshine as fallback). */\n engine?: TranscriberEngine;\n /** BCP-47 language tag for the browser recognizer. Default `en-US`. */\n language?: string;\n /** Moonshine model name. Default `model/small`. */\n model?: string;\n /** Fires continuously with the current in-progress phrase. */\n onPartial?: (text: string) => void;\n /** Fires once per phrase the recognizer has settled on. */\n onCommit?: (text: string) => void;\n /** Fires whenever the microphone starts or stops. */\n onStateChange?: (listening: boolean) => void;\n onError?: (error: Error) => void;\n}\n\ntype SpeechRecognitionCtor = new () => any;\n\nfunction getSpeechRecognition(): SpeechRecognitionCtor | null {\n if (typeof window === \"undefined\") return null;\n return (\n (window as any).SpeechRecognition ||\n (window as any).webkitSpeechRecognition ||\n null\n );\n}\n\n/** True when this browser can dictate at all (either engine). */\nexport function isTranscriptionSupported(): boolean {\n if (typeof window === \"undefined\") return false;\n if (getSpeechRecognition()) return true;\n return !!navigator.mediaDevices?.getUserMedia;\n}\n\nexport class LiveTranscriber {\n private options: LiveTranscriberOptions;\n private listening = false;\n /** Distinguishes a deliberate `stop()` from Chromium's idle auto-stop. */\n private stopRequested = false;\n private recognition: any = null;\n private moonshine: any = null;\n\n constructor(options: LiveTranscriberOptions = {}) {\n this.options = options;\n }\n\n setOptions(options: LiveTranscriberOptions): void {\n this.options = options;\n }\n\n isListening(): boolean {\n return this.listening;\n }\n\n async start(): Promise<void> {\n if (this.listening) return;\n this.stopRequested = false;\n\n const engine = this.options.engine ?? \"auto\";\n const Recognition = getSpeechRecognition();\n\n try {\n if (engine !== \"moonshine\" && Recognition) {\n this.startWebSpeech(Recognition);\n return;\n }\n if (engine === \"webspeech\") {\n throw new Error(\"Speech recognition is not available in this browser\");\n }\n await this.startMoonshine();\n } catch (error) {\n this.setListening(false);\n this.options.onError?.(\n error instanceof Error ? error : new Error(String(error))\n );\n }\n }\n\n async stop(): Promise<void> {\n this.stopRequested = true;\n\n if (this.recognition) {\n try {\n this.recognition.stop();\n } catch {\n /* already stopped */\n }\n this.recognition = null;\n }\n\n if (this.moonshine) {\n try {\n await this.moonshine.stop?.();\n } catch {\n /* already stopped */\n }\n this.moonshine = null;\n }\n\n this.setListening(false);\n }\n\n async toggle(): Promise<void> {\n if (this.listening) {\n await this.stop();\n } else {\n await this.start();\n }\n }\n\n private setListening(listening: boolean): void {\n if (this.listening === listening) return;\n this.listening = listening;\n this.options.onStateChange?.(listening);\n }\n\n private startWebSpeech(Recognition: SpeechRecognitionCtor): void {\n const recognition = new Recognition();\n recognition.continuous = true;\n recognition.interimResults = true;\n recognition.lang = this.options.language ?? \"en-US\";\n\n recognition.onstart = () => this.setListening(true);\n\n recognition.onresult = (event: any) => {\n let interim = \"\";\n for (let i = event.resultIndex; i < event.results.length; i += 1) {\n const result = event.results[i];\n const text = String(result[0]?.transcript ?? \"\").trim();\n if (!text) continue;\n if (result.isFinal) {\n // No `onPartial(\"\")` first: a commit supersedes the interim phrase, and\n // clearing it separately would make consumers that write the interim\n // into a document erase and re-insert the same words.\n this.options.onCommit?.(text);\n } else {\n interim += `${interim ? \" \" : \"\"}${text}`;\n }\n }\n if (interim) this.options.onPartial?.(interim);\n };\n\n recognition.onerror = (event: any) => {\n const code = event?.error;\n // `no-speech` and `aborted` are routine during a long dictation session;\n // `onend` restarts the recognizer for us.\n if (code === \"no-speech\" || code === \"aborted\") return;\n this.stopRequested = true;\n this.options.onError?.(new Error(`Speech recognition error: ${code}`));\n };\n\n recognition.onend = () => {\n if (this.stopRequested || this.recognition !== recognition) {\n this.recognition = null;\n this.setListening(false);\n return;\n }\n // Chromium ends the session after a pause — start a fresh one so the user\n // can keep dictating without touching the button again.\n try {\n recognition.start();\n } catch {\n this.recognition = null;\n this.setListening(false);\n }\n };\n\n this.recognition = recognition;\n recognition.start();\n }\n\n private async startMoonshine(): Promise<void> {\n const Moonshine = await import(\"@moonshine-ai/moonshine-js\");\n\n const transcriber = new Moonshine.MicrophoneTranscriber(\n this.options.model ?? \"model/small\",\n {\n onTranscriptionUpdated: (text: string) => {\n this.options.onPartial?.(String(text ?? \"\").trim());\n },\n onTranscriptionCommitted: (text: string) => {\n const committed = String(text ?? \"\").trim();\n if (committed) this.options.onCommit?.(committed);\n else this.options.onPartial?.(\"\");\n },\n },\n false // streaming mode\n );\n\n this.moonshine = transcriber;\n await transcriber.start();\n if (this.stopRequested) {\n await this.stop();\n return;\n }\n this.setListening(true);\n }\n}\n"],"names":["DEFAULT_ENDPOINT","DEFAULT_VOICE","DEFAULT_MAX_CHUNK","abortPromise","signal","resolve","ReadAloudController","options","__publicField","text","maxChunkLength","chunks","splitTextSmart","chunk","abortController","upcoming","index","current","blob","_b","_a","_d","_c","error","_f","_e","_h","_g","wasActive","format","looksLikeMarkdown","markdownToSpeech","state","promise","endpoint","response","contentType","url","audio","finished","reject","utterance","getSpeechRecognition","isTranscriptionSupported","LiveTranscriber","engine","Recognition","listening","recognition","event","interim","i","result","code","Moonshine","transcriber","committed"],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { TTSResult } from '../types/types';
|
|
2
|
+
export type KokoroDtype = "fp32" | "fp16" | "q8" | "q4" | "q4f16";
|
|
3
|
+
export type KokoroDevice = "wasm" | "webgpu" | "cpu";
|
|
4
|
+
export interface KokoroNodeOptions {
|
|
5
|
+
/** Voice id, e.g. `af_heart`. Default `af_heart`. */
|
|
6
|
+
voice?: string;
|
|
7
|
+
/** Speaking rate; 1 is the model's natural pace. Default 1. */
|
|
8
|
+
speed?: number;
|
|
9
|
+
/** Hugging Face model id. Default `onnx-community/Kokoro-82M-v1.0-ONNX`. */
|
|
10
|
+
model?: string;
|
|
11
|
+
/** Weight quantization. `q8` (default) is the best size/quality trade on CPU. */
|
|
12
|
+
dtype?: KokoroDtype;
|
|
13
|
+
/** Execution device. Default `cpu`. */
|
|
14
|
+
device?: KokoroDevice;
|
|
15
|
+
/**
|
|
16
|
+
* Target chunk size in characters. Kokoro's context is limited, so long text
|
|
17
|
+
* is split before synthesis; 400 leaves headroom for phoneme expansion.
|
|
18
|
+
*/
|
|
19
|
+
maxChunkLength?: number;
|
|
20
|
+
/** Silence inserted between chunks, in milliseconds. Default 120. */
|
|
21
|
+
gapMs?: number;
|
|
22
|
+
/** Model download/loading progress, forwarded from transformers.js. */
|
|
23
|
+
onModelProgress?: (progress: unknown) => void;
|
|
24
|
+
/** Called before each chunk is synthesized, for CLI progress output. */
|
|
25
|
+
onChunk?: (info: {
|
|
26
|
+
index: number;
|
|
27
|
+
total: number;
|
|
28
|
+
text: string;
|
|
29
|
+
}) => void;
|
|
30
|
+
}
|
|
31
|
+
export interface KokoroAudio {
|
|
32
|
+
samples: Float32Array;
|
|
33
|
+
sampleRate: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Loads (and caches) a `kokoro-js` instance.
|
|
37
|
+
*
|
|
38
|
+
* `kokoro-js` is an optional dependency: a browser-only consumer should not have
|
|
39
|
+
* to download onnxruntime-node, so a missing install is reported as an
|
|
40
|
+
* actionable error rather than a module-resolution stack trace.
|
|
41
|
+
*/
|
|
42
|
+
export declare function loadKokoroTTS(options?: KokoroNodeOptions): Promise<any>;
|
|
43
|
+
/** Drops cached model instances. Mainly useful in tests and long-lived workers. */
|
|
44
|
+
export declare function resetKokoroCache(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Synthesizes `text` to raw samples, chunking anything longer than the model's
|
|
47
|
+
* comfortable context.
|
|
48
|
+
*/
|
|
49
|
+
export declare function synthesizeSamples(text: string, options?: KokoroNodeOptions): Promise<KokoroAudio>;
|
|
50
|
+
/** Synthesizes `text` and encodes it as a 16-bit PCM WAV file. */
|
|
51
|
+
export declare function synthesizeWav(text: string, options?: KokoroNodeOptions): Promise<TTSResult>;
|
package/dist/core/kokoro.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { TTSResult } from '../types/types';
|
|
2
|
+
import { KokoroNodeOptions } from './kokoro-node';
|
|
2
3
|
/**
|
|
3
|
-
* Generate speech from text using Kokoro
|
|
4
|
+
* Generate speech from text using Kokoro.
|
|
5
|
+
*
|
|
6
|
+
* Long text is chunked and joined automatically. An unrecognised voice falls
|
|
7
|
+
* back to `af_heart` rather than failing the request.
|
|
4
8
|
*/
|
|
5
|
-
export declare function generateKokoroSpeech(text: string, voice?: string): Promise<TTSResult>;
|
|
9
|
+
export declare function generateKokoroSpeech(text: string, voice?: string, options?: Omit<KokoroNodeOptions, "voice">): Promise<TTSResult>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { TTSOptions, TTSResult } from './types/types';
|
|
2
2
|
export * from './types/types';
|
|
3
|
+
export { looksLikeMarkdown, markdownToSpeech, markdownToSpeechSegments, stripInlineMarkdown, type MarkdownToSpeechOptions, type SpeechSegment, type SpeechSegmentType, } from './utils/markdown-to-speech';
|
|
4
|
+
export { encodeWav, concatSamples, wavDurationSeconds } from './utils/wav';
|
|
3
5
|
/**
|
|
4
6
|
* Generate speech from text using the specified provider
|
|
5
7
|
*
|
package/dist/index.js
CHANGED
|
@@ -1,102 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"af_nicole",
|
|
8
|
-
"af_river",
|
|
9
|
-
"af_sarah",
|
|
10
|
-
"af_sky",
|
|
11
|
-
"am_adam",
|
|
12
|
-
"am_echo",
|
|
13
|
-
"am_fable",
|
|
14
|
-
"am_fenrir",
|
|
15
|
-
"am_liam",
|
|
16
|
-
"am_michael",
|
|
17
|
-
"am_onyx"
|
|
18
|
-
], u = [
|
|
19
|
-
"angus",
|
|
20
|
-
"asteria",
|
|
21
|
-
"arcas",
|
|
22
|
-
"orion",
|
|
23
|
-
"orpheus",
|
|
24
|
-
"athena",
|
|
25
|
-
"luna",
|
|
26
|
-
"zeus",
|
|
27
|
-
"perseus",
|
|
28
|
-
"helios",
|
|
29
|
-
"hera",
|
|
30
|
-
"stella"
|
|
31
|
-
];
|
|
32
|
-
let t = null, n = null;
|
|
33
|
-
async function f() {
|
|
34
|
-
return t || (n ? (await n, t) : (n = (async () => {
|
|
35
|
-
try {
|
|
36
|
-
const e = await import("@huggingface/transformers"), { StyleTextToSpeech2Model: o, AutoTokenizer: a } = e, s = "hexgrad/Kokoro-82M", [i, l] = await Promise.all([
|
|
37
|
-
o.from_pretrained(s, {
|
|
38
|
-
device: "cpu",
|
|
39
|
-
dtype: "q8"
|
|
40
|
-
}),
|
|
41
|
-
a.from_pretrained(s)
|
|
42
|
-
]);
|
|
43
|
-
t = { model: i, tokenizer: l }, console.log("[Kokoro] Model loaded successfully");
|
|
44
|
-
} catch (r) {
|
|
45
|
-
throw console.error("[Kokoro] Failed to load model:", r), r;
|
|
46
|
-
} finally {
|
|
47
|
-
n = null;
|
|
48
|
-
}
|
|
49
|
-
})(), await n, t));
|
|
50
|
-
}
|
|
51
|
-
async function d(r, e = "af_heart") {
|
|
52
|
-
c.includes(e), await f();
|
|
53
|
-
try {
|
|
54
|
-
throw new Error("Server-side Kokoro TTS requires additional setup. Use the browser-based implementation via Web Workers.");
|
|
55
|
-
} catch (o) {
|
|
56
|
-
throw console.error("[Kokoro] Error generating speech:", o), o;
|
|
57
|
-
}
|
|
1
|
+
import { K as t, s as a, D as c } from "./kokoro-node-DJ_Rxp_N.js";
|
|
2
|
+
import { c as h, d as k, e as w, w as m } from "./kokoro-node-DJ_Rxp_N.js";
|
|
3
|
+
import { looksLikeMarkdown as v, markdownToSpeech as _, markdownToSpeechSegments as y, stripInlineMarkdown as A } from "./markdown.js";
|
|
4
|
+
async function s(r, e = "af_heart", n = {}) {
|
|
5
|
+
const o = t.includes(e) ? e : "af_heart";
|
|
6
|
+
return a(r, { ...n, voice: o });
|
|
58
7
|
}
|
|
59
|
-
function
|
|
60
|
-
var e,
|
|
8
|
+
function i() {
|
|
9
|
+
var e, n;
|
|
61
10
|
const r = globalThis;
|
|
62
11
|
try {
|
|
63
12
|
if (typeof r.getCloudflareContext == "function") {
|
|
64
|
-
const
|
|
65
|
-
if ((e =
|
|
13
|
+
const o = r.getCloudflareContext();
|
|
14
|
+
if ((e = o == null ? void 0 : o.env) != null && e.AI) return o.env.AI;
|
|
66
15
|
}
|
|
67
16
|
} catch {
|
|
68
17
|
}
|
|
69
|
-
return ((
|
|
18
|
+
return ((n = r.__env__) == null ? void 0 : n.AI) ?? r.AI;
|
|
70
19
|
}
|
|
71
|
-
async function
|
|
72
|
-
const
|
|
73
|
-
if (!
|
|
20
|
+
async function u(r, e = "angus") {
|
|
21
|
+
const n = c.includes(e) ? e : "angus", o = i();
|
|
22
|
+
if (!o)
|
|
74
23
|
throw new Error("Cloudflare AI binding not available");
|
|
75
24
|
return {
|
|
76
|
-
audio: await
|
|
25
|
+
audio: await o.run("@cf/deepgram/aura-1", {
|
|
77
26
|
text: r.slice(0, 5e3),
|
|
78
|
-
speaker:
|
|
27
|
+
speaker: n,
|
|
79
28
|
encoding: "mp3"
|
|
80
29
|
}),
|
|
81
30
|
contentType: "audio/mpeg"
|
|
82
31
|
};
|
|
83
32
|
}
|
|
84
|
-
async function
|
|
85
|
-
const { text: e, provider:
|
|
33
|
+
async function l(r) {
|
|
34
|
+
const { text: e, provider: n = "kokoro", voice: o = "af_heart" } = r;
|
|
86
35
|
if (!e || typeof e != "string" || e.trim().length === 0)
|
|
87
36
|
throw new Error("Text is required");
|
|
88
|
-
switch (
|
|
37
|
+
switch (n) {
|
|
89
38
|
case "kokoro":
|
|
90
|
-
return
|
|
39
|
+
return s(e, o);
|
|
91
40
|
case "deepgram":
|
|
92
|
-
return
|
|
41
|
+
return u(e, o);
|
|
93
42
|
default:
|
|
94
|
-
throw new Error(`Unknown TTS provider: ${
|
|
43
|
+
throw new Error(`Unknown TTS provider: ${n}`);
|
|
95
44
|
}
|
|
96
45
|
}
|
|
97
46
|
export {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
47
|
+
c as DEEPGRAM_SPEAKERS,
|
|
48
|
+
t as KOKORO_VOICES,
|
|
49
|
+
h as concatSamples,
|
|
50
|
+
k as describeKokoroVoice,
|
|
51
|
+
w as encodeWav,
|
|
52
|
+
l as generateSpeech,
|
|
53
|
+
v as looksLikeMarkdown,
|
|
54
|
+
_ as markdownToSpeech,
|
|
55
|
+
y as markdownToSpeechSegments,
|
|
56
|
+
A as stripInlineMarkdown,
|
|
57
|
+
m as wavDurationSeconds
|
|
101
58
|
};
|
|
102
59
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../speech/
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../speech/core/kokoro.ts","../speech/core/deepgram.ts","../speech/index.ts"],"sourcesContent":["/**\n * @fileoverview Kokoro TTS provider for `generateSpeech`.\n *\n * The work happens in `kokoro-node.ts`, which runs the Kokoro model locally\n * through `kokoro-js`; this file is the thin provider adapter that validates the\n * voice and returns the package's `TTSResult` shape.\n */\nimport type { TTSResult } from \"../types/types\";\nimport { KOKORO_VOICES, type KokoroVoice } from \"../types/types\";\nimport { synthesizeWav, type KokoroNodeOptions } from \"./kokoro-node\";\n\n/**\n * Generate speech from text using Kokoro.\n *\n * Long text is chunked and joined automatically. An unrecognised voice falls\n * back to `af_heart` rather than failing the request.\n */\nexport async function generateKokoroSpeech(\n text: string,\n voice: string = \"af_heart\",\n options: Omit<KokoroNodeOptions, \"voice\"> = {}\n): Promise<TTSResult> {\n const kokoroVoice = KOKORO_VOICES.includes(voice as KokoroVoice)\n ? (voice as KokoroVoice)\n : \"af_heart\";\n\n return synthesizeWav(text, { ...options, voice: kokoroVoice });\n}\n","/**\n * @fileoverview Deepgram TTS provider implementation using Cloudflare Workers AI\n * Requires Cloudflare AI binding\n */\nimport type { TTSResult } from \"../types/types\";\nimport { DEEPGRAM_SPEAKERS, type DeepgramSpeaker } from \"../types/types\";\n\n/**\n * Resolve the Cloudflare Workers AI binding at runtime without a hard\n * dependency on the host application. Consumers running on Cloudflare can\n * expose the context by setting `globalThis.getCloudflareContext` (as the\n * `@opennextjs/cloudflare` / `@cloudflare/next-on-pages` helpers do) or by\n * placing the bound `env` on `globalThis.__env__`.\n */\nfunction resolveCloudflareAI(): any {\n const g = globalThis as any;\n try {\n if (typeof g.getCloudflareContext === \"function\") {\n const ctx = g.getCloudflareContext();\n if (ctx?.env?.AI) return ctx.env.AI;\n }\n } catch {\n // CF context helper threw (e.g. called outside a request scope)\n }\n return g.__env__?.AI ?? g.AI;\n}\n\n/**\n * Generate speech from text using Deepgram Aura via Cloudflare Workers AI\n */\nexport async function generateDeepgramSpeech(\n text: string,\n speaker: string = \"angus\"\n): Promise<TTSResult> {\n // Validate speaker\n const auraVoice = DEEPGRAM_SPEAKERS.includes(speaker as DeepgramSpeaker)\n ? (speaker as DeepgramSpeaker)\n : \"angus\";\n\n const ai = resolveCloudflareAI();\n\n if (!ai) {\n throw new Error(\"Cloudflare AI binding not available\");\n }\n\n const result = await ai.run(\"@cf/deepgram/aura-1\", {\n text: text.slice(0, 5000),\n speaker: auraVoice,\n encoding: \"mp3\",\n });\n\n return {\n audio: result,\n contentType: \"audio/mpeg\",\n };\n}\n","/**\n * @fileoverview Unified text-to-speech API supporting Kokoro (default) and Deepgram\n *\n * Kokoro: Faster, more natural, runs on Node CPU\n * Deepgram: Requires Cloudflare AI binding, MP3 output\n */\nimport type { TTSOptions, TTSResult } from \"./types/types\";\nimport { generateKokoroSpeech } from \"./core/kokoro\";\nimport { generateDeepgramSpeech } from \"./core/deepgram\";\n\nexport * from \"./types/types\";\n\n// Markdown handling is exported from the root entry so a server route can turn a\n// document into speakable text with the same rules the CLI and the browser use.\nexport {\n looksLikeMarkdown,\n markdownToSpeech,\n markdownToSpeechSegments,\n stripInlineMarkdown,\n type MarkdownToSpeechOptions,\n type SpeechSegment,\n type SpeechSegmentType,\n} from \"./utils/markdown-to-speech\";\n\nexport { encodeWav, concatSamples, wavDurationSeconds } from \"./utils/wav\";\n\n/**\n * Generate speech from text using the specified provider\n *\n * @param options - TTS configuration\n * @returns Audio buffer and content type\n *\n * @example\n * ```ts\n * // Use Kokoro (default)\n * const audio = await generateSpeech({\n * text: \"Hello world\",\n * voice: \"af_heart\"\n * });\n *\n * // Use Deepgram\n * const audio = await generateSpeech({\n * text: \"Hello world\",\n * provider: \"deepgram\",\n * voice: \"angus\"\n * });\n * ```\n */\nexport async function generateSpeech(\n options: TTSOptions\n): Promise<TTSResult> {\n const { text, provider = \"kokoro\", voice = \"af_heart\" } = options;\n\n if (!text || typeof text !== \"string\" || text.trim().length === 0) {\n throw new Error(\"Text is required\");\n }\n\n switch (provider) {\n case \"kokoro\":\n return generateKokoroSpeech(text, voice);\n\n case \"deepgram\":\n return generateDeepgramSpeech(text, voice);\n\n default:\n throw new Error(`Unknown TTS provider: ${provider}`);\n }\n}\n"],"names":["generateKokoroSpeech","text","voice","options","kokoroVoice","KOKORO_VOICES","synthesizeWav","resolveCloudflareAI","g","ctx","_a","_b","generateDeepgramSpeech","speaker","auraVoice","DEEPGRAM_SPEAKERS","ai","generateSpeech","provider"],"mappings":";;;AAiBA,eAAsBA,EACpBC,GACAC,IAAgB,YAChBC,IAA4C,CAAA,GACxB;AACpB,QAAMC,IAAcC,EAAc,SAASH,CAAoB,IAC1DA,IACD;AAEJ,SAAOI,EAAcL,GAAM,EAAE,GAAGE,GAAS,OAAOC,GAAa;AAC/D;ACbA,SAASG,IAA2B;;AAClC,QAAMC,IAAI;AACV,MAAI;AACF,QAAI,OAAOA,EAAE,wBAAyB,YAAY;AAChD,YAAMC,IAAMD,EAAE,qBAAA;AACd,WAAIE,IAAAD,KAAA,gBAAAA,EAAK,QAAL,QAAAC,EAAU,GAAI,QAAOD,EAAI,IAAI;AAAA,IACnC;AAAA,EACF,QAAQ;AAAA,EAER;AACA,WAAOE,IAAAH,EAAE,YAAF,gBAAAG,EAAW,OAAMH,EAAE;AAC5B;AAKA,eAAsBI,EACpBX,GACAY,IAAkB,SACE;AAEpB,QAAMC,IAAYC,EAAkB,SAASF,CAA0B,IAClEA,IACD,SAEEG,IAAKT,EAAA;AAEX,MAAI,CAACS;AACH,UAAM,IAAI,MAAM,qCAAqC;AASvD,SAAO;AAAA,IACL,OAPa,MAAMA,EAAG,IAAI,uBAAuB;AAAA,MACjD,MAAMf,EAAK,MAAM,GAAG,GAAI;AAAA,MACxB,SAASa;AAAA,MACT,UAAU;AAAA,IAAA,CACX;AAAA,IAIC,aAAa;AAAA,EAAA;AAEjB;ACPA,eAAsBG,EACpBd,GACoB;AACpB,QAAM,EAAE,MAAAF,GAAM,UAAAiB,IAAW,UAAU,OAAAhB,IAAQ,eAAeC;AAE1D,MAAI,CAACF,KAAQ,OAAOA,KAAS,YAAYA,EAAK,KAAA,EAAO,WAAW;AAC9D,UAAM,IAAI,MAAM,kBAAkB;AAGpC,UAAQiB,GAAA;AAAA,IACN,KAAK;AACH,aAAOlB,EAAqBC,GAAMC,CAAK;AAAA,IAEzC,KAAK;AACH,aAAOU,EAAuBX,GAAMC,CAAK;AAAA,IAE3C;AACE,YAAM,IAAI,MAAM,yBAAyBgB,CAAQ,EAAE;AAAA,EAAA;AAEzD;"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { s as d } from "./semantic-split-CXhk-k1F.js";
|
|
2
|
+
const v = [
|
|
3
|
+
"af_heart",
|
|
4
|
+
"af_alloy",
|
|
5
|
+
"af_aoede",
|
|
6
|
+
"af_bella",
|
|
7
|
+
"af_jessica",
|
|
8
|
+
"af_kore",
|
|
9
|
+
"af_nicole",
|
|
10
|
+
"af_nova",
|
|
11
|
+
"af_river",
|
|
12
|
+
"af_sarah",
|
|
13
|
+
"af_sky",
|
|
14
|
+
"am_adam",
|
|
15
|
+
"am_echo",
|
|
16
|
+
"am_eric",
|
|
17
|
+
"am_fenrir",
|
|
18
|
+
"am_liam",
|
|
19
|
+
"am_michael",
|
|
20
|
+
"am_onyx",
|
|
21
|
+
"am_puck",
|
|
22
|
+
"am_santa",
|
|
23
|
+
"bf_alice",
|
|
24
|
+
"bf_emma",
|
|
25
|
+
"bf_isabella",
|
|
26
|
+
"bf_lily",
|
|
27
|
+
"bm_daniel",
|
|
28
|
+
"bm_fable",
|
|
29
|
+
"bm_george",
|
|
30
|
+
"bm_lewis"
|
|
31
|
+
];
|
|
32
|
+
function x(e) {
|
|
33
|
+
const [t, n] = e, o = e.slice(3) || e;
|
|
34
|
+
return {
|
|
35
|
+
id: e,
|
|
36
|
+
name: o.charAt(0).toUpperCase() + o.slice(1),
|
|
37
|
+
accent: t === "b" ? "British English" : "American English",
|
|
38
|
+
gender: n === "m" ? "Male" : "Female"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const S = [
|
|
42
|
+
"angus",
|
|
43
|
+
"asteria",
|
|
44
|
+
"arcas",
|
|
45
|
+
"orion",
|
|
46
|
+
"orpheus",
|
|
47
|
+
"athena",
|
|
48
|
+
"luna",
|
|
49
|
+
"zeus",
|
|
50
|
+
"perseus",
|
|
51
|
+
"helios",
|
|
52
|
+
"hera",
|
|
53
|
+
"stella"
|
|
54
|
+
], _ = 44;
|
|
55
|
+
function g(e, t = 0) {
|
|
56
|
+
const n = Math.max(e.length - 1, 0) * Math.max(t, 0), o = e.reduce((i, r) => i + r.length, 0) + n, a = new Float32Array(o);
|
|
57
|
+
let c = 0;
|
|
58
|
+
return e.forEach((i, r) => {
|
|
59
|
+
a.set(i, c), c += i.length, r < e.length - 1 && (c += Math.max(t, 0));
|
|
60
|
+
}), a;
|
|
61
|
+
}
|
|
62
|
+
function E(e, t = 24e3, n = 1) {
|
|
63
|
+
const o = new ArrayBuffer(_ + e.length * 2), a = new DataView(o), c = (r, s) => {
|
|
64
|
+
for (let f = 0; f < s.length; f += 1)
|
|
65
|
+
a.setUint8(r + f, s.charCodeAt(f));
|
|
66
|
+
}, i = t * n * 2;
|
|
67
|
+
c(0, "RIFF"), a.setUint32(4, 36 + e.length * 2, !0), c(8, "WAVE"), c(12, "fmt "), a.setUint32(16, 16, !0), a.setUint16(20, 1, !0), a.setUint16(22, n, !0), a.setUint32(24, t, !0), a.setUint32(28, i, !0), a.setUint16(32, n * 2, !0), a.setUint16(34, 16, !0), c(36, "data"), a.setUint32(40, e.length * 2, !0);
|
|
68
|
+
for (let r = 0; r < e.length; r += 1) {
|
|
69
|
+
const s = Math.max(-1, Math.min(1, e[r]));
|
|
70
|
+
a.setInt16(_ + r * 2, s < 0 ? s * 32768 : s * 32767, !0);
|
|
71
|
+
}
|
|
72
|
+
return o;
|
|
73
|
+
}
|
|
74
|
+
function C(e, t) {
|
|
75
|
+
return t > 0 ? e.length / t : 0;
|
|
76
|
+
}
|
|
77
|
+
const w = "onnx-community/Kokoro-82M-v1.0-ONNX", p = "af_heart", y = "q8", U = "cpu", b = 400, k = 120, u = /* @__PURE__ */ new Map();
|
|
78
|
+
function A() {
|
|
79
|
+
var t, n;
|
|
80
|
+
const e = globalThis;
|
|
81
|
+
return !!((n = (t = e.process) == null ? void 0 : t.versions) != null && n.node || e.Bun || e.Deno);
|
|
82
|
+
}
|
|
83
|
+
async function M(e = {}) {
|
|
84
|
+
const t = e.model ?? w, n = e.dtype ?? y, o = e.device ?? (A() ? U : "wasm"), a = `${t}|${n}|${o}`, c = u.get(a);
|
|
85
|
+
if (c) return c;
|
|
86
|
+
const i = (async () => {
|
|
87
|
+
let r;
|
|
88
|
+
try {
|
|
89
|
+
({ KokoroTTS: r } = await import("kokoro-js"));
|
|
90
|
+
} catch (s) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Local Kokoro speech needs the optional \`kokoro-js\` package. Install it with \`npm install kokoro-js\`. (import failed: ${s instanceof Error ? s.message : String(s)})`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return r.from_pretrained(t, {
|
|
96
|
+
dtype: n,
|
|
97
|
+
device: o,
|
|
98
|
+
progress_callback: e.onModelProgress
|
|
99
|
+
});
|
|
100
|
+
})();
|
|
101
|
+
u.set(a, i);
|
|
102
|
+
try {
|
|
103
|
+
return await i;
|
|
104
|
+
} catch (r) {
|
|
105
|
+
throw u.delete(a), r;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function K() {
|
|
109
|
+
u.clear();
|
|
110
|
+
}
|
|
111
|
+
async function T(e, t = {}) {
|
|
112
|
+
var m;
|
|
113
|
+
const n = (e ?? "").trim();
|
|
114
|
+
if (!n) throw new Error("Text is required");
|
|
115
|
+
const o = d(n, t.maxChunkLength ?? b).map((l) => l.trim()).filter((l) => l.length > 0), a = await M(t), c = t.voice ?? p, i = t.speed ?? 1, r = [];
|
|
116
|
+
let s = 24e3;
|
|
117
|
+
for (let l = 0; l < o.length; l += 1) {
|
|
118
|
+
(m = t.onChunk) == null || m.call(t, { index: l, total: o.length, text: o[l] });
|
|
119
|
+
const h = await a.generate(o[l], { voice: c, speed: i });
|
|
120
|
+
r.push(h.audio), s = h.sampling_rate ?? s;
|
|
121
|
+
}
|
|
122
|
+
const f = t.gapMs ?? k;
|
|
123
|
+
return {
|
|
124
|
+
samples: g(r, Math.round(f / 1e3 * s)),
|
|
125
|
+
sampleRate: s
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function F(e, t = {}) {
|
|
129
|
+
const { samples: n, sampleRate: o } = await T(e, t);
|
|
130
|
+
return { audio: E(n, o), contentType: "audio/wav" };
|
|
131
|
+
}
|
|
132
|
+
export {
|
|
133
|
+
S as D,
|
|
134
|
+
v as K,
|
|
135
|
+
T as a,
|
|
136
|
+
g as c,
|
|
137
|
+
x as d,
|
|
138
|
+
E as e,
|
|
139
|
+
M as l,
|
|
140
|
+
K as r,
|
|
141
|
+
F as s,
|
|
142
|
+
C as w
|
|
143
|
+
};
|
|
144
|
+
//# sourceMappingURL=kokoro-node-DJ_Rxp_N.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kokoro-node-DJ_Rxp_N.js","sources":["../speech/types/types.ts","../speech/utils/wav.ts","../speech/core/kokoro-node.ts"],"sourcesContent":["/**\n * @fileoverview Type definitions for text-to-speech providers\n */\n\nexport type TTSProvider = \"kokoro\" | \"deepgram\";\n\nexport interface TTSOptions {\n text: string;\n provider?: TTSProvider;\n voice?: string;\n}\n\nexport interface TTSResult {\n audio: ArrayBuffer;\n contentType: string;\n}\n\n// Kokoro voices from the model. The `a`/`b` prefix is the accent (American /\n// British English) and the `f`/`m` that follows it is the voice's gender, which\n// is how `describeKokoroVoice` derives a listing without a second catalog.\nexport const KOKORO_VOICES = [\n \"af_heart\", \"af_alloy\", \"af_aoede\", \"af_bella\", \"af_jessica\",\n \"af_kore\", \"af_nicole\", \"af_nova\", \"af_river\", \"af_sarah\", \"af_sky\",\n \"am_adam\", \"am_echo\", \"am_eric\", \"am_fenrir\", \"am_liam\",\n \"am_michael\", \"am_onyx\", \"am_puck\", \"am_santa\",\n \"bf_alice\", \"bf_emma\", \"bf_isabella\", \"bf_lily\",\n \"bm_daniel\", \"bm_fable\", \"bm_george\", \"bm_lewis\",\n] as const;\n\nexport type KokoroVoice = (typeof KOKORO_VOICES)[number];\n\nexport interface KokoroVoiceDescription {\n id: string;\n /** Display name, e.g. `Heart` for `af_heart`. */\n name: string;\n /** `American English` or `British English`. */\n accent: string;\n gender: \"Female\" | \"Male\";\n}\n\n/**\n * Describes a voice from its id, so listings (`--list-voices`, a voice picker)\n * do not need a hand-maintained table that can drift from `KOKORO_VOICES`.\n */\nexport function describeKokoroVoice(id: string): KokoroVoiceDescription {\n const [accentCode, genderCode] = id;\n const suffix = id.slice(3) || id;\n\n return {\n id,\n name: suffix.charAt(0).toUpperCase() + suffix.slice(1),\n accent: accentCode === \"b\" ? \"British English\" : \"American English\",\n gender: genderCode === \"m\" ? \"Male\" : \"Female\",\n };\n}\n\n// Deepgram Aura speakers\nexport const DEEPGRAM_SPEAKERS = [\n \"angus\", \"asteria\", \"arcas\", \"orion\", \"orpheus\", \"athena\",\n \"luna\", \"zeus\", \"perseus\", \"helios\", \"hera\", \"stella\",\n] as const;\n\nexport type DeepgramSpeaker = (typeof DEEPGRAM_SPEAKERS)[number];\n","/**\n * @fileoverview Minimal WAV (RIFF) encoder for raw Float32 PCM.\n *\n * The browser path gets its WAV from `convertAudioBufferToWav`, which needs a Web\n * Audio `AudioBuffer`. Node has no such thing: the Kokoro model hands back a bare\n * `Float32Array`, and long documents are synthesized one chunk at a time and\n * joined before encoding. Both of those are pure buffer work, so they live here\n * with no platform dependency.\n */\n\n/** Number of bytes in the fixed RIFF/fmt/data header this encoder writes. */\nconst HEADER_BYTES = 44;\n\n/**\n * Joins per-chunk sample buffers into one contiguous track.\n *\n * @param chunks Sample buffers in playback order.\n * @param silenceSamples Samples of silence to insert between chunks, so the\n * seams between separately synthesized chunks do not sound clipped.\n */\nexport function concatSamples(\n chunks: Float32Array[],\n silenceSamples = 0\n): Float32Array {\n const gaps = Math.max(chunks.length - 1, 0) * Math.max(silenceSamples, 0);\n const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0) + gaps;\n const out = new Float32Array(total);\n\n let offset = 0;\n chunks.forEach((chunk, i) => {\n out.set(chunk, offset);\n offset += chunk.length;\n if (i < chunks.length - 1) offset += Math.max(silenceSamples, 0);\n });\n\n return out;\n}\n\n/**\n * Encodes Float32 samples (nominally -1..1) as a 16-bit PCM WAV file.\n *\n * @param samples Interleaved samples when `channels` is greater than 1.\n * @param sampleRate Sample rate of the audio, e.g. 24000 for Kokoro.\n * @param channels Channel count. Kokoro is mono.\n */\nexport function encodeWav(\n samples: Float32Array,\n sampleRate = 24000,\n channels = 1\n): ArrayBuffer {\n const buffer = new ArrayBuffer(HEADER_BYTES + samples.length * 2);\n const view = new DataView(buffer);\n\n const writeString = (offset: number, value: string): void => {\n for (let i = 0; i < value.length; i += 1) {\n view.setUint8(offset + i, value.charCodeAt(i));\n }\n };\n\n const byteRate = sampleRate * channels * 2;\n\n writeString(0, \"RIFF\");\n view.setUint32(4, 36 + samples.length * 2, true);\n writeString(8, \"WAVE\");\n writeString(12, \"fmt \");\n view.setUint32(16, 16, true); // PCM header size\n view.setUint16(20, 1, true); // format: PCM\n view.setUint16(22, channels, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, byteRate, true);\n view.setUint16(32, channels * 2, true); // block align\n view.setUint16(34, 16, true); // bits per sample\n writeString(36, \"data\");\n view.setUint32(40, samples.length * 2, true);\n\n for (let i = 0; i < samples.length; i += 1) {\n // Clamp before scaling: the model occasionally overshoots 1.0 and wrapping\n // a 16-bit sample turns that into a loud click.\n const sample = Math.max(-1, Math.min(1, samples[i]));\n view.setInt16(HEADER_BYTES + i * 2, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true);\n }\n\n return buffer;\n}\n\n/** Duration of a Float32 track in seconds, for progress and log lines. */\nexport function wavDurationSeconds(samples: Float32Array, sampleRate: number): number {\n return sampleRate > 0 ? samples.length / sampleRate : 0;\n}\n","/**\n * @fileoverview Kokoro TTS running locally on Node (or Bun/Deno) via `kokoro-js`.\n *\n * This is the engine behind `generateSpeech({ provider: \"kokoro\" })` on a server\n * and behind the `use-voice-control` CLI. The model runs on the CPU through\n * onnxruntime, so nothing is sent to a third-party service; the first call\n * downloads the weights (~90 MB at the default `q8`) into the Hugging Face cache\n * and every later call reuses the instance held in this module.\n *\n * Kokoro's context is a few hundred phonemes, so anything longer than a\n * paragraph has to be synthesized in pieces. `synthesizeSamples` chunks on\n * sentence and paragraph boundaries with `splitTextSmart`, runs the chunks in\n * order, and joins the audio with a short silence at each seam.\n */\nimport type { TTSResult } from \"../types/types\";\nimport { splitTextSmart } from \"../utils/semantic-split.js\";\nimport { concatSamples, encodeWav } from \"../utils/wav\";\n\nexport type KokoroDtype = \"fp32\" | \"fp16\" | \"q8\" | \"q4\" | \"q4f16\";\nexport type KokoroDevice = \"wasm\" | \"webgpu\" | \"cpu\";\n\nexport interface KokoroNodeOptions {\n /** Voice id, e.g. `af_heart`. Default `af_heart`. */\n voice?: string;\n /** Speaking rate; 1 is the model's natural pace. Default 1. */\n speed?: number;\n /** Hugging Face model id. Default `onnx-community/Kokoro-82M-v1.0-ONNX`. */\n model?: string;\n /** Weight quantization. `q8` (default) is the best size/quality trade on CPU. */\n dtype?: KokoroDtype;\n /** Execution device. Default `cpu`. */\n device?: KokoroDevice;\n /**\n * Target chunk size in characters. Kokoro's context is limited, so long text\n * is split before synthesis; 400 leaves headroom for phoneme expansion.\n */\n maxChunkLength?: number;\n /** Silence inserted between chunks, in milliseconds. Default 120. */\n gapMs?: number;\n /** Model download/loading progress, forwarded from transformers.js. */\n onModelProgress?: (progress: unknown) => void;\n /** Called before each chunk is synthesized, for CLI progress output. */\n onChunk?: (info: { index: number; total: number; text: string }) => void;\n}\n\nexport interface KokoroAudio {\n samples: Float32Array;\n sampleRate: number;\n}\n\nconst DEFAULT_MODEL = \"onnx-community/Kokoro-82M-v1.0-ONNX\";\nconst DEFAULT_VOICE = \"af_heart\";\nconst DEFAULT_DTYPE: KokoroDtype = \"q8\";\nconst DEFAULT_DEVICE: KokoroDevice = \"cpu\";\nconst DEFAULT_MAX_CHUNK = 400;\nconst DEFAULT_GAP_MS = 120;\n\n/** One loaded model per model/dtype/device combination, keyed by those three. */\nconst instances = new Map<string, Promise<any>>();\n\n/** True on Node, Bun and Deno — anywhere `kokoro-js` can load onnxruntime-node. */\nfunction isServerRuntime(): boolean {\n const g = globalThis as any;\n return Boolean(g.process?.versions?.node || g.Bun || g.Deno);\n}\n\n/**\n * Loads (and caches) a `kokoro-js` instance.\n *\n * `kokoro-js` is an optional dependency: a browser-only consumer should not have\n * to download onnxruntime-node, so a missing install is reported as an\n * actionable error rather than a module-resolution stack trace.\n */\nexport async function loadKokoroTTS(options: KokoroNodeOptions = {}): Promise<any> {\n const model = options.model ?? DEFAULT_MODEL;\n const dtype = options.dtype ?? DEFAULT_DTYPE;\n const device = options.device ?? (isServerRuntime() ? DEFAULT_DEVICE : \"wasm\");\n const key = `${model}|${dtype}|${device}`;\n\n const cached = instances.get(key);\n if (cached) return cached;\n\n const loading = (async () => {\n let KokoroTTS: any;\n try {\n // Kept in a variable so bundlers treat this as an optional runtime\n // dependency rather than something to resolve at build time.\n const specifier = \"kokoro-js\";\n ({ KokoroTTS } = await import(/* @vite-ignore */ specifier));\n } catch (error) {\n throw new Error(\n \"Local Kokoro speech needs the optional `kokoro-js` package. \" +\n \"Install it with `npm install kokoro-js`. \" +\n `(import failed: ${error instanceof Error ? error.message : String(error)})`\n );\n }\n\n return KokoroTTS.from_pretrained(model, {\n dtype,\n device,\n progress_callback: options.onModelProgress,\n });\n })();\n\n instances.set(key, loading);\n\n try {\n return await loading;\n } catch (error) {\n // A failed download must not poison every later attempt.\n instances.delete(key);\n throw error;\n }\n}\n\n/** Drops cached model instances. Mainly useful in tests and long-lived workers. */\nexport function resetKokoroCache(): void {\n instances.clear();\n}\n\n/**\n * Synthesizes `text` to raw samples, chunking anything longer than the model's\n * comfortable context.\n */\nexport async function synthesizeSamples(\n text: string,\n options: KokoroNodeOptions = {}\n): Promise<KokoroAudio> {\n const trimmed = (text ?? \"\").trim();\n if (!trimmed) throw new Error(\"Text is required\");\n\n const chunks = splitTextSmart(trimmed, options.maxChunkLength ?? DEFAULT_MAX_CHUNK)\n .map((chunk: string) => chunk.trim())\n .filter((chunk: string) => chunk.length > 0);\n\n const tts = await loadKokoroTTS(options);\n const voice = options.voice ?? DEFAULT_VOICE;\n const speed = options.speed ?? 1;\n\n const rendered: Float32Array[] = [];\n let sampleRate = 24000;\n\n for (let index = 0; index < chunks.length; index += 1) {\n options.onChunk?.({ index, total: chunks.length, text: chunks[index] });\n const audio = await tts.generate(chunks[index], { voice, speed });\n rendered.push(audio.audio);\n sampleRate = audio.sampling_rate ?? sampleRate;\n }\n\n const gapMs = options.gapMs ?? DEFAULT_GAP_MS;\n return {\n samples: concatSamples(rendered, Math.round((gapMs / 1000) * sampleRate)),\n sampleRate,\n };\n}\n\n/** Synthesizes `text` and encodes it as a 16-bit PCM WAV file. */\nexport async function synthesizeWav(\n text: string,\n options: KokoroNodeOptions = {}\n): Promise<TTSResult> {\n const { samples, sampleRate } = await synthesizeSamples(text, options);\n return { audio: encodeWav(samples, sampleRate), contentType: \"audio/wav\" };\n}\n"],"names":["KOKORO_VOICES","describeKokoroVoice","id","accentCode","genderCode","suffix","DEEPGRAM_SPEAKERS","HEADER_BYTES","concatSamples","chunks","silenceSamples","gaps","total","sum","chunk","out","offset","i","encodeWav","samples","sampleRate","channels","buffer","view","writeString","value","byteRate","sample","wavDurationSeconds","DEFAULT_MODEL","DEFAULT_VOICE","DEFAULT_DTYPE","DEFAULT_DEVICE","DEFAULT_MAX_CHUNK","DEFAULT_GAP_MS","instances","isServerRuntime","g","_b","_a","loadKokoroTTS","options","model","dtype","device","key","cached","loading","KokoroTTS","error","resetKokoroCache","synthesizeSamples","text","trimmed","splitTextSmart","tts","voice","speed","rendered","index","audio","gapMs","synthesizeWav"],"mappings":"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"}
|