🎤 Micdrop

Dictation and Text-Only Calls

agent and tts are optional. Leaving one of them out changes what a call does, while the microphone, the voice activity detection and the transport keep working the same way.

ConfigurationWhat the user gets
stt + agent + ttsA full spoken conversation
stt + agentAn assistant that answers in writing
sttDictation, with nothing answering back
stt + ttsA call your own code drives, speaking when it decides to

Dictation

With a speech to text alone, the call transcribes and stays quiet. Each sentence reaches the client as a message, exactly as a user message does in a full conversation.

import { MicdropServer } from '@micdrop/server'
import { GladiaSTT } from '@micdrop/gladia'
new MicdropServer(socket, {
stt: new GladiaSTT({ apiKey: process.env.GLADIA_API_KEY }),
})

The client displays them from conversation, like any other message:

Micdrop.on('StateChange', (state) => {
const text = state.conversation
.filter((message) => message.role === 'user')
.map((message) => message.content)
.join(' ')
textarea.value = text
})

Each sentence lands once the speech to text has settled it, so the text grows sentence by sentence, at the pauses.

The dictation example does exactly this, in a page with a microphone button, a language picker and a text area.

An assistant that writes instead of speaking

Without a text to speech, the agent still answers, and its answer reaches the client as a message. Nothing is synthesized, so nothing plays.

new MicdropServer(socket, {
stt,
agent: new OpenaiAgent({
apiKey: process.env.OPENAI_API_KEY,
systemPrompt: 'You are a helpful assistant',
}),
})

The client leaves its processing state as soon as the answer is complete, and isAssistantSpeaking stays false for the whole call, since no voice ever plays.

Turn on partialMessages to stream the answer as it is written, which is what makes a text answer feel as immediate as a spoken one.

Driving the conversation yourself

A call with a voice and no agent speaks only when your code asks it to. This is the shape to reach for when the answers come from a state machine, a workflow engine, or an orchestration framework you already run.

const server = new MicdropServer(socket, { stt, tts })
server.on('Message', async (message) => {
if (message.role !== 'user') return
const answer = await myOwnOrchestrator(message.content)
server.speak(answer)
})

speak() accepts a string or a stream of text. Given a string, it also records the sentence in the conversation, since no agent is there to do it.

Reading the conversation

server.conversation holds everything said, whether an agent keeps it or the server does, and stays readable once the call is over.

const server = new MicdropServer(socket, { stt })
server.on('End', ({ conversation, duration }) => {
console.log(`${conversation.length} sentences in ${duration}s`)
})

The Message event fires for every message added on either side. See Save Messages for storing them as they arrive.

With an agent, the conversation opens with the system message that carries the prompt. Without one, it holds nothing but what was said.

What still needs an agent

generateFirstMessage asks the agent for its opening line, so it does nothing without one. A static firstMessage works either way, and is spoken when a text to speech is configured.

Tools, automatic call ending, extraction and semantic turn detection are agent features, and are out of reach in a call that has no agent.