@voicethere/agent 0.7.11 → 0.7.13

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.
@@ -152,6 +152,12 @@ function isPlayStopAckMessage(value) {
152
152
  const msg = value;
153
153
  return msg.type === "play_stop_ack" && typeof msg.requestId === "string";
154
154
  }
155
+ function isPlayPoseAckMessage(value) {
156
+ if (!value || typeof value !== "object")
157
+ return false;
158
+ const msg = value;
159
+ return msg.type === "play_pose_ack" && typeof msg.requestId === "string";
160
+ }
155
161
  function isRecordingControlAckMessage(value) {
156
162
  if (!value || typeof value !== "object")
157
163
  return false;
@@ -212,7 +218,7 @@ function isParentMessage(value) {
212
218
  if (!value || typeof value !== "object")
213
219
  return false;
214
220
  const msg = value;
215
- return msg.type === "session_start" || msg.type === "speech_event" || msg.type === "session_end" || msg.type === "data_channel_message" || msg.type === "data_channel_binary" || msg.type === "idle_timeout" || msg.type === "recording_control_ack" || msg.type === "play_ack" || msg.type === "play_status_ack" || msg.type === "play_stop_ack" || msg.type === "mix_control_ack" || msg.type === "stt_control_ack" || msg.type === "webhook";
221
+ return msg.type === "session_start" || msg.type === "speech_event" || msg.type === "session_end" || msg.type === "data_channel_message" || msg.type === "data_channel_binary" || msg.type === "idle_timeout" || msg.type === "recording_control_ack" || msg.type === "play_ack" || msg.type === "play_status_ack" || msg.type === "play_stop_ack" || msg.type === "play_pose_ack" || msg.type === "mix_control_ack" || msg.type === "stt_control_ack" || msg.type === "webhook";
216
222
  }
217
223
  function isSessionScopedParentMessage(value) {
218
224
  return isParentMessage(value) && !isWebhookMessage(value);
@@ -235,7 +241,9 @@ var pendingRecordingAcks = /* @__PURE__ */ new Map();
235
241
  var pendingPlayAcks = /* @__PURE__ */ new Map();
236
242
  var pendingPlayStatusAcks = /* @__PURE__ */ new Map();
237
243
  var pendingPlayStopAcks = /* @__PURE__ */ new Map();
244
+ var pendingPlayPoseAcks = /* @__PURE__ */ new Map();
238
245
  var MIX_CONTROL_ACK_TIMEOUT_MS = 5e3;
246
+ var STT_CONTROL_ACK_TIMEOUT_MS = 5e3;
239
247
  var pendingMixAcks = /* @__PURE__ */ new Map();
240
248
  var pendingSttAcks = /* @__PURE__ */ new Map();
241
249
  function handleRecordingControlAck(message) {
@@ -290,6 +298,19 @@ function handlePlayStopAck(message) {
290
298
  requestId: message.requestId
291
299
  });
292
300
  }
301
+ function handlePlayPoseAck(message) {
302
+ const pending = pendingPlayPoseAcks.get(message.requestId);
303
+ if (!pending)
304
+ return;
305
+ clearTimeout(pending.timer);
306
+ pendingPlayPoseAcks.delete(message.requestId);
307
+ pending.resolve({
308
+ ok: message.ok,
309
+ playId: message.playId,
310
+ reason: message.reason,
311
+ requestId: message.requestId
312
+ });
313
+ }
293
314
  function handleMixControlAck(message) {
294
315
  const pending = pendingMixAcks.get(message.requestId);
295
316
  if (!pending)
@@ -402,7 +423,7 @@ function sendParentMessage(message) {
402
423
  process.send?.(message);
403
424
  return;
404
425
  }
405
- if (msgType === "play" || msgType === "play_status" || msgType === "play_stop") {
426
+ if (msgType === "play" || msgType === "play_status" || msgType === "play_stop" || msgType === "play_pose") {
406
427
  process.send?.(message);
407
428
  return;
408
429
  }
@@ -588,6 +609,10 @@ function defineAgent(handlers) {
588
609
  handlePlayStopAck(message);
589
610
  return;
590
611
  }
612
+ if (isPlayPoseAckMessage(message)) {
613
+ handlePlayPoseAck(message);
614
+ return;
615
+ }
591
616
  if (isMixControlAckMessage(message)) {
592
617
  handleMixControlAck(message);
593
618
  return;
@@ -778,6 +803,26 @@ async function sendTtsPoseControl(action, payload) {
778
803
  });
779
804
  });
780
805
  }
