@jambonz/schema 0.3.18 → 0.3.19
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,186 @@
|
|
|
1
|
+
# Conference Monitoring, Coaching & Transcription
|
|
2
|
+
|
|
3
|
+
This guide covers building supervision features on top of jambonz conferences:
|
|
4
|
+
silently monitoring a live room, whisper-coaching the agents in it, barging in
|
|
5
|
+
as a full participant, and tapping the room's audio for transcription or AI
|
|
6
|
+
analysis — all without interrupting or re-dialing any call.
|
|
7
|
+
|
|
8
|
+
A complete, working reference application (React console + Node backend) built
|
|
9
|
+
on these patterns is at **https://github.com/jambonz/room-monitor** — start
|
|
10
|
+
with its `docs/ADAPTING.md`. The `conference-supervision` SDK example
|
|
11
|
+
(`get_sdk_example`) is a distilled version of the same patterns.
|
|
12
|
+
|
|
13
|
+
## The building blocks
|
|
14
|
+
|
|
15
|
+
| Capability | Mechanism |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Classify participants (who is an "agent") | `memberTag` on each conference member — set at join or changed mid-call |
|
|
18
|
+
| Silent monitor | join the conference muted (`joinMuted: true`) |
|
|
19
|
+
| Coach / whisper | the supervisor's audio is delivered **only** to members carrying a given tag (`speakOnlyTo` at join, or the `coach` participant action mid-call) |
|
|
20
|
+
| Barge-in | `uncoach` + unmute — the supervisor is heard by everyone |
|
|
21
|
+
| Room audio out (transcription / AI / recording) | a **conference listen fork**: jambonz streams the room's mixed audio (L16 PCM) to a WebSocket you host |
|
|
22
|
+
| Discover live conferences | `GET /Accounts/{sid}/Conferences` (add `?expand=participants` for members + tags on systems that support it) |
|
|
23
|
+
|
|
24
|
+
Mode changes are mid-call commands on an existing leg — a supervisor moves
|
|
25
|
+
between monitor → coach → barge instantly, with no re-INVITE and no
|
|
26
|
+
interruption to the room.
|
|
27
|
+
|
|
28
|
+
## Tagging members
|
|
29
|
+
|
|
30
|
+
Tags drive everything: coaching targets, UI classification, and per-tag audio
|
|
31
|
+
flows. Set a tag when a member joins:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
session.conference({
|
|
35
|
+
name: 'support-room-42',
|
|
36
|
+
memberTag: 'agent',
|
|
37
|
+
}).send();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Tags are **fully dynamic** — add or remove them on a participant who is already
|
|
41
|
+
in the conference (no re-join). From the application controlling that leg:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// promote a live participant (e.g. warm transfer completes, human takes over from AI)
|
|
45
|
+
session.injectCommand('conf:participant-action', { action: 'tag', tag: 'agent' });
|
|
46
|
+
|
|
47
|
+
// demote — clears the member's tag
|
|
48
|
+
session.injectCommand('conf:participant-action', { action: 'untag' });
|
|
49
|
+
|
|
50
|
+
// third argument targets another leg this session controls (e.g. a dialed B-leg)
|
|
51
|
+
session.injectCommand('conf:participant-action', { action: 'tag', tag: 'agent' }, bLegCallSid);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or from anywhere via the REST client:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { JambonzClient } from '@jambonz/sdk/client';
|
|
58
|
+
|
|
59
|
+
const client = new JambonzClient({ baseUrl, accountSid, apiKey });
|
|
60
|
+
await client.calls.update(callSid, {
|
|
61
|
+
conferenceParticipantAction: { action: 'tag', tag: 'agent' },
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The full mid-call action set is `tag | untag | coach | uncoach | mute | unmute
|
|
66
|
+
| hold | unhold`. Tag changes take effect live: an active coach automatically
|
|
67
|
+
starts/stops reaching the member as its tag changes.
|
|
68
|
+
|
|
69
|
+
## The supervisor leg
|
|
70
|
+
|
|
71
|
+
The supervisor is just another conference member whose join options and
|
|
72
|
+
participant actions produce the three modes. Join for silent monitoring:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
session
|
|
76
|
+
.answer()
|
|
77
|
+
.conference({
|
|
78
|
+
name: roomName,
|
|
79
|
+
joinMuted: true, // hears everything, heard by no one
|
|
80
|
+
memberTag: 'supervisor', // lets other tooling filter this leg out
|
|
81
|
+
startConferenceOnEnter: false, // never create/destroy the room being watched
|
|
82
|
+
endConferenceOnExit: false,
|
|
83
|
+
actionHook: '/conf-done',
|
|
84
|
+
})
|
|
85
|
+
.send();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
To join directly in coach mode instead, add `speakOnlyTo: 'agent'` and omit
|
|
89
|
+
`joinMuted`. Then switch modes mid-call (from any service, via REST):
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// coach: audio delivered only to members tagged 'agent'
|
|
93
|
+
await client.calls.update(supervisorCallSid, {
|
|
94
|
+
conferenceParticipantAction: { action: 'coach', tag: 'agent' },
|
|
95
|
+
});
|
|
96
|
+
await client.calls.update(supervisorCallSid, { conf_mute_status: 'unmute' });
|
|
97
|
+
|
|
98
|
+
// barge-in: heard by everyone
|
|
99
|
+
await client.calls.update(supervisorCallSid, {
|
|
100
|
+
conferenceParticipantAction: { action: 'uncoach' },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// back to silent monitoring
|
|
104
|
+
await client.calls.update(supervisorCallSid, { conf_mute_status: 'mute' });
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Coach semantics to rely on:
|
|
108
|
+
|
|
109
|
+
- Coached audio reaches **only** members carrying the target tag — including
|
|
110
|
+
members (and audio forks) that join *after* coaching started.
|
|
111
|
+
- Audio forks are never tagged, so a transcription or recording tap **cannot
|
|
112
|
+
hear private coaching**; it hears the coach again after `uncoach`.
|
|
113
|
+
- If the last tagged member leaves, fall back to monitoring in your
|
|
114
|
+
application logic (the reference app polls participants and downgrades
|
|
115
|
+
coach → monitor automatically).
|
|
116
|
+
|
|
117
|
+
## Tapping the room's audio (transcription, AI, recording)
|
|
118
|
+
|
|
119
|
+
A **conference listen fork** streams the room's mixed audio to a WebSocket you
|
|
120
|
+
operate. jambonz only transports audio — what you do with it (STT, sentiment,
|
|
121
|
+
archiving) is entirely your side of the socket.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# start — jambonz dials out to your wsUrl and streams L16 PCM
|
|
125
|
+
curl -X POST "$BASE_URL/v1/Accounts/$ACCOUNT_SID/Conferences/support-room-42/listen" \
|
|
126
|
+
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
|
|
127
|
+
-d '{"url": "wss://your-host/fork", "sampleRate": 16000,
|
|
128
|
+
"metadata": {"room": "support-room-42", "sampleRate": 16000}}'
|
|
129
|
+
|
|
130
|
+
# stop
|
|
131
|
+
curl -X DELETE "$BASE_URL/v1/Accounts/$ACCOUNT_SID/Conferences/support-room-42/listen" \
|
|
132
|
+
-H "Authorization: Bearer $API_KEY"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Consume it in the same `@jambonz/sdk` app that runs your call logic:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const makeService = createEndpoint({ server, port: 3000 });
|
|
139
|
+
const audio = makeService.audio({ path: '/fork' });
|
|
140
|
+
|
|
141
|
+
audio.on('connection', (stream) => {
|
|
142
|
+
// your `metadata` arrives verbatim as the fork's first text frame and is
|
|
143
|
+
// exposed as stream.metadata — make it self-describing (include sampleRate)
|
|
144
|
+
console.log('fork connected', stream.metadata);
|
|
145
|
+
stream.on('audio', (pcm: Buffer) => {
|
|
146
|
+
// feed your STT / AI / recorder here
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Lifecycle guarantees: the fork is a media-server-owned bot member — it is
|
|
152
|
+
**excluded from participant counts, never keeps a room alive, and is torn down
|
|
153
|
+
automatically when the conference ends**. Starting a fork requires no
|
|
154
|
+
participant leg (you can transcribe a room nobody is monitoring), and repeated
|
|
155
|
+
starts for the same conference are idempotent.
|
|
156
|
+
|
|
157
|
+
> Conference-level listen (`/Conferences/{name}/listen`) and the
|
|
158
|
+
> `?expand=participants` listing require a jambonz release with MediaJam-based
|
|
159
|
+
> conferencing that includes these endpoints.
|
|
160
|
+
|
|
161
|
+
## Discovering live rooms
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// names only (all releases)
|
|
165
|
+
const rooms = await client.conferences.list();
|
|
166
|
+
|
|
167
|
+
// with participants + tags (releases with the enriched listing)
|
|
168
|
+
const res = await fetch(
|
|
169
|
+
`${baseUrl}/v1/Accounts/${accountSid}/Conferences?expand=participants`,
|
|
170
|
+
{ headers: { Authorization: `Bearer ${apiKey}` } });
|
|
171
|
+
// → [{ id, name, durationSec, participants: [{ call_sid, label, memberTag, isAgent }] }]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Filter out your own supervisor legs (`memberTag === 'supervisor'`) before
|
|
175
|
+
displaying counts, and derive "can I coach?" from agent presence.
|
|
176
|
+
|
|
177
|
+
## Putting it together
|
|
178
|
+
|
|
179
|
+
A typical supervision backend is three small pieces: a poller on the
|
|
180
|
+
conferences listing (rooms → your UI), a jambonz application that joins
|
|
181
|
+
supervisor legs and applies participant actions, and an audio-fork consumer
|
|
182
|
+
feeding your STT. The reference app implements exactly this — and ships a
|
|
183
|
+
closed-loop end-to-end test (`tools/e2e/`) that verifies the audio-visibility
|
|
184
|
+
contracts, including coach privacy, using scripted fake-microphone browsers.
|
|
185
|
+
If you adapt the patterns, adapt the test too: it will keep verifying who can
|
|
186
|
+
hear whom as your code evolves.
|