@absolutejs/meeting 0.0.1-beta.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 ADDED
@@ -0,0 +1,55 @@
1
+ # @absolutejs/meeting
2
+
3
+ Meeting-bot core for AbsoluteJS. Join a call through a **source adapter**,
4
+ transcribe it live with the [`@absolutejs/voice`](https://github.com/absolutejs/voice)
5
+ scribe, and surface **diarized turns + participants** for analysis (deal
6
+ coaching, summaries, action items, …).
7
+
8
+ Platform-agnostic: the core only depends on the small `MeetingSource` contract,
9
+ so each platform is a separate adapter under
10
+ [`meeting-adapters`](https://github.com/absolutejs/meeting-adapters) (mirrors
11
+ `voice-adapters`):
12
+
13
+ - `@absolutejs/meeting-recall` — Recall.ai (Meet / Zoom / Teams)
14
+ - `@absolutejs/meeting-discord` — native `@discordjs/voice` receive
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import { createMeeting } from "@absolutejs/meeting";
20
+ import { deepgram } from "@absolutejs/voice-deepgram";
21
+ import { recall } from "@absolutejs/meeting-recall";
22
+
23
+ const meeting = await createMeeting({
24
+ source: recall({ apiKey: process.env.RECALL_API_KEY!, meetingUrl }),
25
+ stt: deepgram({ apiKey: process.env.DEEPGRAM_API_KEY!, diarize: true }),
26
+ sessionId: "deal-123",
27
+ });
28
+
29
+ meeting.on("turn", ({ turn }) => {
30
+ // { speaker, text, participant? } — stream to your analyzer / UI
31
+ });
32
+ meeting.on("end", ({ transcript }) => {
33
+ // full diarized transcript — run your deal-call analysis
34
+ });
35
+
36
+ await meeting.start(); // bot joins the call
37
+ // ... later: await meeting.stop()
38
+ ```
39
+
40
+ ### Testing without a platform
41
+
42
+ `createBufferMeetingSource` streams an in-memory PCM buffer in real time — the
43
+ reference `MeetingSource` implementation and a test harness.
44
+
45
+ ## API
46
+
47
+ - `createMeeting(options)` → `MeetingSession` (`on`, `start`, `stop`,
48
+ `getTranscript`, `getParticipants`).
49
+ - `createBufferMeetingSource(options)` → `MeetingSource`.
50
+ - Types: `MeetingSource`, `MeetingSourceEventMap`, `MeetingParticipant`,
51
+ `MeetingSession`, `MeetingTurn`, `CreateMeetingOptions`.
52
+
53
+ ## License
54
+
55
+ CC BY-NC 4.0
@@ -0,0 +1,17 @@
1
+ import type { AudioFormat } from "@absolutejs/voice";
2
+ import type { MeetingParticipant, MeetingSource } from "./source";
3
+ export type BufferMeetingSourceOptions = {
4
+ format: AudioFormat;
5
+ /** Raw audio matching `format` (e.g. pcm_s16le bytes). */
6
+ pcm: Uint8Array;
7
+ /** Chunk cadence in ms (how the buffer is paced out). Default 40. */
8
+ chunkMs?: number;
9
+ /** Optional roster to announce on start. */
10
+ participants?: MeetingParticipant[];
11
+ };
12
+ /**
13
+ * A meeting source backed by an in-memory PCM buffer, paced out in real time.
14
+ * Used to test the meeting core + voice scribe without a live platform, and as
15
+ * the reference implementation of `MeetingSource` for adapter authors.
16
+ */
17
+ export declare const createBufferMeetingSource: (options: BufferMeetingSourceOptions) => MeetingSource;
@@ -0,0 +1,5 @@
1
+ export { createMeeting } from "./meeting";
2
+ export type { MeetingSession, MeetingTurn, MeetingEventMap, CreateMeetingOptions, } from "./meeting";
3
+ export { createBufferMeetingSource } from "./bufferSource";
4
+ export type { BufferMeetingSourceOptions } from "./bufferSource";
5
+ export type { MeetingSource, MeetingSourceEventMap, MeetingParticipant, } from "./source";
package/dist/index.js ADDED
@@ -0,0 +1,120 @@
1
+ // @bun
2
+ // src/meeting.ts
3
+ import {
4
+ createVoiceScribe
5
+ } from "@absolutejs/voice";
6
+ var createMeeting = async (options) => {
7
+ const scribe = await createVoiceScribe({
8
+ format: options.source.format,
9
+ languageStrategy: options.languageStrategy,
10
+ lexicon: options.lexicon,
11
+ phraseHints: options.phraseHints,
12
+ sessionId: options.sessionId,
13
+ stt: options.stt
14
+ });
15
+ const participants = new Map;
16
+ 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
+ };
27
+ const resolveParticipant = (turn) => participants.get(turn.speaker) ?? (lastSpeaker ? participants.get(lastSpeaker) : undefined);
28
+ scribe.on("turn", ({ turn }) => {
29
+ emit("turn", { turn: { ...turn, participant: resolveParticipant(turn) } });
30
+ });
31
+ scribe.on("error", (event) => emit("error", { error: event.error }));
32
+ let ended = false;
33
+ const unsubscribe = [];
34
+ const finalize = async (reason) => {
35
+ if (ended)
36
+ return;
37
+ ended = true;
38
+ for (const off of unsubscribe)
39
+ off();
40
+ await scribe.close(reason);
41
+ const transcript = scribe.getTranscript().map((turn) => ({
42
+ ...turn,
43
+ participant: resolveParticipant(turn)
44
+ }));
45
+ emit("end", { reason, transcript });
46
+ };
47
+ unsubscribe.push(options.source.on("audio", ({ chunk, participant }) => {
48
+ if (participant)
49
+ lastSpeaker = participant;
50
+ 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 })));
55
+ return {
56
+ getParticipants: () => [...participants.values()],
57
+ getTranscript: () => scribe.getTranscript().map((turn) => ({
58
+ ...turn,
59
+ participant: resolveParticipant(turn)
60
+ })),
61
+ on: (event, handler) => {
62
+ listeners[event].add(handler);
63
+ return () => {
64
+ listeners[event].delete(handler);
65
+ };
66
+ },
67
+ start: () => options.source.start(),
68
+ stop: async (reason) => {
69
+ await options.source.stop(reason);
70
+ await finalize(reason);
71
+ }
72
+ };
73
+ };
74
+ // src/bufferSource.ts
75
+ var bytesPerSample = (format) => (format.encoding === "pcm_s16le" ? 2 : 1) * format.channels;
76
+ var createBufferMeetingSource = (options) => {
77
+ const chunkMs = options.chunkMs ?? 40;
78
+ const listeners = {
79
+ audio: new Set,
80
+ end: new Set,
81
+ error: new Set,
82
+ participant: new Set
83
+ };
84
+ const emit = (event, payload) => {
85
+ for (const handler of listeners[event])
86
+ handler(payload);
87
+ };
88
+ let stopped = false;
89
+ return {
90
+ format: options.format,
91
+ on: (event, handler) => {
92
+ listeners[event].add(handler);
93
+ return () => {
94
+ listeners[event].delete(handler);
95
+ };
96
+ },
97
+ start: async () => {
98
+ for (const participant of options.participants ?? []) {
99
+ emit("participant", { participant });
100
+ }
101
+ const chunkBytes = Math.max(2, Math.round(bytesPerSample(options.format) * options.format.sampleRateHz * chunkMs / 1000));
102
+ (async () => {
103
+ for (let off = 0;off < options.pcm.length && !stopped; off += chunkBytes) {
104
+ emit("audio", { chunk: options.pcm.subarray(off, off + chunkBytes) });
105
+ await new Promise((resolve) => setTimeout(resolve, chunkMs));
106
+ }
107
+ if (!stopped)
108
+ emit("end", { reason: "buffer-complete" });
109
+ })();
110
+ },
111
+ stop: async (reason) => {
112
+ stopped = true;
113
+ emit("end", { reason: reason ?? "stopped" });
114
+ }
115
+ };
116
+ };
117
+ export {
118
+ createMeeting,
119
+ createBufferMeetingSource
120
+ };
@@ -0,0 +1,45 @@
1
+ import { type STTAdapter, type VoiceLanguageStrategy, type VoiceLexiconEntry, type VoicePhraseHint, type VoiceScribeTurn } from "@absolutejs/voice";
2
+ import type { MeetingParticipant, MeetingSource } from "./source";
3
+ export type MeetingTurn = VoiceScribeTurn & {
4
+ /** Resolved participant for this turn, when the source identified speakers. */
5
+ participant?: MeetingParticipant;
6
+ };
7
+ export type MeetingEventMap = {
8
+ turn: {
9
+ turn: MeetingTurn;
10
+ };
11
+ participant: {
12
+ participant: MeetingParticipant;
13
+ };
14
+ end: {
15
+ reason?: string;
16
+ transcript: MeetingTurn[];
17
+ };
18
+ error: {
19
+ error: Error;
20
+ };
21
+ };
22
+ export type MeetingSession = {
23
+ on: <K extends keyof MeetingEventMap>(event: K, handler: (payload: MeetingEventMap[K]) => void | Promise<void>) => () => void;
24
+ /** Join the call and start transcribing. */
25
+ start: () => Promise<void>;
26
+ /** Leave the call, finalize, and emit `end` with the full transcript. */
27
+ stop: (reason?: string) => Promise<void>;
28
+ getTranscript: () => MeetingTurn[];
29
+ getParticipants: () => MeetingParticipant[];
30
+ };
31
+ export type CreateMeetingOptions = {
32
+ source: MeetingSource;
33
+ stt: STTAdapter;
34
+ sessionId: string;
35
+ languageStrategy?: VoiceLanguageStrategy;
36
+ lexicon?: VoiceLexiconEntry[];
37
+ phraseHints?: VoicePhraseHint[];
38
+ };
39
+ /**
40
+ * Wire a meeting source (any platform) to the voice scribe: audio flows
41
+ * source → scribe → diarized turns; the participant roster from the source is
42
+ * tracked alongside. Platform-agnostic — the Recall / Discord / onSpark
43
+ * adapters all satisfy `MeetingSource`, so this core never changes per platform.
44
+ */
45
+ export declare const createMeeting: (options: CreateMeetingOptions) => Promise<MeetingSession>;
@@ -0,0 +1,44 @@
1
+ import type { AudioChunk, AudioFormat } from "@absolutejs/voice";
2
+ /** A participant in the call, as reported by the source platform. */
3
+ export type MeetingParticipant = {
4
+ id: string;
5
+ name?: string;
6
+ /** Platform of origin, e.g. "recall", "discord", "onspark". */
7
+ platform?: string;
8
+ metadata?: Record<string, unknown>;
9
+ };
10
+ export type MeetingSourceEventMap = {
11
+ /** A chunk of call audio (in `format`). `participant` is set when the source
12
+ * knows who is speaking for this chunk (per-user streams); otherwise omit it
13
+ * and rely on the scribe's diarization. */
14
+ audio: {
15
+ chunk: AudioChunk;
16
+ participant?: string;
17
+ };
18
+ /** Roster update — someone joined / was identified. */
19
+ participant: {
20
+ participant: MeetingParticipant;
21
+ };
22
+ /** The call ended (everyone left, host stopped, etc.). */
23
+ end: {
24
+ reason?: string;
25
+ };
26
+ error: {
27
+ error: Error;
28
+ };
29
+ };
30
+ /**
31
+ * A meeting "source" — the thing that gets audio out of a call. Implemented by
32
+ * the platform adapters (`@absolutejs/meeting-recall`, `@absolutejs/meeting-
33
+ * discord`, an onSpark browser source, …). The core only depends on this
34
+ * contract, so a new platform is just a new adapter.
35
+ */
36
+ export type MeetingSource = {
37
+ /** Audio format this source emits — fed straight into the scribe's STT. */
38
+ readonly format: AudioFormat;
39
+ on: <K extends keyof MeetingSourceEventMap>(event: K, handler: (payload: MeetingSourceEventMap[K]) => void | Promise<void>) => () => void;
40
+ /** Join the call / begin streaming audio. */
41
+ start: () => Promise<void>;
42
+ /** Leave the call / stop streaming. */
43
+ stop: (reason?: string) => Promise<void>;
44
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@absolutejs/meeting",
3
+ "version": "0.0.1-beta.0",
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
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/absolutejs/meeting.git"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "import": "./dist/index.js",
18
+ "types": "./dist/index.d.ts"
19
+ }
20
+ },
21
+ "license": "CC BY-NC 4.0",
22
+ "author": "Alex Kahn",
23
+ "scripts": {
24
+ "build": "rm -rf dist && bun build ./src/index.ts --outdir dist --target bun --external @absolutejs/voice && tsc --emitDeclarationOnly --project tsconfig.json",
25
+ "format": "prettier --write \"./**/*.{js,ts,json,md}\"",
26
+ "release": "bun run format && bun run build && bun publish",
27
+ "test": "bun test",
28
+ "typecheck": "bun run tsc --noEmit"
29
+ },
30
+ "dependencies": {
31
+ "@absolutejs/voice": "0.0.22-beta.550"
32
+ },
33
+ "devDependencies": {
34
+ "@types/bun": "1.3.9",
35
+ "typescript": "^5.9.3"
36
+ }
37
+ }