@absolutejs/meeting 0.0.1-beta.3 → 0.0.1-beta.5

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.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Tiny hand-rolled event emitter — a `Set` of listeners per event plus a fan-out
3
+ * `emit`. Deliberately not Node's `EventEmitter`: this runs in the browser too,
4
+ * keeps zero deps, and stays fully typed against an event map (no `any`, no
5
+ * `as never`). Used by the meeting core and the buffer source, and the shape the
6
+ * platform adapters mirror.
7
+ *
8
+ * Construct with the exhaustive list of event names so each set exists up front;
9
+ * the mapped type then guarantees `on`/`emit` are exhaustive over the map.
10
+ */
11
+ export type Listener<T> = (payload: T) => void | Promise<void>;
12
+ export type Emitter<EventMap> = {
13
+ on: <K extends keyof EventMap>(event: K, handler: Listener<EventMap[K]>) => () => void;
14
+ emit: <K extends keyof EventMap>(event: K, payload: EventMap[K]) => void;
15
+ };
16
+ export declare const createEmitter: <EventMap>(events: readonly (keyof EventMap)[]) => Emitter<EventMap>;
package/dist/index.d.ts CHANGED
@@ -2,4 +2,4 @@ export { createMeeting } from "./meeting";
2
2
  export type { MeetingSession, MeetingTurn, MeetingEventMap, CreateMeetingOptions, } from "./meeting";
3
3
  export { createBufferMeetingSource } from "./bufferSource";
4
4
  export type { BufferMeetingSourceOptions } from "./bufferSource";
5
- export type { MeetingSource, MeetingSourceEventMap, MeetingParticipant, SpeakAudio, } from "./source";
5
+ export type { ChatMessage, MeetingCapabilities, MeetingSource, MeetingSourceEventMap, MeetingParticipant, SpeakAudio, } from "./source";
package/dist/index.js CHANGED
@@ -3,6 +3,28 @@
3
3
  import {
4
4
  createVoiceScribe
5
5
  } from "@absolutejs/voice";
