@molecule/api-ai-speech-llm
Provider bond · ai-speech · API (Node) · v1.0.1 · Apache-2.0
Language-model speech-to-text provider for molecule.dev — transcribes audio through any audio-capable @molecule/api-ai chat model
npm install @molecule/api-ai-speech-llmnpm · Source on GitHub · Implements @molecule/api-ai-speech
How it works
@molecule/api-ai-speech-llm is a provider bond on the API (Node) side: it implements the ai-speech core interface (@molecule/api-ai-speech) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
import { setProvider as setAi } from '@molecule/api-ai'
import { provider as google } from '@molecule/api-ai-google'
import { requireProvider, setProvider } from '@molecule/api-ai-speech'
import { createProvider } from '@molecule/api-ai-speech-llm'
setAi(google) // the app's AI provider — its model must accept audio
setProvider(createProvider()) // or createProvider({ ai, model }) for another
const { text } = await requireProvider().transcribe!({
audio: new Uint8Array(recordingBytes),
filename: 'memo.webm',
language: 'en',
})Works with: @molecule/api-ai, @molecule/api-ai-speech
Reference
Language-model speech-to-text provider for molecule.dev.
Implements transcribe from @molecule/api-ai-speech with any
@molecule/api-ai chat model that accepts audio input, so an app with no
speech service still gets batch transcription through the model it already
pays for — hosted or self-hosted.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { setProvider as setAi } from '@molecule/api-ai'
import { provider as google } from '@molecule/api-ai-google'
import { requireProvider, setProvider } from '@molecule/api-ai-speech'
import { createProvider } from '@molecule/api-ai-speech-llm'
setAi(google) // the app's AI provider — its model must accept audio
setProvider(createProvider()) // or createProvider({ ai, model }) for another
const { text } = await requireProvider().transcribe!({
audio: new Uint8Array(recordingBytes),
filename: 'memo.webm',
language: 'en',
})
Type
provider
Installation
npm install @molecule/api-ai-speech-llm @molecule/api-ai @molecule/api-ai-speech
API
Interfaces
LlmSpeechConfig
Options for createProvider. ai falls back to the bonded ai
provider; every other field has a prompt-level default.
interface LlmSpeechConfig {
/**
* The AI provider to transcribe with. Defaults to the provider bonded as
* `ai` (`@molecule/api-ai` `requireProvider()`), whose model must accept
* audio input.
*/
ai?: AIProvider
/** The model id to pass to the provider. Omit to use the provider's default. */
model?: string
/** Default language hint when a `transcribe` call passes none. */
language?: string
/** Extra instructions, e.g. "Medical dictation; keep drug names verbatim". Appended to the system prompt. */
instructions?: string
/** Output token ceiling. Defaults to a length derived from the audio size. */
maxOutputTokens?: number
}
Functions
systemPrompt(options)
The system prompt for one transcription pass.
function systemPrompt(options: {
diarize?: boolean
maxSpeakers?: number
instructions?: string
}): string
options— Diarization request and extra caller instructions.options.diarize— Ask the model to label speakers line by line.options.maxSpeakers— Upper bound on speakers to label.options.instructions— Extra caller instructions, appended last.
Returns: The system prompt.
userPrompt(language, context)
The text part of the user message (the audio block is sent alongside it).
function userPrompt(language?: string, context?: string): string
language— Optional language hint.context— Optional context / spelling hints.
Returns: The user-side instruction.
audioMediaType(filename)
Picks the audio MIME type from a filename.
function audioMediaType(filename: string | undefined): string
filename— Filename hint (defaults to WAV when absent or unknown).
Returns: The MIME type.
parseSpeakerLines(transcript)
Parses speaker_<n>: text lines into labelled segments.
function parseSpeakerLines(
transcript: string,
): { segments: TranscriptionSegment[]; text: string } | null
transcript— The model's diarized answer.
Returns: The segments (start = end = 0 — the model gives no timings) and the transcript with labels removed, or null when no line carried a label.
createProvider(config)
Creates a language-model speech provider.
function createProvider(config?: LlmSpeechConfig): AISpeechProvider
config— Which AI provider/model to use and how to prompt it.
Returns: An AISpeechProvider backed by an audio-capable chat model.
Constants
provider
The provider implementation.
const provider: AISpeechProvider
Core Interface
Implements @molecule/api-ai-speech interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/api-ai-speech'
import { provider } from '@molecule/api-ai-speech-llm'
export function setupAiSpeechLlm(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-ai>=1.3.0@molecule/api-ai-speech>=1.1.0
Runtime Dependencies
-
@molecule/api-ai -
@molecule/api-ai-speech -
The model must accept audio input. Most chat models do not — a text-only model fails the request or invents a transcript. Do not bond this provider for a non-audio model; check the model catalog first and pass one with
createProvider({ model }). -
Batch only. No
transcribeStream, nodiarize(), notranslate, no TTS. Feature-detect per the core.responseFormat: 'srt' | 'vtt'throws — a chat model returns no timings — and no result carrieswordsorduration. -
diarize: trueis best-effort text labelling, NOT acoustic diarization — treat it as unreliable. The model is asked to prefix lines withspeaker_<n>:; those becomesegments[].speaker, and every segment hasstart = end = 0because there are no timings. Use@molecule/api-ai-speech-nemo-speechwhen "who said what" matters. -
A model is a transcriber that may "improve" the speaker (fix grammar, drop fillers, translate) despite the prompt. Spot-check a new model before trusting it in bulk; markdown fences are stripped, other chatter lands in
textverbatim. -
The
filenameextension picks the audio MIME type (.webm→audio/webm,.m4a→audio/mp4, unknown →audio/wav); whether the provider accepts that container is up to the chat bond and model. -
Uses the AI provider bonded as
aiunless you pass one. Spend is that provider's token spend — audio bills as input tokens.
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip. The wired provider implements a subset (TTS, STT, or both) — feature-detect and run only the direction(s) it actually exposes:
- TTS: each read-aloud / narration / voice-note flow the app defines
returns real PLAYABLE audio — the response carries an audio Content-Type
(e.g.
audio/mpeg) and non-trivial bytes (not a 0-byte file, not a JSON-encoded blob), and the UI's<audio>/player actually plays it. The spoken audio reflects the input text you sent. - TTS: synthesizing two different strings yields two audibly different clips (distinct byte payloads) — proves the text drives synthesis and you are not hearing a cached/placeholder file.
- TTS: any exposed voice/format/speed option takes effect — switching
voice (
voice/voiceId) or output format (responseFormat/outputFormat) changes the returned audio (audible voice, Content-Type, or file extension); an unknown voice/format surfaces a visible error rather than silently falling back. - STT: transcribing a KNOWN spoken clip returns text that MATCHES the words spoken (not empty, not garbage) and renders in the UI; translating a non-English clip returns English text. Any timestamps the UI shows line up with the audio.
- STT: a silent / empty / too-short clip is handled gracefully — an empty or "no speech detected" result shown in the UI, never a crash or a spinner that hangs forever.
- The audio artifact is served from the app's OWN origin (a stored upload or a route the app serves), NOT a raw expiring provider URL — copy the audio src, reload it later, and confirm it still plays.
- A provider error (bad key, quota, unsupported format/voice, or a capability the wired provider lacks) surfaces as a readable message in the UI, not an unhandled crash or a blank/broken player.
- Synthesis/transcription runs SERVER-SIDE and authorized: the provider key never reaches the browser (check the network tab / client bundle), and no unauthenticated or unbounded endpoint lets a caller fire arbitrary synthesize/transcribe calls — each is billed per character / per minute of audio, so an open endpoint is a cost-DoS vector.