@micdrop/server 2.3.0 → 2.5.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 🖐️🎤 Micdrop: Real-Time Voice Conversations with AI
2
2
 
3
- [Micdrop website](https://micdrop.dev) | [Documentation](https://micdrop.dev/docs/server) | [Demo](../../examples/demo-server)
3
+ [Micdrop website](https://micdrop.dev) | [Documentation](https://micdrop.dev/docs/server) | [Basic example](../../examples/basic) | [Demo](../../examples/advanced)
4
4
 
5
5
  Micdrop is a set of open source Typescript packages to build real-time voice conversations with AI agents. It handles all the complexities on the browser and server side (microphone, speaker, VAD, network communication, etc) and provides ready-to-use implementations for various AI providers.
6
6
 
package/dist/index.d.mts CHANGED
@@ -21,6 +21,28 @@ declare enum MicdropServerCommands {
21
21
  EndCall = "EndCall",
22
22
  ToolCall = "ToolCall"
23
23
  }
24
+ /**
25
+ * Hears whether a sentence has landed, where voice activity detection only
26
+ * hears whether someone is speaking.
27
+ *
28
+ * `SmartTurn` from `@micdrop/smart-turn` implements it, and so can anything
29
+ * else, a call to a service included. Both sides of a call can hold one, the
30
+ * client to decide when its turn ends and the server to decide when to answer.
31
+ */
32
+ interface TurnDetector {
33
+ /**
34
+ * Feeds the audio received since the last call
35
+ * @param samples - Mono samples, in the -1..1 range
36
+ * @param sampleRate - Sample rate of `samples`, in Hz
37
+ */
38
+ push(samples: Float32Array, sampleRate?: number): void;
39
+ /** Answers whether the turn pushed so far sounds finished */
40
+ predict(): Promise<{
41
+ complete: boolean;
42
+ }>;
43
+ /** Starts a new turn, forgetting the previous one */
44
+ reset(): void;
45
+ }
24
46
  interface MicdropCallSummary {
25
47
  conversation: MicdropConversation;
26
48
  duration: number;
@@ -327,6 +349,7 @@ declare abstract class SentenceTTS extends TTS {
327
349
  private controller?;
328
350
  private generation;
329
351
  private counter;
352
+ private synthesizing;
330
353
  /**
331
354
  * Turns one sentence into PCM16 audio at the client's sample rate.
332
355
  *
@@ -335,6 +358,16 @@ declare abstract class SentenceTTS extends TTS {
335
358
  * nothing emits nothing, which is how a cancelled synthesis reports back.
336
359
  */
337
360
  protected abstract synthesize(text: string, signal: AbortSignal): Promise<Buffer | undefined>;
361
+ /**
362
+ * Emits a piece of the sentence being synthesized.
363
+ *
364
+ * A model that generates progressively can hand its chunks over as they
365
+ * come rather than waiting for the sentence to be finished, which brings
366
+ * the first word forward by the duration of that sentence. The false it
367
+ * returns says the utterance was cancelled or replaced, so the generation
368
+ * it comes from can be stopped there.
369
+ */
370
+ protected emitAudio(audio: Buffer): boolean;
338
371
  speak(textStream: Readable): void;
339
372
  cancel(): void;
340
373
  private enqueue;
@@ -352,6 +385,17 @@ interface MicdropConfig {
352
385
  agent: Agent;
353
386
  stt: STT;
354
387
  tts: TTS;
388
+ /**
389
+ * Waits for the rest of the sentence when the speaker paused in the middle
390
+ * of one, instead of answering an unfinished question.
391
+ *
392
+ * Prefer running the detector in the client, which reaches the same decision
393
+ * without the round trip and can then close its turns sooner. This is the
394
+ * option for the browsers where the model has nowhere to run.
395
+ */
396
+ turnDetector?: TurnDetector;
397
+ /** How long to wait for the rest of a sentence, 4000 ms by default */
398
+ turnMaxWait?: number;
355
399
  }
356
400
  declare class MicdropServer extends EventEmitter<MicdropServerEvents> {
357
401
  socket: WebSocket$1 | null;
@@ -363,6 +407,8 @@ declare class MicdropServer extends EventEmitter<MicdropServerEvents> {
363
407
  private isProcessingQueue;
364
408
  private currentUserStream?;
365
409
  private userSpeechChunks;
410
+ private turnComplete?;
411
+ private heldTurnTimer?;
366
412
  constructor(socket: WebSocket$1, config: MicdropConfig);
367
413
  private log;
368
414
  private processQueue;
@@ -374,6 +420,17 @@ declare class MicdropServer extends EventEmitter<MicdropServerEvents> {
374
420
  private onMute;
375
421
  private onStartSpeaking;
376
422
  private onStopSpeaking;
423
+ private predictTurnComplete;
424
+ /** Answers, unless the sentence sounds like it has more coming */
425
+ private answerUserTurn;
426
+ /**
427
+ * Answers anyway if the rest of the sentence never comes.
428
+ *
429
+ * Without it, a detector that hears an unfinished sentence where there is
430
+ * none leaves the call silent for good.
431
+ */
432
+ private holdTurn;
433
+ private releaseHeldTurn;
377
434
  private onTranscriptSTT;
378
435
  private onAudioTTS;
379
436
  private sendFirstMessage;
@@ -416,4 +473,4 @@ declare class MicdropRecorder extends EventEmitter<MicdropRecorderEvents> {
416
473
 
417
474
  declare function waitForParams<CallParams>(socket: WebSocket$1, validate: (params: any) => CallParams): Promise<CallParams>;
418
475
 
419
- export { AUTO_END_CALL_PROMPT, AUTO_END_CALL_TOOL_NAME, AUTO_IGNORE_USER_NOISE_PROMPT, AUTO_IGNORE_USER_NOISE_TOOL_NAME, AUTO_SEMANTIC_TURN_PROMPT, AUTO_SEMANTIC_TURN_TOOL_NAME, Agent, type AgentEvents, type AgentOptions, type AudioMessage, type DeepPartial, type ExtractJsonOptions, type ExtractOptions, type ExtractTagOptions, FallbackAgent, type FallbackAgentOptions, FallbackSTT, type FallbackSTTOptions, FallbackTTS, type FallbackTTSOptions, Logger, type MicdropAnswerMetadata, type MicdropCallSummary, MicdropClientCommands, type MicdropConfig, type MicdropConversation, type MicdropConversationItem, type MicdropConversationMessage, type MicdropConversationToolCall, type MicdropConversationToolResult, MicdropError, MicdropErrorCode, MicdropRecorder, type MicdropRecorderEvents, MicdropServer, MicdropServerCommands, type MicdropServerEvents, type MicdropToolCall, MockAgent, MockSTT, MockTTS, Pcm16Resampler, STT, type STTEvents, SentenceSplitter, SentenceTTS, TTS, type TTSEvents, type Tool, float32ToPcm16, handleError, pcm16ToFloat32, waitForParams };
476
+ export { AUTO_END_CALL_PROMPT, AUTO_END_CALL_TOOL_NAME, AUTO_IGNORE_USER_NOISE_PROMPT, AUTO_IGNORE_USER_NOISE_TOOL_NAME, AUTO_SEMANTIC_TURN_PROMPT, AUTO_SEMANTIC_TURN_TOOL_NAME, Agent, type AgentEvents, type AgentOptions, type AudioMessage, type DeepPartial, type ExtractJsonOptions, type ExtractOptions, type ExtractTagOptions, FallbackAgent, type FallbackAgentOptions, FallbackSTT, type FallbackSTTOptions, FallbackTTS, type FallbackTTSOptions, Logger, type MicdropAnswerMetadata, type MicdropCallSummary, MicdropClientCommands, type MicdropConfig, type MicdropConversation, type MicdropConversationItem, type MicdropConversationMessage, type MicdropConversationToolCall, type MicdropConversationToolResult, MicdropError, MicdropErrorCode, MicdropRecorder, type MicdropRecorderEvents, MicdropServer, MicdropServerCommands, type MicdropServerEvents, type MicdropToolCall, MockAgent, MockSTT, MockTTS, Pcm16Resampler, STT, type STTEvents, SentenceSplitter, SentenceTTS, TTS, type TTSEvents, type Tool, type TurnDetector, float32ToPcm16, handleError, pcm16ToFloat32, waitForParams };
package/dist/index.d.ts CHANGED
@@ -21,6 +21,28 @@ declare enum MicdropServerCommands {
21
21
  EndCall = "EndCall",
22
22
  ToolCall = "ToolCall"
23
23
  }
24
+ /**
25
+ * Hears whether a sentence has landed, where voice activity detection only
26
+ * hears whether someone is speaking.
27
+ *
28
+ * `SmartTurn` from `@micdrop/smart-turn` implements it, and so can anything
29
+ * else, a call to a service included. Both sides of a call can hold one, the
30
+ * client to decide when its turn ends and the server to decide when to answer.
31
+ */
32
+ interface TurnDetector {
33
+ /**
34
+ * Feeds the audio received since the last call
35
+ * @param samples - Mono samples, in the -1..1 range
36
+ * @param sampleRate - Sample rate of `samples`, in Hz
37
+ */
38
+ push(samples: Float32Array, sampleRate?: number): void;
39
+ /** Answers whether the turn pushed so far sounds finished */
40
+ predict(): Promise<{
41
+ complete: boolean;
42
+ }>;
43
+ /** Starts a new turn, forgetting the previous one */
44
+ reset(): void;
45
+ }
24
46
  interface MicdropCallSummary {
25
47
  conversation: MicdropConversation;
26
48
  duration: number;
@@ -327,6 +349,7 @@ declare abstract class SentenceTTS extends TTS {
327
349
  private controller?;
328
350
  private generation;
329
351
  private counter;
352
+ private synthesizing;
330
353
  /**
331
354
  * Turns one sentence into PCM16 audio at the client's sample rate.
332
355
  *
@@ -335,6 +358,16 @@ declare abstract class SentenceTTS extends TTS {
335
358
  * nothing emits nothing, which is how a cancelled synthesis reports back.
336
359
  */
337
360
  protected abstract synthesize(text: string, signal: AbortSignal): Promise<Buffer | undefined>;
361
+ /**
362
+ * Emits a piece of the sentence being synthesized.
363
+ *
364
+ * A model that generates progressively can hand its chunks over as they
365
+ * come rather than waiting for the sentence to be finished, which brings
366
+ * the first word forward by the duration of that sentence. The false it
367
+ * returns says the utterance was cancelled or replaced, so the generation
368
+ * it comes from can be stopped there.
369
+ */
370
+ protected emitAudio(audio: Buffer): boolean;
338
371
  speak(textStream: Readable): void;
339
372
  cancel(): void;
340
373
  private enqueue;
@@ -352,6 +385,17 @@ interface MicdropConfig {
352
385
  agent: Agent;
353
386
  stt: STT;
354
387
  tts: TTS;
388
+ /**
389
+ * Waits for the rest of the sentence when the speaker paused in the middle
390
+ * of one, instead of answering an unfinished question.
391
+ *
392
+ * Prefer running the detector in the client, which reaches the same decision
393
+ * without the round trip and can then close its turns sooner. This is the
394
+ * option for the browsers where the model has nowhere to run.
395
+ */
396
+ turnDetector?: TurnDetector;
397
+ /** How long to wait for the rest of a sentence, 4000 ms by default */
398
+ turnMaxWait?: number;
355
399
  }
356
400
  declare class MicdropServer extends EventEmitter<MicdropServerEvents> {
357
401
  socket: WebSocket$1 | null;
@@ -363,6 +407,8 @@ declare class MicdropServer extends EventEmitter<MicdropServerEvents> {
363
407
  private isProcessingQueue;
364
408
  private currentUserStream?;
365
409
  private userSpeechChunks;
410
+ private turnComplete?;
411
+ private heldTurnTimer?;
366
412
  constructor(socket: WebSocket$1, config: MicdropConfig);
367
413
  private log;
368
414
  private processQueue;
@@ -374,6 +420,17 @@ declare class MicdropServer extends EventEmitter<MicdropServerEvents> {
374
420
  private onMute;
375
421
  private onStartSpeaking;
376
422
  private onStopSpeaking;
423
+ private predictTurnComplete;
424
+ /** Answers, unless the sentence sounds like it has more coming */
425
+ private answerUserTurn;
426
+ /**
427
+ * Answers anyway if the rest of the sentence never comes.
428
+ *
429
+ * Without it, a detector that hears an unfinished sentence where there is
430
+ * none leaves the call silent for good.
431
+ */
432
+ private holdTurn;
433
+ private releaseHeldTurn;
377
434
  private onTranscriptSTT;
378
435
  private onAudioTTS;
379
436
  private sendFirstMessage;
@@ -416,4 +473,4 @@ declare class MicdropRecorder extends EventEmitter<MicdropRecorderEvents> {
416
473
 
417
474
  declare function waitForParams<CallParams>(socket: WebSocket$1, validate: (params: any) => CallParams): Promise<CallParams>;
418
475
 
419
- export { AUTO_END_CALL_PROMPT, AUTO_END_CALL_TOOL_NAME, AUTO_IGNORE_USER_NOISE_PROMPT, AUTO_IGNORE_USER_NOISE_TOOL_NAME, AUTO_SEMANTIC_TURN_PROMPT, AUTO_SEMANTIC_TURN_TOOL_NAME, Agent, type AgentEvents, type AgentOptions, type AudioMessage, type DeepPartial, type ExtractJsonOptions, type ExtractOptions, type ExtractTagOptions, FallbackAgent, type FallbackAgentOptions, FallbackSTT, type FallbackSTTOptions, FallbackTTS, type FallbackTTSOptions, Logger, type MicdropAnswerMetadata, type MicdropCallSummary, MicdropClientCommands, type MicdropConfig, type MicdropConversation, type MicdropConversationItem, type MicdropConversationMessage, type MicdropConversationToolCall, type MicdropConversationToolResult, MicdropError, MicdropErrorCode, MicdropRecorder, type MicdropRecorderEvents, MicdropServer, MicdropServerCommands, type MicdropServerEvents, type MicdropToolCall, MockAgent, MockSTT, MockTTS, Pcm16Resampler, STT, type STTEvents, SentenceSplitter, SentenceTTS, TTS, type TTSEvents, type Tool, float32ToPcm16, handleError, pcm16ToFloat32, waitForParams };
476
+ export { AUTO_END_CALL_PROMPT, AUTO_END_CALL_TOOL_NAME, AUTO_IGNORE_USER_NOISE_PROMPT, AUTO_IGNORE_USER_NOISE_TOOL_NAME, AUTO_SEMANTIC_TURN_PROMPT, AUTO_SEMANTIC_TURN_TOOL_NAME, Agent, type AgentEvents, type AgentOptions, type AudioMessage, type DeepPartial, type ExtractJsonOptions, type ExtractOptions, type ExtractTagOptions, FallbackAgent, type FallbackAgentOptions, FallbackSTT, type FallbackSTTOptions, FallbackTTS, type FallbackTTSOptions, Logger, type MicdropAnswerMetadata, type MicdropCallSummary, MicdropClientCommands, type MicdropConfig, type MicdropConversation, type MicdropConversationItem, type MicdropConversationMessage, type MicdropConversationToolCall, type MicdropConversationToolResult, MicdropError, MicdropErrorCode, MicdropRecorder, type MicdropRecorderEvents, MicdropServer, MicdropServerCommands, type MicdropServerEvents, type MicdropToolCall, MockAgent, MockSTT, MockTTS, Pcm16Resampler, STT, type STTEvents, SentenceSplitter, SentenceTTS, TTS, type TTSEvents, type Tool, type TurnDetector, float32ToPcm16, handleError, pcm16ToFloat32, waitForParams };
package/dist/index.js CHANGED
@@ -511,6 +511,8 @@ var MicdropServerCommands = /* @__PURE__ */ ((MicdropServerCommands2) => {
511
511
  })(MicdropServerCommands || {});
512
512
 
513
513
  // src/MicdropServer.ts
514
+ var USER_SAMPLE_RATE = 16e3;
515
+ var DEFAULT_TURN_MAX_WAIT = 4e3;
514
516
  var MicdropServer = class extends import_eventemitter32.EventEmitter {
515
517
  constructor(socket, config) {
516
518
  super();
@@ -522,6 +524,7 @@ var MicdropServer = class extends import_eventemitter32.EventEmitter {
522
524
  this.isProcessingQueue = false;
523
525
  this.userSpeechChunks = 0;
524
526
  this.onClose = () => {
527
+ this.releaseHeldTurn();
525
528
  if (!this.config) return;
526
529
  this.log("Connection closed");
527
530
  const duration = Math.round((Date.now() - this.startTime) / 1e3);
@@ -565,8 +568,7 @@ var MicdropServer = class extends import_eventemitter32.EventEmitter {
565
568
  this.config.agent.addUserMessage(transcript);
566
569
  if (!this.currentUserStream) {
567
570
  this.log("User stopped speaking, answering");
568
- this.cancel();
569
- this.answer();
571
+ this.answerUserTurn();
570
572
  }
571
573
  };
572
574
  this.onAudioTTS = (audio) => {
@@ -639,6 +641,7 @@ var MicdropServer = class extends import_eventemitter32.EventEmitter {
639
641
  this.log(`Received chunk (${chunk.byteLength} bytes)`);
640
642
  this.currentUserStream?.write(chunk);
641
643
  this.userSpeechChunks++;
644
+ this.config?.turnDetector?.push(pcm16ToFloat32(chunk), USER_SAMPLE_RATE);
642
645
  this.emit("UserAudio", chunk);
643
646
  }
644
647
  onMute() {
@@ -652,6 +655,9 @@ var MicdropServer = class extends import_eventemitter32.EventEmitter {
652
655
  this.userSpeechChunks = 0;
653
656
  this.currentUserStream?.end();
654
657
  this.currentUserStream = new import_stream2.PassThrough();
658
+ this.config.turnDetector?.reset();
659
+ this.turnComplete = void 0;
660
+ this.releaseHeldTurn();
655
661
  this.config.stt.transcribe(this.currentUserStream);
656
662
  this.cancel();
657
663
  }
@@ -664,15 +670,60 @@ var MicdropServer = class extends import_eventemitter32.EventEmitter {
664
670
  this.socket?.send("SkipAnswer" /* SkipAnswer */);
665
671
  return;
666
672
  }
673
+ this.turnComplete = this.predictTurnComplete();
667
674
  const conversation = this.config?.agent.conversation;
668
675
  const lastMessage = conversation?.[conversation.length - 1];
669
676
  if (lastMessage?.role === "user" && this.lastMessageSpeeched !== lastMessage) {
670
677
  this.log(
671
678
  "User stopped speaking and a transcript already exists, answering"
672
679
  );
680
+ this.answerUserTurn();
681
+ }
682
+ }
683
+ async predictTurnComplete() {
684
+ const detector = this.config?.turnDetector;
685
+ if (!detector) return true;
686
+ try {
687
+ const { complete } = await detector.predict();
688
+ this.log(`Turn sounds ${complete ? "finished" : "unfinished"}`);
689
+ return complete;
690
+ } catch (error) {
691
+ this.log(`Turn detection failed: ${error}`);
692
+ return true;
693
+ }
694
+ }
695
+ /** Answers, unless the sentence sounds like it has more coming */
696
+ async answerUserTurn() {
697
+ const complete = await (this.turnComplete ?? Promise.resolve(true));
698
+ if (!complete) {
699
+ this.log("Waiting for the rest of the sentence");
700
+ this.socket?.send("SkipAnswer" /* SkipAnswer */);
701
+ this.holdTurn();
702
+ return;
703
+ }
704
+ this.releaseHeldTurn();
705
+ this.cancel();
706
+ this.answer();
707
+ }
708
+ /**
709
+ * Answers anyway if the rest of the sentence never comes.
710
+ *
711
+ * Without it, a detector that hears an unfinished sentence where there is
712
+ * none leaves the call silent for good.
713
+ */
714
+ holdTurn() {
715
+ this.releaseHeldTurn();
716
+ this.heldTurnTimer = setTimeout(() => {
717
+ this.heldTurnTimer = void 0;
718
+ this.log("Nothing more came, answering");
673
719
  this.cancel();
674
720
  this.answer();
675
- }
721
+ }, this.config?.turnMaxWait ?? DEFAULT_TURN_MAX_WAIT);
722
+ }
723
+ releaseHeldTurn() {
724
+ if (!this.heldTurnTimer) return;
725
+ clearTimeout(this.heldTurnTimer);
726
+ this.heldTurnTimer = void 0;
676
727
  }
677
728
  sendFirstMessage() {
678
729
  if (!this.config) return;
@@ -1101,6 +1152,22 @@ var SentenceTTS = class extends TTS {
1101
1152
  // whether it is still the one that should be heard.
1102
1153
  this.generation = 0;
1103
1154
  this.counter = 0;
1155
+ // Identifies the current speak() call
1156
+ this.synthesizing = 0;
1157
+ }
1158
+ /**
1159
+ * Emits a piece of the sentence being synthesized.
1160
+ *
1161
+ * A model that generates progressively can hand its chunks over as they
1162
+ * come rather than waiting for the sentence to be finished, which brings
1163
+ * the first word forward by the duration of that sentence. The false it
1164
+ * returns says the utterance was cancelled or replaced, so the generation
1165
+ * it comes from can be stopped there.
1166
+ */
1167
+ emitAudio(audio) {
1168
+ if (this.synthesizing !== this.counter) return false;
1169
+ if (audio.length) this.emit("Audio", audio);
1170
+ return true;
1104
1171
  }
1105
1172
  speak(textStream) {
1106
1173
  const generation = ++this.generation;
@@ -1146,6 +1213,7 @@ var SentenceTTS = class extends TTS {
1146
1213
  this.draining = true;
1147
1214
  while (this.queue.length > 0) {
1148
1215
  const counter = this.counter;
1216
+ this.synthesizing = counter;
1149
1217
  const text = this.queue.shift();
1150
1218
  const controller = new AbortController();
1151
1219
  this.controller = controller;