6
+
7
+ // src/emitter.ts
8
+ var createEmitter = (events) => {
9
+ const listeners = {};
10
+ for (const event of events) {
11
+ listeners[event] = new Set;
12
+ }
13
+ return {
14
+ emit: (event, payload) => {
15
+ for (const handler of listeners[event])
16
+ handler(payload);
17
+ },
18
+ on: (event, handler) => {
19
+ listeners[event].add(handler);
20
+ return () => {
21
+ listeners[event].delete(handler);
22
+ };
23
+ }
24
+ };
25
+ };
26
+
27
+ // src/meeting.ts
6
28
  var createMeeting = async (options) => {
7
29
  const scribe = await createVoiceScribe({
8
30
  format: options.source.format,
@@ -14,16 +36,13 @@ var createMeeting = async (options) => {
14
36
  });
15
37
  const participants = new Map;
16
38
  let lastSpeaker;
17
- const listeners = {
18
- end: new Set,
19
- error: new Set,
20
- participant: new Set,
21
- turn: new Set
22
- };
23
- const emit = (event, payload) => {
24
- for (const handler of listeners[event])
25
- handler(payload);
26
- };
39
+ const { emit, on } = createEmitter([
40
+ "chat",
41
+ "end",
42
+ "error",
43
+ "participant",
44
+ "turn"
45
+ ]);
27
46
  const resolveParticipant = (turn) => participants.get(turn.speaker) ?? (lastSpeaker ? participants.get(lastSpeaker) : undefined);
28
47
  scribe.on("turn", ({ turn }) => {
29
48
  emit("turn", { turn: { ...turn, participant: resolveParticipant(turn) } });
@@ -48,21 +67,28 @@ var createMeeting = async (options) => {
48
67
  if (participant)
49
68
  lastSpeaker = participant;
50
69
  scribe.send(chunk);
51
- }), options.source.on("participant", ({ participant }) => {
52
- participants.set(participant.id, participant);
53
- emit("participant", { participant });
54
- }), options.source.on("end", ({ reason }) => void finalize(reason)), options.source.on("error", ({ error }) => emit("error", { error })));
70
+ }), options.source.on("participant", ({ participant, status }) => {
71
+ if (status !== "left")
72
+ participants.set(participant.id, participant);
73
+ emit("participant", { participant, ...status ? { status } : {} });
74
+ }), options.source.on("chat", (payload) => emit("chat", payload)), options.source.on("end", ({ reason }) => void finalize(reason)), options.source.on("error", ({ error }) => emit("error", { error })));
75
+ const capabilities = options.source.capabilities ?? {
76
+ canChat: Boolean(options.source.sendChat),
77
+ canSpeak: Boolean(options.source.speak)
78
+ };
55
79
  return {
80
+ capabilities,
56
81
  getParticipants: () => [...participants.values()],
57
82
  getTranscript: () => scribe.getTranscript().map((turn) => ({
58
83
  ...turn,
59
84
  participant: resolveParticipant(turn)
60
85
  })),
61
- on: (event, handler) => {
62
- listeners[event].add(handler);
63
- return () => {
64
- listeners[event].delete(handler);
65
- };
86
+ on,
87
+ sendChat: async (text, opts) => {
88
+ if (!options.source.sendChat) {
89
+ throw new Error("meeting.sendChat: this source does not implement sendChat() \u2014 the bot can't post to the call chat");
90
+ }
91
+ await options.source.sendChat(text, opts);
66
92
  },
67
93
  speak: async (audio) => {
68
94
  if (!options.source.speak) {
@@ -70,6 +96,9 @@ var createMeeting = async (options) => {
70
96
  }
71
97
  await options.source.speak(audio);
72
98
  },
99
+ stopSpeaking: async () => {
100
+ await options.source.stopSpeaking?.();
101
+ },
73
102
  start: () => options.source.start(),
74
103
  stop: async (reason) => {
75
104
  await options.source.stop(reason);
@@ -81,25 +110,17 @@ var createMeeting = async (options) => {
81
110
  var bytesPerSample = (format) => (format.encoding === "pcm_s16le" ? 2 : 1) * format.channels;
82
111
  var createBufferMeetingSource = (options) => {
83
112
  const chunkMs = options.chunkMs ?? 40;
84
- const listeners = {
85
- audio: new Set,
86
- end: new Set,
87
- error: new Set,
88
- participant: new Set
89
- };
90
- const emit = (event, payload) => {
91
- for (const handler of listeners[event])
92
- handler(payload);
93
- };
113
+ const { emit, on } = createEmitter([
114
+ "audio",
115
+ "chat",
116
+ "end",
117
+ "error",
118
+ "participant"
119
+ ]);
94
120
  let stopped = false;
95
121
  return {
96
122
  format: options.format,
97
- on: (event, handler) => {
98
- listeners[event].add(handler);
99
- return () => {
100
- listeners[event].delete(handler);
101
- };
102
- },
123
+ on,
103
124
  start: async () => {
104
125
  for (const participant of options.participants ?? []) {
105
126
  emit("participant", { participant });
package/dist/meeting.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type STTAdapter, type VoiceLanguageStrategy, type VoiceLexiconEntry, type VoicePhraseHint, type VoiceScribeTurn } from "@absolutejs/voice";
2
- import type { MeetingParticipant, MeetingSource, SpeakAudio } from "./source";
2
+ import type { ChatMessage, MeetingCapabilities, MeetingParticipant, MeetingSource, SpeakAudio } from "./source";
3
3
  export type MeetingTurn = VoiceScribeTurn & {
4
4
  /** Resolved participant for this turn, when the source identified speakers. */
5
5
  participant?: MeetingParticipant;
@@ -8,8 +8,15 @@ export type MeetingEventMap = {
8
8
  turn: {
9
9
  turn: MeetingTurn;
10
10
  };
11
+ /** Roster update. `status` mirrors the source: "joined"/absent on appear,
12
+ * "left" when a participant leaves the call. */
11
13
  participant: {
12
14
  participant: MeetingParticipant;
15
+ status?: "joined" | "left";
16
+ };
17
+ /** A text message arrived in the call chat. */
18
+ chat: {
19
+ message: ChatMessage;
13
20
  };
14
21
  end: {
15
22
  reason?: string;
@@ -31,6 +38,19 @@ export type MeetingSession = {
31
38
  * the formats each adapter accepts (Recall: mp3; Discord: pcm).
32
39
  */
33
40
  speak: (audio: SpeakAudio) => Promise<void>;
41
+ /** Cut the bot's in-progress speech (barge-in). No-op when the source can't
42
+ * inject/stop audio or nothing is playing. */
43
+ stopSpeaking: () => Promise<void>;
44
+ /**
45
+ * Post a text message INTO the call chat. Delegates to the source; throws if
46
+ * the underlying adapter doesn't implement `sendChat`. `opts.system` is a hint
47
+ * for platforms that distinguish system messages (ignored elsewhere).
48
+ */
49
+ sendChat: (text: string, opts?: {
50
+ system?: boolean;
51
+ }) => Promise<void>;
52
+ /** What the underlying source can do (speak / chat). */
53
+ readonly capabilities: MeetingCapabilities;
34
54
  getTranscript: () => MeetingTurn[];
35
55
  getParticipants: () => MeetingParticipant[];
36
56
  };
package/dist/source.d.ts CHANGED
@@ -7,6 +7,36 @@ export type MeetingParticipant = {
7
7
  platform?: string;
8
8
  metadata?: Record<string, unknown>;
9
9
  };
10
+ /**
11
+ * A text message in the call's chat. `kind` distinguishes ordinary messages
12
+ * from emoji reactions and platform/system notices; everything but `text` is
13
+ * best-effort and may be absent depending on what the source can report.
14
+ */
15
+ export type ChatMessage = {
16
+ /** The message body (or reaction emoji / system text). */
17
+ text: string;
18
+ /** What sort of chat event this is. Defaults to a normal "message". */
19
+ kind?: "message" | "reaction" | "system";
20
+ /** Resolved sender, when the source identified them. */
21
+ author?: MeetingParticipant;
22
+ /** Raw platform sender id, even when the full participant isn't resolved. */
23
+ authorId?: string;
24
+ /** Epoch ms the message was sent, when the source reports it. */
25
+ timestamp?: number;
26
+ /** Platform channel/thread id the message belongs to, when applicable. */
27
+ channelId?: string;
28
+ };
29
+ /**
30
+ * Which in-call outputs a source supports. Mirrors the optional `speak` /
31
+ * `sendChat` members so a consumer can branch on capabilities up front instead
32
+ * of probing for thrown "not implemented" errors.
33
+ */
34
+ export type MeetingCapabilities = {
35
+ /** The bot can play audio INTO the call (`speak`). */
36
+ canSpeak: boolean;
37
+ /** The bot can post text into the call chat (`sendChat`). */
38
+ canChat: boolean;
39
+ };
10
40
  /**
11
41
  * Audio the bot wants to play INTO the call. Adapters declare which formats
12
42
  * they accept (Recall: mp3; Discord: pcm); a TypeError is the right signal
@@ -29,9 +59,16 @@ export type MeetingSourceEventMap = {
29
59
  chunk: AudioChunk;
30
60
  participant?: string;
31
61
  };
32
- /** Roster update — someone joined / was identified. */
62
+ /** Roster update — someone joined / left / was identified. `status` signals
63
+ * the transition: "joined" (or absent — the historical behavior) when a
64
+ * participant appears/is identified, "left" when they leave the call. */
33
65
  participant: {
34
66
  participant: MeetingParticipant;
67
+ status?: "joined" | "left";
68
+ };
69
+ /** A text message arrived in the call chat. */
70
+ chat: {
71
+ message: ChatMessage;
35
72
  };
36
73
  /** The call ended (everyone left, host stopped, etc.). */
37
74
  end: {
@@ -62,4 +99,24 @@ export type MeetingSource = {
62
99
  * the platform has accepted it, for fire-and-forget transports).
63
100
  */
64
101
  speak?: (audio: SpeakAudio) => Promise<void>;
102
+ /**
103
+ * Stop any in-progress `speak()` output immediately (barge-in / ducking).
104
+ * Optional — sources that can't inject audio (or can't stop it) omit it;
105
+ * `meeting.stopSpeaking()` then no-ops.
106
+ */
107
+ stopSpeaking?: () => Promise<void>;
108
+ /**
109
+ * Post a text message INTO the call chat. Optional — adapters that can't write
110
+ * chat simply don't implement it; `meeting.sendChat()` will throw a clear
111
+ * error in that case. `opts.system` is a hint for platforms that distinguish
112
+ * system/announcement messages from ordinary ones (ignored elsewhere).
113
+ */
114
+ sendChat?: (text: string, opts?: {
115
+ system?: boolean;
116
+ }) => Promise<void>;
117
+ /**
118
+ * What this source can do (speak / chat). Optional — when absent, consumers
119
+ * fall back to probing the presence of `speak` / `sendChat`.
120
+ */
121
+ readonly capabilities?: MeetingCapabilities;
65
122
  };
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@absolutejs/meeting",
3
- "version": "0.0.1-beta.3",
3
+ "version": "0.0.1-beta.5",
4
4
  "description": "Meeting-bot core for AbsoluteJS — join a call (via a source adapter), transcribe it live with the voice scribe, and surface diarized turns + participants for analysis",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/absolutejs/meeting.git"
8
8
  },
9
+ "homepage": "https://github.com/absolutejs/meeting",
10
+ "bugs": {
11
+ "url": "https://github.com/absolutejs/meeting/issues"
12
+ },
9
13
  "files": [
10
14
  "dist",
11
15
  "README.md"