806
+ async function sendSttControl(options) {
807
+ const requestId = randomUUID();
808
+ if (!isVoicethereAgentChild()) {
809
+ return { ok: true, reason: "local_mock", requestId };
810
+ }
811
+ return new Promise((resolve) => {
812
+ const timer = setTimeout(() => {
813
+ pendingSttAcks.delete(requestId);
814
+ resolve({ ok: false, reason: "timeout", requestId });
815
+ }, STT_CONTROL_ACK_TIMEOUT_MS);
816
+ pendingSttAcks.set(requestId, { resolve, timer });
817
+ sendParentMessage({
818
+ type: "stt_control",
819
+ requestId,
820
+ enabled: options.enabled,
821
+ ...options.clientId ? { clientId: options.clientId } : {},
822
+ ...options.sessionId ? { sessionId: options.sessionId } : {}
823
+ });
824
+ });
825
+ }
781
826
  function createMixGroup(options) {
782
827
  return sendMixControl("create_group", {
783
828
  groupId: options.id,
@@ -809,6 +854,9 @@ function setListenerMute(options) {
809
854
  muted: options.muted
810
855
  });
811
856
  }
857
+ function setSttEnabled(options) {
858
+ return sendSttControl(options);
859
+ }
812
860
  function validatePlayOptions(options) {
813
861
  const requestId = randomUUID();
814
862
  const url = options.url?.trim();
@@ -928,6 +976,36 @@ function getPlay(playId) {
928
976
  function stopPlay(playId) {
929
977
  return sendPlayStop(playId);
930
978
  }
979
+ async function sendPlayPose(playId, pose) {
980
+ const requestId = randomUUID();
981
+ if (!playId?.trim() || !pose) {
982
+ return {
983
+ ok: false,
984
+ playId: playId ?? "",
985
+ reason: "invalid_payload",
986
+ requestId
987
+ };
988
+ }
989
+ if (!isVoicethereAgentChild()) {
990
+ return { ok: true, playId, reason: "local_mock", requestId };
991
+ }
992
+ return new Promise((resolve) => {
993
+ const timer = setTimeout(() => {
994
+ pendingPlayPoseAcks.delete(requestId);
995
+ resolve({ ok: false, playId, reason: "timeout", requestId });
996
+ }, PLAY_CONTROL_ACK_TIMEOUT_MS);
997
+ pendingPlayPoseAcks.set(requestId, { playId, resolve, timer });
998
+ sendParentMessage({
999
+ type: "play_pose",
1000
+ requestId,
1001
+ playId,
1002
+ pose
1003
+ });
1004
+ });
1005
+ }
1006
+ function setPlayPose(playId, pose) {
1007
+ return sendPlayPose(playId, pose);
1008
+ }
931
1009
  function sendToClient(sessionId, payload) {
932
1010
  sendParentMessage({ type: "send_to_client", sessionId, payload });
933
1011
  }
@@ -1059,6 +1137,8 @@ function resolveClipUrl(assetOrigin, clipId) {
1059
1137
  var SHOWCASE_DEMOS = /* @__PURE__ */ new Set(["orbit", "soundboard", "proximity"]);
1060
1138
  var MAX_SAY_TEXT_LENGTH = 200;
1061
1139
  var MAX_XZ = 5;
1140
+ var ORBIT_SINE_MIN_FREQUENCY_HZ = 110;
1141
+ var ORBIT_SINE_MAX_FREQUENCY_HZ = 1760;
1062
1142
  function isFiniteNumber(value) {
1063
1143
  return typeof value === "number" && Number.isFinite(value);
1064
1144
  }
@@ -1068,6 +1148,9 @@ function clampVolume(value) {
1068
1148
  function isInXzRange(value) {
1069
1149
  return value >= -MAX_XZ && value <= MAX_XZ;
1070
1150
  }
1151
+ function isOrbitSineFrequencyHz(value) {
1152
+ return value >= ORBIT_SINE_MIN_FREQUENCY_HZ && value <= ORBIT_SINE_MAX_FREQUENCY_HZ;
1153
+ }
1071
1154
  function isMixPlacement(value) {
1072
1155
  return value === "center" || value === "left" || value === "right" || value === "front" || value === "behind" || value === "below" || value === "above";
1073
1156
  }
@@ -1116,15 +1199,33 @@ function parseShowcaseMessage(message) {
1116
1199
  if (set.paused !== void 0 && typeof set.paused !== "boolean") {
1117
1200
  return null;
1118
1201
  }
1202
+ if (set.frequencyHz !== void 0 && (!isFiniteNumber(set.frequencyHz) || !isOrbitSineFrequencyHz(set.frequencyHz))) {
1203
+ return null;
1204
+ }
1205
+ if (set.sinePlaying !== void 0 && typeof set.sinePlaying !== "boolean") {
1206
+ return null;
1207
+ }
1119
1208
  return {
1120
1209
  type: "orbit",
1121
1210
  action: "set",
1122
1211
  ...set.radius !== void 0 ? { radius: set.radius } : {},
1123
1212
  ...set.periodSec !== void 0 ? { periodSec: set.periodSec } : {},
1124
1213
  ...set.elevation !== void 0 ? { elevation: set.elevation } : {},
1125
- ...set.paused !== void 0 ? { paused: set.paused } : {}
1214
+ ...set.paused !== void 0 ? { paused: set.paused } : {},
1215
+ ...set.frequencyHz !== void 0 ? { frequencyHz: set.frequencyHz } : {},
1216
+ ...set.sinePlaying !== void 0 ? { sinePlaying: set.sinePlaying } : {}
1126
1217
  };
1127
1218
  }
1219
+ if (orbit.action === "place") {
1220
+ const place = message;
1221
+ if (!isFiniteNumber(place.x) || !isInXzRange(place.x)) {
1222
+ return null;
1223
+ }
1224
+ if (!isFiniteNumber(place.z) || !isInXzRange(place.z)) {
1225
+ return null;
1226
+ }
1227
+ return { type: "orbit", action: "place", x: place.x, z: place.z };
1228
+ }
1128
1229
  if (orbit.action === "say") {
1129
1230
  const say = message;
1130
1231
  if (typeof say.text !== "string" || say.text.length === 0 || say.text.length > MAX_SAY_TEXT_LENGTH) {
@@ -1176,6 +1277,24 @@ function parseShowcaseMessage(message) {
1176
1277
  }
1177
1278
  return { type: "pad_status", playId: status.playId.trim() };
1178
1279
  }
1280
+ case "pad_move": {
1281
+ const padMove = message;
1282
+ if (!isNonEmptyString(padMove.playId)) {
1283
+ return null;
1284
+ }
1285
+ if (!isFiniteNumber(padMove.x) || !isInXzRange(padMove.x)) {
1286
+ return null;
1287
+ }
1288
+ if (!isFiniteNumber(padMove.z) || !isInXzRange(padMove.z)) {
1289
+ return null;
1290
+ }
1291
+ return {
1292
+ type: "pad_move",
1293
+ playId: padMove.playId.trim(),
1294
+ x: padMove.x,
1295
+ z: padMove.z
1296
+ };
1297
+ }
1179
1298
  case "move": {
1180
1299
  const move = message;
1181
1300
  if (!isFiniteNumber(move.x) || !isInXzRange(move.x)) {
@@ -1286,12 +1405,60 @@ var ProximityRoom = class {
1286
1405
  }
1287
1406
  };
1288
1407
 
1408
+ // templates/spatial-showcase/sine.ts
1409
+ var ORBIT_SINE_DURATION_MS = 2e3;
1410
+ var ORBIT_SINE_FREQUENCY_HZ = 440;
1411
+ var ORBIT_SINE_AMPLITUDE = 8e3;
1412
+ var ORBIT_SINE_SAMPLE_RATE = 16e3;
1413
+ var ORBIT_SINE_INLINE_URL = "https://example.com/inline-orbit-sine.wav";
1414
+ function orbitSineSampleCount(durationMs = ORBIT_SINE_DURATION_MS, frequencyHz = ORBIT_SINE_FREQUENCY_HZ, sampleRate = ORBIT_SINE_SAMPLE_RATE) {
1415
+ const targetCycles = durationMs / 1e3 * frequencyHz;
1416
+ const cycles = Math.max(1, Math.floor(targetCycles));
1417
+ return Math.round(cycles * sampleRate / frequencyHz);
1418
+ }
1419
+ function orbitSineClipDurationMs(durationMs = ORBIT_SINE_DURATION_MS, frequencyHz = ORBIT_SINE_FREQUENCY_HZ, sampleRate = ORBIT_SINE_SAMPLE_RATE) {
1420
+ const numSamples = orbitSineSampleCount(durationMs, frequencyHz, sampleRate);
1421
+ return numSamples / sampleRate * 1e3;
1422
+ }
1423
+ function buildOrbitSineInlineClipBase64(durationMs = ORBIT_SINE_DURATION_MS, frequencyHz = ORBIT_SINE_FREQUENCY_HZ, amplitude = ORBIT_SINE_AMPLITUDE, phaseRad = 0) {
1424
+ const sampleRate = ORBIT_SINE_SAMPLE_RATE;
1425
+ const channels = 1;
1426
+ const bytesPerSample = 2;
1427
+ const numSamples = orbitSineSampleCount(durationMs, frequencyHz, sampleRate);
1428
+ const dataSize = numSamples * channels * bytesPerSample;
1429
+ const buffer = Buffer.alloc(44 + dataSize);
1430
+ buffer.write("RIFF", 0);
1431
+ buffer.writeUInt32LE(36 + dataSize, 4);
1432
+ buffer.write("WAVE", 8);
1433
+ buffer.write("fmt ", 12);
1434
+ buffer.writeUInt32LE(16, 16);
1435
+ buffer.writeUInt16LE(1, 20);
1436
+ buffer.writeUInt16LE(channels, 22);
1437
+ buffer.writeUInt32LE(sampleRate, 24);
1438
+ buffer.writeUInt32LE(sampleRate * channels * bytesPerSample, 28);
1439
+ buffer.writeUInt16LE(channels * bytesPerSample, 32);
1440
+ buffer.writeUInt16LE(16, 34);
1441
+ buffer.write("data", 36);
1442
+ buffer.writeUInt32LE(dataSize, 40);
1443
+ for (let i = 0; i < numSamples; i++) {
1444
+ const t = i / sampleRate;
1445
+ const sample = Math.round(
1446
+ amplitude * Math.sin(2 * Math.PI * frequencyHz * t + phaseRad)
1447
+ );
1448
+ buffer.writeInt16LE(sample, 44 + i * 2);
1449
+ }
1450
+ return buffer.toString("base64");
1451
+ }
1452
+
1289
1453
  // templates/spatial-showcase/agent.ts
1290
1454
  var ORBIT_TICK_MS = 50;
1291
1455
  var ORBIT_POSE_EMIT_MS = 100;
1292
1456
  var ROOM_STATE_TICK_MS = 100;
1293
1457
  var LOOP_PAD_POLL_MS = 250;
1294
- var MAX_ACTIVE_PLAYS = 4;
1458
+ var ORBIT_SINE_POLL_MS = 250;
1459
+ var ORBIT_SINE_GAPLESS_LEAD_MS = 200;
1460
+ var ORBIT_SINE_VOLUME = 0.25;
1461
+ var MAX_ACTIVE_PLAYS = 12;
1295
1462
  var SHOWCASE_ROOM_GROUP_ID = "showcase-room";
1296
1463
  var sessions = /* @__PURE__ */ new Map();
1297
1464
  var proximityRoom = new ProximityRoom();
@@ -1314,12 +1481,13 @@ function ack(sessionId, action, extra) {
1314
1481
  ...extra
1315
1482
  });
1316
1483
  }
1317
- function ackError(sessionId, action, error) {
1484
+ function ackError(sessionId, action, error, extra) {
1318
1485
  sendToClient(sessionId, {
1319
1486
  type: "showcase_ack",
1320
1487
  action,
1321
1488
  ok: false,
1322
- error
1489
+ error,
1490
+ ...extra?.clipId !== void 0 ? { clipId: extra.clipId } : {}
1323
1491
  });
1324
1492
  }
1325
1493
  async function safeMixCall(sessionId, label, fn) {
@@ -1351,6 +1519,9 @@ function defaultSessionState(demo, assetOrigin) {
1351
1519
  playIds: /* @__PURE__ */ new Set(),
1352
1520
  loopPads: /* @__PURE__ */ new Map(),
1353
1521
  loopPollTimers: /* @__PURE__ */ new Map(),
1522
+ orbitSineFrequencyHz: 440,
1523
+ orbitSineEnabled: true,
1524
+ orbitSinePhaseRad: 0,
1354
1525
  inProximityRoom: false
1355
1526
  };
1356
1527
  }
@@ -1360,6 +1531,14 @@ function clearRoomStateTimer(state) {
1360
1531
  state.roomStateTimer = void 0;
1361
1532
  }
1362
1533
  }
1534
+ function stopLoopPadsForClip(sessionId, state, clipId) {
1535
+ for (const [playId, pad] of [...state.loopPads.entries()]) {
1536
+ if (pad.clipId === clipId) {
1537
+ void stopPlay(playId);
1538
+ clearLoopPad(sessionId, state, playId);
1539
+ }
1540
+ }
1541
+ }
1363
1542
  function clearLoopPad(sessionId, state, playId) {
1364
1543
  const timer = state.loopPollTimers.get(playId);
1365
1544
  if (timer) {
@@ -1383,8 +1562,162 @@ function clearAllPlays(sessionId, state) {
1383
1562
  state.loopPads.clear();
1384
1563
  state.loopPollTimers.clear();
1385
1564
  }
1386
- function emitOrbitPose(sessionId, state) {
1565
+ function currentOrbitPose(state) {
1566
+ if (state.orbitManualPose) {
1567
+ const elevation = state.orbit.elevation ?? 0;
1568
+ return {
1569
+ position: {
1570
+ x: state.orbitManualPose.x,
1571
+ y: elevation,
1572
+ z: state.orbitManualPose.z
1573
+ },
1574
+ orientation: { x: 0, y: 0, z: 0, w: 1 }
1575
+ };
1576
+ }
1387
1577
  if (!state.orbitStartMs) {
1578
+ return orbitTtsPose(0, state.orbit);
1579
+ }
1580
+ const elapsedSec = (Date.now() - state.orbitStartMs) / 1e3;
1581
+ return orbitTtsPose(elapsedSec, state.orbit);
1582
+ }
1583
+ function orbitPoseFromXz(state, x, z) {
1584
+ const elevation = state.orbit.elevation ?? 0;
1585
+ return {
1586
+ position: { x, y: elevation, z },
1587
+ orientation: { x: 0, y: 0, z: 0, w: 1 }
1588
+ };
1589
+ }
1590
+ function syncOrbitClockFromManualPose(state) {
1591
+ const manual = state.orbitManualPose;
1592
+ if (!manual) {
1593
+ return;
1594
+ }
1595
+ const periodSec = state.orbit.periodSec ?? 2 * Math.PI;
1596
+ const angleRad = Math.atan2(manual.z, manual.x);
1597
+ const elapsedSec = angleRad * periodSec / (2 * Math.PI);
1598
+ state.orbitStartMs = Date.now() - elapsedSec * 1e3;
1599
+ state.orbitManualPose = void 0;
1600
+ }
1601
+ function clearOrbitSinePoll(state) {
1602
+ if (state.orbitSinePollTimer) {
1603
+ clearInterval(state.orbitSinePollTimer);
1604
+ state.orbitSinePollTimer = void 0;
1605
+ }
1606
+ }
1607
+ function clearOrbitSineRestartTimer(state) {
1608
+ if (state.orbitSineRestartTimer) {
1609
+ clearTimeout(state.orbitSineRestartTimer);
1610
+ state.orbitSineRestartTimer = void 0;
1611
+ }
1612
+ }
1613
+ async function stopOrbitSine(state) {
1614
+ clearOrbitSinePoll(state);
1615
+ clearOrbitSineRestartTimer(state);
1616
+ const playId = state.orbitSinePlayId;
1617
+ state.orbitSinePlayId = void 0;
1618
+ if (playId) {
1619
+ await stopPlay(playId);
1620
+ }
1621
+ }
1622
+ function orbitSineClipBytes(state) {
1623
+ return buildOrbitSineInlineClipBase64(
1624
+ ORBIT_SINE_DURATION_MS,
1625
+ state.orbitSineFrequencyHz,
1626
+ void 0,
1627
+ state.orbitSinePhaseRad
1628
+ );
1629
+ }
1630
+ function orbitSineClipDuration(state) {
1631
+ return orbitSineClipDurationMs(
1632
+ ORBIT_SINE_DURATION_MS,
1633
+ state.orbitSineFrequencyHz
1634
+ );
1635
+ }
1636
+ function scheduleOrbitSineGaplessRestart(sessionId, state) {
1637
+ clearOrbitSineRestartTimer(state);
1638
+ const durationMs = orbitSineClipDuration(state);
1639
+ const delayMs = Math.max(0, durationMs - ORBIT_SINE_GAPLESS_LEAD_MS);
1640
+ state.orbitSineRestartTimer = setTimeout(() => {
1641
+ state.orbitSineRestartTimer = void 0;
1642
+ void restartOrbitSineGapless(sessionId, state);
1643
+ }, delayMs);
1644
+ }
1645
+ async function restartOrbitSineGapless(sessionId, state) {
1646
+ if (!state.orbitSineEnabled || !state.orbitSinePlayId) {
1647
+ return;
1648
+ }
1649
+ const previousPlayId = state.orbitSinePlayId;
1650
+ const result = await play({
1651
+ url: ORBIT_SINE_INLINE_URL,
1652
+ bytes: orbitSineClipBytes(state),
1653
+ sessionIds: [sessionId],
1654
+ volume: ORBIT_SINE_VOLUME,
1655
+ pose: currentOrbitPose(state)
1656
+ });
1657
+ if (!result.ok || !result.playId) {
1658
+ agentLog(
1659
+ "warn",
1660
+ `spatial-showcase orbit sine gapless restart failed: ${result.reason ?? "play_failed"}`,
1661
+ sessionId
1662
+ );
1663
+ startOrbitSineLoopPoll(sessionId, state);
1664
+ return;
1665
+ }
1666
+ state.orbitSinePlayId = result.playId;
1667
+ void stopPlay(previousPlayId);
1668
+ scheduleOrbitSineGaplessRestart(sessionId, state);
1669
+ }
1670
+ function startOrbitSineLoopPoll(sessionId, state) {
1671
+ if (state.orbitSineRestartTimer) {
1672
+ return;
1673
+ }
1674
+ clearOrbitSinePoll(state);
1675
+ const playId = state.orbitSinePlayId;
1676
+ if (!playId) {
1677
+ return;
1678
+ }
1679
+ state.orbitSinePollTimer = setInterval(() => {
1680
+ void (async () => {
1681
+ if (!state.orbitSinePlayId || state.orbitSineRestartTimer) {
1682
+ clearOrbitSinePoll(state);
1683
+ return;
1684
+ }
1685
+ const status = await getPlay(state.orbitSinePlayId);
1686
+ if (!status.ok) {
1687
+ return;
1688
+ }
1689
+ if (status.status === "ended") {
1690
+ await restartOrbitSineGapless(sessionId, state);
1691
+ }
1692
+ })();
1693
+ }, ORBIT_SINE_POLL_MS);
1694
+ }
1695
+ async function startOrbitSinePlay(sessionId, state) {
1696
+ if (!state.orbitSineEnabled) {
1697
+ return;
1698
+ }
1699
+ await stopOrbitSine(state);
1700
+ const result = await play({
1701
+ url: ORBIT_SINE_INLINE_URL,
1702
+ bytes: orbitSineClipBytes(state),
1703
+ sessionIds: [sessionId],
1704
+ volume: ORBIT_SINE_VOLUME,
1705
+ pose: currentOrbitPose(state)
1706
+ });
1707
+ if (!result.ok || !result.playId) {
1708
+ agentLog(
1709
+ "warn",
1710
+ `spatial-showcase orbit sine play failed: ${result.reason ?? "play_failed"}`,
1711
+ sessionId
1712
+ );
1713
+ return;
1714
+ }
1715
+ state.orbitSinePlayId = result.playId;
1716
+ scheduleOrbitSineGaplessRestart(sessionId, state);
1717
+ startOrbitSineLoopPoll(sessionId, state);
1718
+ }
1719
+ function emitOrbitPose(sessionId, state) {
1720
+ if (!state.orbitStartMs && !state.orbitManualPose) {
1388
1721
  return;
1389
1722
  }
1390
1723
  const now = Date.now();
@@ -1392,9 +1725,8 @@ function emitOrbitPose(sessionId, state) {
1392
1725
  return;
1393
1726
  }
1394
1727
  state.lastOrbitPoseEmitMs = now;
1395
- const elapsedSec = (now - state.orbitStartMs) / 1e3;
1396
- const pose = orbitTtsPose(elapsedSec, state.orbit);
1397
- const angleRad = 2 * Math.PI * elapsedSec / (state.orbit.periodSec ?? 2 * Math.PI);
1728
+ const pose = currentOrbitPose(state);
1729
+ const angleRad = state.orbitManualPose ? Math.atan2(state.orbitManualPose.z, state.orbitManualPose.x) : 2 * Math.PI * (now - state.orbitStartMs) / 1e3 / (state.orbit.periodSec ?? 2 * Math.PI);
1398
1730
  sendToClient(sessionId, {
1399
1731
  type: "orbit_pose",
1400
1732
  angleDeg: angleRad * 180 / Math.PI,
@@ -1402,18 +1734,36 @@ function emitOrbitPose(sessionId, state) {
1402
1734
  z: pose.position.z
1403
1735
  });
1404
1736
  }
1737
+ function emitOrbitPlacePose(sessionId, state, x, z) {
1738
+ state.lastOrbitPoseEmitMs = Date.now();
1739
+ const angleRad = Math.atan2(z, x);
1740
+ sendToClient(sessionId, {
1741
+ type: "orbit_pose",
1742
+ angleDeg: angleRad * 180 / Math.PI,
1743
+ x,
1744
+ z
1745
+ });
1746
+ }
1405
1747
  function startOrbitDemo(sessionId, state) {
1406
1748
  stopOrbitInterval(state);
1407
1749
  beginOrbitClock(state);
1750
+ void startOrbitSinePlay(sessionId, state);
1408
1751
  state.orbitTimer = setInterval(() => {
1409
- if (state.orbit.paused || !state.orbitStartMs) {
1752
+ if (state.orbit.paused || !state.orbitStartMs && !state.orbitManualPose) {
1410
1753
  return;
1411
1754
  }
1412
- const elapsedSec = (Date.now() - state.orbitStartMs) / 1e3;
1755
+ const pose = currentOrbitPose(state);
1756
+ if (state.orbitSinePlayId) {
1757
+ void safeMixCall(
1758
+ sessionId,
1759
+ "setPlayPose",
1760
+ () => setPlayPose(state.orbitSinePlayId, pose)
1761
+ );
1762
+ }
1413
1763
  void safeMixCall(
1414
1764
  sessionId,
1415
1765
  "setTtsPose",
1416
- () => setTtsPose(sessionId, orbitTtsPose(elapsedSec, state.orbit))
1766
+ () => setTtsPose(sessionId, pose)
1417
1767
  );
1418
1768
  emitOrbitPose(sessionId, state);
1419
1769
  }, ORBIT_TICK_MS);
@@ -1482,6 +1832,13 @@ async function startLoopPadPoll(sessionId, state, playId, pad) {
1482
1832
  clearLoopPad(sessionId, state, playId);
1483
1833
  state.playIds.add(replay.playId);
1484
1834
  state.loopPads.set(replay.playId, pad);
1835
+ ack(sessionId, "pad", {
1836
+ playId: replay.playId,
1837
+ clipId: pad.clipId,
1838
+ x: pad.x,
1839
+ z: pad.z,
1840
+ ...pad.placement !== void 0 ? { placement: pad.placement } : {}
1841
+ });
1485
1842
  void startLoopPadPoll(sessionId, state, replay.playId, pad);
1486
1843
  }
1487
1844
  } else if (status.status) {
@@ -1496,21 +1853,24 @@ async function startLoopPadPoll(sessionId, state, playId, pad) {
1496
1853
  state.loopPollTimers.set(playId, timer);
1497
1854
  }
1498
1855
  async function handlePadPlay(sessionId, state, clipId, x, z, volume, placement) {
1499
- if (state.playIds.size >= MAX_ACTIVE_PLAYS) {
1500
- ackError(sessionId, "pad", "busy");
1501
- return;
1502
- }
1503
1856
  const assetOrigin = state.assetOrigin;
1504
1857
  if (!assetOrigin) {
1505
- ackError(sessionId, "pad", "missing_asset_origin");
1858
+ ackError(sessionId, "pad", "missing_asset_origin", { clipId });
1506
1859
  return;
1507
1860
  }
1508
1861
  const url = resolveClipUrl(assetOrigin, clipId);
1509
1862
  if (!url) {
1510
- ackError(sessionId, "pad", "invalid_asset_origin");
1863
+ ackError(sessionId, "pad", "invalid_asset_origin", { clipId });
1511
1864
  return;
1512
1865
  }
1513
1866
  const sound = SHOWCASE_SOUND_FILES[clipId];
1867
+ if (sound.loop) {
1868
+ stopLoopPadsForClip(sessionId, state, clipId);
1869
+ }
1870
+ if (state.playIds.size >= MAX_ACTIVE_PLAYS) {
1871
+ ackError(sessionId, "pad", "busy", { clipId });
1872
+ return;
1873
+ }
1514
1874
  const playVolume = volume ?? sound.defaultVolume;
1515
1875
  const options = {
1516
1876
  url,
@@ -1520,11 +1880,17 @@ async function handlePadPlay(sessionId, state, clipId, x, z, volume, placement)
1520
1880
  };
1521
1881
  const result = await play(options);
1522
1882
  if (!result.ok || !result.playId) {
1523
- ackError(sessionId, "pad", result.reason ?? "play_failed");
1883
+ ackError(sessionId, "pad", result.reason ?? "play_failed", { clipId });
1524
1884
  return;
1525
1885
  }
1526
1886
  state.playIds.add(result.playId);
1527
- ack(sessionId, "pad", { playId: result.playId });
1887
+ ack(sessionId, "pad", {
1888
+ playId: result.playId,
1889
+ clipId,
1890
+ x,
1891
+ z,
1892
+ ...placement !== void 0 ? { placement } : {}
1893
+ });
1528
1894
  if (sound.loop) {
1529
1895
  const pad = {
1530
1896
  clipId,
@@ -1538,6 +1904,24 @@ async function handlePadPlay(sessionId, state, clipId, x, z, volume, placement)
1538
1904
  void startLoopPadPoll(sessionId, state, result.playId, pad);
1539
1905
  }
1540
1906
  }
1907
+ async function handlePadMove(sessionId, state, playId, x, z) {
1908
+ if (!state.playIds.has(playId)) {
1909
+ ackError(sessionId, "pad_move", "unknown_play");
1910
+ return;
1911
+ }
1912
+ const poseResult = await setPlayPose(playId, poseAt(x, z));
1913
+ if (!poseResult.ok) {
1914
+ ackError(sessionId, "pad_move", poseResult.reason ?? "set_pose_failed");
1915
+ return;
1916
+ }
1917
+ const loopPad = state.loopPads.get(playId);
1918
+ if (loopPad) {
1919
+ loopPad.x = x;
1920
+ loopPad.z = z;
1921
+ delete loopPad.placement;
1922
+ }
1923
+ ack(sessionId, "pad_move", { playId, x, z });
1924
+ }
1541
1925
  async function handleJoin(sessionId, demo, assetOrigin) {
1542
1926
  teardownSession(sessionId);
1543
1927
  const state = defaultSessionState(demo, assetOrigin);
@@ -1553,9 +1937,15 @@ async function handleJoin(sessionId, demo, assetOrigin) {
1553
1937
  }
1554
1938
  ack(sessionId, "join");
1555
1939
  switch (demo) {
1556
- case "orbit":
1940
+ case "orbit": {
1941
+ void safeMixCall(
1942
+ sessionId,
1943
+ "setSttEnabled",
1944
+ () => setSttEnabled({ enabled: false, sessionId })
1945
+ );
1557
1946
  startOrbitDemo(sessionId, state);
1558
1947
  break;
1948
+ }
1559
1949
  case "soundboard":
1560
1950
  break;
1561
1951
  case "proximity": {
@@ -1600,8 +1990,14 @@ function teardownSession(sessionId) {
1600
1990
  }
1601
1991
  resetOrbitSession(state);
1602
1992
  clearRoomStateTimer(state);
1993
+ void stopOrbitSine(state);
1603
1994
  clearAllPlays(sessionId, state);
1604
1995
  void safeMixCall(sessionId, "clearTtsPose", () => clearTtsPose(sessionId));
1996
+ void safeMixCall(
1997
+ sessionId,
1998
+ "setSttEnabled",
1999
+ () => setSttEnabled({ enabled: true, sessionId })
2000
+ );
1605
2001
  if (state.inProximityRoom) {
1606
2002
  proximityRoom.leave(sessionId);
1607
2003
  void safeMixCall(
@@ -1652,11 +2048,54 @@ async function dispatchMessage(sessionId, message) {
1652
2048
  state.orbit.elevation = message.elevation;
1653
2049
  }
1654
2050
  if (message.paused !== void 0) {
2051
+ if (message.paused === false && state.orbitManualPose) {
2052
+ syncOrbitClockFromManualPose(state);
2053
+ }
1655
2054
  state.orbit.paused = message.paused;
1656
2055
  }
2056
+ if (message.frequencyHz !== void 0) {
2057
+ state.orbitSineFrequencyHz = message.frequencyHz;
2058
+ if (state.orbitSineEnabled && state.orbitSinePlayId) {
2059
+ void startOrbitSinePlay(sessionId, state);
2060
+ }
2061
+ }
2062
+ if (message.sinePlaying !== void 0) {
2063
+ state.orbitSineEnabled = message.sinePlaying;
2064
+ if (message.sinePlaying) {
2065
+ if (!state.orbitSinePlayId) {
2066
+ void startOrbitSinePlay(sessionId, state);
2067
+ }
2068
+ } else {
2069
+ void stopOrbitSine(state);
2070
+ }
2071
+ }
1657
2072
  ack(sessionId, "orbit_set");
1658
2073
  return;
1659
2074
  }
2075
+ if (message.action === "place") {
2076
+ state.orbit.paused = true;
2077
+ state.orbitManualPose = { x: message.x, z: message.z };
2078
+ const pose = orbitPoseFromXz(state, message.x, message.z);
2079
+ if (state.orbitSinePlayId) {
2080
+ void safeMixCall(
2081
+ sessionId,
2082
+ "setPlayPose",
2083
+ () => setPlayPose(state.orbitSinePlayId, pose)
2084
+ );
2085
+ }
2086
+ void safeMixCall(
2087
+ sessionId,
2088
+ "setTtsPose",
2089
+ () => setTtsPose(sessionId, pose)
2090
+ );
2091
+ emitOrbitPlacePose(sessionId, state, message.x, message.z);
2092
+ ack(sessionId, "orbit_place");
2093
+ return;
2094
+ }
2095
+ sendToClient(sessionId, {
2096
+ type: "orbit_say_status",
2097
+ status: "playing"
2098
+ });
1660
2099
  speak(sessionId, message.text);
1661
2100
  ack(sessionId, "orbit_say");
1662
2101
  return;
@@ -1666,6 +2105,10 @@ async function dispatchMessage(sessionId, message) {
1666
2105
  ackError(sessionId, "pad", "wrong_demo");
1667
2106
  return;
1668
2107
  }
2108
+ if (!isShowcaseClipId(message.clipId)) {
2109
+ ackError(sessionId, "pad", "invalid_clip_id");
2110
+ return;
2111
+ }
1669
2112
  await handlePadPlay(
1670
2113
  sessionId,
1671
2114
  state,
@@ -1709,6 +2152,20 @@ async function dispatchMessage(sessionId, message) {
1709
2152
  }
1710
2153
  return;
1711
2154
  }
2155
+ case "pad_move": {
2156
+ if (state.demo !== "soundboard") {
2157
+ ackError(sessionId, "pad_move", "wrong_demo");
2158
+ return;
2159
+ }
2160
+ await handlePadMove(
2161
+ sessionId,
2162
+ state,
2163
+ message.playId,
2164
+ message.x,
2165
+ message.z
2166
+ );
2167
+ return;
2168
+ }
1712
2169
  case "move": {
1713
2170
  if (state.demo !== "proximity") {
1714
2171
  ackError(sessionId, "move", "wrong_demo");
@@ -1784,13 +2241,6 @@ defineAgent({
1784
2241
  ok: true
1785
2242
  });
1786
2243
  },
1787
- onUserSpeechFinal({ sessionId, text }) {
1788
- const state = sessions.get(sessionId);
1789
- if (!state || state.demo !== "orbit") {
1790
- return;
1791
- }
1792
- speak(sessionId, `You said: ${text}`);
1793
- },
1794
2244
  onDataChannelMessage(ctx) {
1795
2245
  const message = parseShowcaseMessage(ctx.message);
1796
2246
  if (!message) {
@@ -1798,11 +2248,30 @@ defineAgent({
1798
2248
  }
1799
2249
  void dispatchMessage(ctx.sessionId, message);
1800
2250
  },
2251
+ onSpeechEvent(ctx, event) {
2252
+ const state = sessions.get(ctx.sessionId);
2253
+ if (!state || state.demo !== "orbit") {
2254
+ return;
2255
+ }
2256
+ if (event.type === "agent_speaking_start") {
2257
+ sendToClient(ctx.sessionId, {
2258
+ type: "orbit_say_status",
2259
+ status: "playing"
2260
+ });
2261
+ } else if (event.type === "agent_speaking_end") {
2262
+ sendToClient(ctx.sessionId, {
2263
+ type: "orbit_say_status",
2264
+ status: "ended"
2265
+ });
2266
+ }
2267
+ },
1801
2268
  onSessionEnd({ sessionId }) {
1802
2269
  teardownSession(sessionId);
1803
2270
  agentLog("info", `spatial-showcase session_end ${sessionId}`, sessionId);
1804
2271
  }
1805
2272
  });
1806
2273
  export {
2274
+ MAX_ACTIVE_PLAYS,
2275
+ handlePadMove,
1807
2276
  handlePadPlay
1808
2277
  };