@agimon-ai/doompi-voice 0.0.1-alpha.71 → 0.0.1-alpha.73
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 +1 -1
- package/dist/src/controllers/voiceMediaHubChannel.cjs +1 -1
- package/dist/src/controllers/voiceMediaHubChannel.cjs.map +1 -1
- package/dist/src/controllers/voiceMediaHubChannel.mjs +1 -1
- package/dist/src/controllers/voiceMediaHubChannel.mjs.map +1 -1
- package/dist/src/controllers/voiceSessionApi.cjs +1 -1
- package/dist/src/controllers/voiceSessionApi.cjs.map +1 -1
- package/dist/src/controllers/voiceSessionApi.mjs +1 -1
- package/dist/src/controllers/voiceSessionApi.mjs.map +1 -1
- package/dist/src/types/clientMedia.cjs +1 -1
- package/dist/src/types/clientMedia.cjs.map +1 -1
- package/dist/src/types/clientMedia.d.cts.map +1 -1
- package/dist/src/types/clientMedia.d.mts.map +1 -1
- package/dist/src/types/clientMedia.mjs +1 -1
- package/dist/src/types/clientMedia.mjs.map +1 -1
- package/package.json +15 -15
- package/src/types/clientMedia.ts +3 -1
- package/src/web/stores/voiceMicrophoneStore.ts +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Voice owns these authenticated server endpoints:
|
|
|
17
17
|
|
|
18
18
|
| Mount | Endpoint | Purpose |
|
|
19
19
|
| --------------------------------------- | ------------------------------- | -------------------------------------------------------- |
|
|
20
|
-
| `/api/
|
|
20
|
+
| `/api/plugins/voice` | `GET /readiness` | Check configuration and local transcription dependencies |
|
|
21
21
|
| same | `GET /clients/:clientId` | Read browser microphone preferences |
|
|
22
22
|
| same | `PUT /clients/:clientId/inputs` | Register `{inputs: [{deviceId, groupId, label}]}` |
|
|
23
23
|
| same | `PUT /clients/:clientId` | Save `{deviceId}`; null clears selection |
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../types/clientMedia.cjs"),t=require("../types/voiceOwnership.cjs"),n=require("../services/voiceOwnershipCoordinator/index.cjs");function r(e){if(typeof e!=`object`||!e||Array.isArray(e))return;let t=e;if(!(Object.keys(t).sort().join(`,`)!==`eventEpoch,sequence`||typeof t.eventEpoch!=`string`||t.eventEpoch.length===0||t.eventEpoch.length>200||!Number.isSafeInteger(t.sequence)||t.sequence<0))return{eventEpoch:t.eventEpoch,sequence:t.sequence}}function i(){return{frameType:e.VOICE_MEDIA_WAKE_TYPE,start(t){let n=new Map,i=new Map;return{payloadFor(e){return n.get(e.sessionId)},sessionAdded(a){i.get(a.sessionId)?.();let o=t.directEvents.subscribe(e.VOICE_MEDIA_WAKE_TYPE,a.sessionId,e=>{let i=r(e);if(i===void 0)return;let o=n.get(a.sessionId);o?.eventEpoch===i.eventEpoch&&i.sequence<=o.sequence||(n.set(a.sessionId,i),t.publish(a.sessionId,i))},{replayLatest:!0});i.set(a.sessionId,o)},sessionRemoved(e){i.get(e)?.(),i.delete(e),n.delete(e)},close(){for(let e of i.values())e();i.clear(),n.clear()}}}}}function a(){return{frameType:t.VOICE_OWNERSHIP_FRAME_TYPE,lifecycle:`hub`,start(r){let i=new Map,a=new Map,o=new Set,s=``,c=new n.VoiceOwnershipCoordinator({send:async(n,a)=>{let o=i.get(n);if(o===void 0)throw Error(`Voice session ${n} is unavailable.`);let s=await r.requestSessionApi(o,{basePath:e.VOICE_MEDIA_API_BASE_PATH,path:t.VOICE_OWNERSHIP_ROUTES.command,method:`POST`,body:JSON.stringify(a),signal:AbortSignal.timeout(t.VOICE_OWNERSHIP_COMMAND_TIMEOUT_MS)}),c=s.ok?t.parseVoiceOwnershipAcknowledgement(await s.json()):void 0;if(c===void 0)throw Error(`Voice ownership command failed with HTTP ${s.status}.`);return c}},e=>{for(let t of i.values())r.publish(t.sessionId,e)},{now:()=>Date.now(),createId:()=>globalThis.crypto.randomUUID()}),
|
|
1
|
+
const e=require("../types/clientMedia.cjs"),t=require("../types/voiceOwnership.cjs"),n=require("../services/voiceOwnershipCoordinator/index.cjs");function r(e){if(typeof e!=`object`||!e||Array.isArray(e))return;let t=e;if(!(Object.keys(t).sort().join(`,`)!==`eventEpoch,sequence`||typeof t.eventEpoch!=`string`||t.eventEpoch.length===0||t.eventEpoch.length>200||!Number.isSafeInteger(t.sequence)||t.sequence<0))return{eventEpoch:t.eventEpoch,sequence:t.sequence}}function i(){return{frameType:e.VOICE_MEDIA_WAKE_TYPE,start(t){let n=new Map,i=new Map;return{payloadFor(e){return n.get(e.sessionId)},sessionAdded(a){i.get(a.sessionId)?.();let o=t.directEvents.subscribe(e.VOICE_MEDIA_WAKE_TYPE,a.sessionId,e=>{let i=r(e);if(i===void 0)return;let o=n.get(a.sessionId);o?.eventEpoch===i.eventEpoch&&i.sequence<=o.sequence||(n.set(a.sessionId,i),t.publish(a.sessionId,i))},{replayLatest:!0});i.set(a.sessionId,o)},sessionRemoved(e){i.get(e)?.(),i.delete(e),n.delete(e)},close(){for(let e of i.values())e();i.clear(),n.clear()}}}}}function a(){return{frameType:t.VOICE_OWNERSHIP_FRAME_TYPE,lifecycle:`hub`,start(r){let i=new Map,a=new Map,o=new Set,s=``,c,l=new n.VoiceOwnershipCoordinator({send:async(n,a)=>{let o=i.get(n);if(o===void 0)throw Error(`Voice session ${n} is unavailable.`);let s=await r.requestSessionApi(o,{basePath:e.VOICE_MEDIA_API_BASE_PATH,path:t.VOICE_OWNERSHIP_ROUTES.command,method:`POST`,body:JSON.stringify(a),signal:AbortSignal.timeout(t.VOICE_OWNERSHIP_COMMAND_TIMEOUT_MS)}),c=s.ok?t.parseVoiceOwnershipAcknowledgement(await s.json()):void 0;if(c===void 0)throw Error(`Voice ownership command failed with HTTP ${s.status}.`);return c}},e=>{for(let t of i.values())r.publish(t.sessionId,e)},{now:()=>Date.now(),createId:()=>globalThis.crypto.randomUUID()}),u=e=>{if(o.has(e))return!1;if(o.add(e),o.size>512){let e=o.values().next().value;e!==void 0&&o.delete(e)}return!0},d=async()=>{let e=[...i.keys()].sort((e,t)=>e.localeCompare(t)).map(e=>`${e}:${JSON.stringify(l.catalog(e))}`).join(`|`);e!==s&&(await l.publishCatalogs(),s=e)},f=async()=>{for(;c!==void 0;)await c.catch(()=>void 0);let e=d();c=e;try{await e}finally{c===e&&(c=void 0)}},p=async(e,t)=>{if(t.handoff!==void 0){let n=`${e}:handoff:${t.handoff.requestId}`;if(!u(n))return;try{await l.handoff(e,t.handoff.handle)||r.onNotice(`voice handoff requested by ${e} was rejected`)}catch(t){r.onNotice(`voice handoff requested by ${e} failed (${t instanceof Error?t.message:String(t)})`)}return}if(t.activation===void 0)return;let n=`${e}:activate:${t.activation.requestId}`;if(u(n))try{await l.activate(e)||r.onNotice(`voice activation requested by ${e} was rejected`)}catch(t){r.onNotice(`voice activation requested by ${e} failed (${t instanceof Error?t.message:String(t)})`)}},m=(e,n)=>{let i=t.parseVoiceOwnershipSessionSnapshot(n);if(i?.registration===void 0){l.remove(e),f().catch(e=>r.onNotice(`voice ownership catalog update failed (${e instanceof Error?e.message:String(e)})`));return}l.update(e,i.registration),p(e,i),f().catch(e=>r.onNotice(`voice ownership catalog update failed (${e instanceof Error?e.message:String(e)})`))};return{payloadFor(){return l.payload()},sessionAdded(e){i.set(e.sessionId,e),a.get(e.sessionId)?.();let n=r.directEvents.subscribe(t.VOICE_OWNERSHIP_FRAME_TYPE,e.sessionId,t=>m(e.sessionId,t),{replayLatest:!0});a.set(e.sessionId,n),f().catch(e=>r.onNotice(`voice ownership catalog update failed (${e instanceof Error?e.message:String(e)})`))},sessionRemoved(e){a.get(e)?.(),a.delete(e),i.delete(e),l.remove(e),s=``},close(){for(let e of a.values())e();a.clear(),i.clear(),o.clear()}}}}}exports.createVoiceMediaWakeChannel=i,exports.createVoiceOwnershipChannel=a;
|
|
2
2
|
//# sourceMappingURL=voiceMediaHubChannel.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voiceMediaHubChannel.cjs","names":["VOICE_MEDIA_WAKE_TYPE","VOICE_OWNERSHIP_FRAME_TYPE","VOICE_MEDIA_API_BASE_PATH","VOICE_OWNERSHIP_ROUTES","VOICE_OWNERSHIP_COMMAND_TIMEOUT_MS","parseVoiceOwnershipAcknowledgement","VoiceOwnershipCoordinator","parseVoiceOwnershipSessionSnapshot"],"sources":["../../../src/controllers/voiceMediaHubChannel.ts"],"sourcesContent":["import type { DoomHubChannel, DoomHubChannelSource, DoomHubSessionScope } from '@agimon-ai/doompi-core/hub-channel';\n\nimport { VoiceOwnershipCoordinator } from '../services/voiceOwnershipCoordinator';\nimport { VOICE_MEDIA_API_BASE_PATH, VOICE_MEDIA_WAKE_TYPE, type VoiceMediaWake } from '../types/clientMedia';\nimport {\n VOICE_OWNERSHIP_COMMAND_TIMEOUT_MS,\n VOICE_OWNERSHIP_FRAME_TYPE,\n VOICE_OWNERSHIP_ROUTES,\n parseVoiceOwnershipAcknowledgement,\n parseVoiceOwnershipSessionSnapshot,\n type BrowserVoiceOwnershipPayload,\n type VoiceOwnershipCommand,\n type VoiceOwnershipSessionSnapshot,\n} from '../types/voiceOwnership';\n\nconst MAX_HANDLED_REQUESTS = 512;\nconst MAX_EVENT_EPOCH_LENGTH = 200;\n\nfunction parseVoiceMediaWake(value: unknown): VoiceMediaWake | undefined {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;\n const record = value as Record<string, unknown>;\n if (\n Object.keys(record).sort().join(',') !== 'eventEpoch,sequence' ||\n typeof record.eventEpoch !== 'string' ||\n record.eventEpoch.length === 0 ||\n record.eventEpoch.length > MAX_EVENT_EPOCH_LENGTH ||\n !Number.isSafeInteger(record.sequence) ||\n (record.sequence as number) < 0\n )\n return undefined;\n return { eventEpoch: record.eventEpoch, sequence: record.sequence as number };\n}\n\n/** Publishes session-owned media wakeups through the host's direct event bus. */\nexport function createVoiceMediaWakeChannel(): DoomHubChannel {\n return {\n frameType: VOICE_MEDIA_WAKE_TYPE,\n start(host) {\n const latest = new Map<string, VoiceMediaWake>();\n const subscriptions = new Map<string, () => void>();\n return {\n payloadFor(scope) {\n return latest.get(scope.sessionId);\n },\n sessionAdded(scope) {\n subscriptions.get(scope.sessionId)?.();\n const unsubscribe = host.directEvents.subscribe(\n VOICE_MEDIA_WAKE_TYPE,\n scope.sessionId,\n (payload) => {\n const wake = parseVoiceMediaWake(payload);\n if (wake === undefined) return;\n const current = latest.get(scope.sessionId);\n if (current?.eventEpoch === wake.eventEpoch && wake.sequence <= current.sequence) return;\n latest.set(scope.sessionId, wake);\n host.publish(scope.sessionId, wake);\n },\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n latest.clear();\n },\n } satisfies DoomHubChannelSource;\n },\n };\n}\n\n/** Coordinates browser voice ownership from session lifecycle events. */\nexport function createVoiceOwnershipChannel(): DoomHubChannel {\n return {\n frameType: VOICE_OWNERSHIP_FRAME_TYPE,\n lifecycle: 'hub',\n start(host) {\n const scopes = new Map<string, DoomHubSessionScope>();\n const subscriptions = new Map<string, () => void>();\n const handledRequests = new Set<string>();\n let catalogSignature = '';\n\n const publishSelection = (payload: BrowserVoiceOwnershipPayload): void => {\n for (const scope of scopes.values()) host.publish(scope.sessionId, payload);\n };\n\n const sendCommand = async (sessionId: string, command: VoiceOwnershipCommand) => {\n const scope = scopes.get(sessionId);\n if (scope === undefined) throw new Error(`Voice session ${sessionId} is unavailable.`);\n const response = await host.requestSessionApi(scope, {\n basePath: VOICE_MEDIA_API_BASE_PATH,\n path: VOICE_OWNERSHIP_ROUTES.command,\n method: 'POST',\n body: JSON.stringify(command),\n signal: AbortSignal.timeout(VOICE_OWNERSHIP_COMMAND_TIMEOUT_MS),\n });\n const acknowledgement = response.ok ? parseVoiceOwnershipAcknowledgement(await response.json()) : undefined;\n if (acknowledgement === undefined)\n throw new Error(`Voice ownership command failed with HTTP ${response.status}.`);\n return acknowledgement;\n };\n\n const coordinator = new VoiceOwnershipCoordinator({ send: sendCommand }, publishSelection, {\n now: () => Date.now(),\n createId: () => globalThis.crypto.randomUUID(),\n });\n\n const remember = (key: string): boolean => {\n if (handledRequests.has(key)) return false;\n handledRequests.add(key);\n if (handledRequests.size > MAX_HANDLED_REQUESTS) {\n const oldest = handledRequests.values().next().value;\n if (oldest !== undefined) handledRequests.delete(oldest);\n }\n return true;\n };\n\n const refreshCatalogs = async (): Promise<void> => {\n const nextSignature = [...scopes.keys()]\n .sort((left, right) => left.localeCompare(right))\n .map((sessionId) => `${sessionId}:${JSON.stringify(coordinator.catalog(sessionId))}`)\n .join('|');\n if (nextSignature === catalogSignature) return;\n await coordinator.publishCatalogs();\n catalogSignature = nextSignature;\n };\n\n const processSnapshot = async (sessionId: string, snapshot: VoiceOwnershipSessionSnapshot): Promise<void> => {\n if (snapshot.handoff !== undefined) {\n const key = `${sessionId}:handoff:${snapshot.handoff.requestId}`;\n if (!remember(key)) return;\n try {\n if (!(await coordinator.handoff(sessionId, snapshot.handoff.handle)))\n host.onNotice(`voice handoff requested by ${sessionId} was rejected`);\n } catch (error) {\n host.onNotice(\n `voice handoff requested by ${sessionId} failed (${error instanceof Error ? error.message : String(error)})`,\n );\n }\n return;\n }\n if (snapshot.activation === undefined) return;\n const key = `${sessionId}:activate:${snapshot.activation.requestId}`;\n if (!remember(key)) return;\n try {\n if (!(await coordinator.activate(sessionId)))\n host.onNotice(`voice activation requested by ${sessionId} was rejected`);\n } catch (error) {\n host.onNotice(\n `voice activation requested by ${sessionId} failed (${error instanceof Error ? error.message : String(error)})`,\n );\n }\n };\n\n const applySnapshot = (sessionId: string, value: unknown): void => {\n const snapshot = parseVoiceOwnershipSessionSnapshot(value);\n if (snapshot?.registration === undefined) {\n coordinator.remove(sessionId);\n void refreshCatalogs().catch((error: unknown) =>\n host.onNotice(\n `voice ownership catalog update failed (${error instanceof Error ? error.message : String(error)})`,\n ),\n );\n return;\n }\n coordinator.update(sessionId, snapshot.registration);\n void processSnapshot(sessionId, snapshot);\n void refreshCatalogs().catch((error: unknown) =>\n host.onNotice(\n `voice ownership catalog update failed (${error instanceof Error ? error.message : String(error)})`,\n ),\n );\n };\n\n const source: DoomHubChannelSource = {\n payloadFor() {\n return coordinator.payload();\n },\n sessionAdded(scope) {\n scopes.set(scope.sessionId, scope);\n subscriptions.get(scope.sessionId)?.();\n const unsubscribe = host.directEvents.subscribe(\n VOICE_OWNERSHIP_FRAME_TYPE,\n scope.sessionId,\n (payload) => applySnapshot(scope.sessionId, payload),\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n void refreshCatalogs().catch((error: unknown) =>\n host.onNotice(\n `voice ownership catalog update failed (${error instanceof Error ? error.message : String(error)})`,\n ),\n );\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n scopes.delete(sessionId);\n coordinator.remove(sessionId);\n catalogSignature = '';\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n scopes.clear();\n handledRequests.clear();\n },\n };\n return source;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"voiceMediaHubChannel.cjs","names":["VOICE_MEDIA_WAKE_TYPE","VOICE_OWNERSHIP_FRAME_TYPE","VOICE_MEDIA_API_BASE_PATH","VOICE_OWNERSHIP_ROUTES","VOICE_OWNERSHIP_COMMAND_TIMEOUT_MS","parseVoiceOwnershipAcknowledgement","VoiceOwnershipCoordinator","parseVoiceOwnershipSessionSnapshot"],"sources":["../../../src/controllers/voiceMediaHubChannel.ts"],"sourcesContent":["import type { DoomHubChannel, DoomHubChannelSource, DoomHubSessionScope } from '@agimon-ai/doompi-core/hub-channel';\n\nimport { VoiceOwnershipCoordinator } from '../services/voiceOwnershipCoordinator';\nimport { VOICE_MEDIA_API_BASE_PATH, VOICE_MEDIA_WAKE_TYPE, type VoiceMediaWake } from '../types/clientMedia';\nimport {\n VOICE_OWNERSHIP_COMMAND_TIMEOUT_MS,\n VOICE_OWNERSHIP_FRAME_TYPE,\n VOICE_OWNERSHIP_ROUTES,\n parseVoiceOwnershipAcknowledgement,\n parseVoiceOwnershipSessionSnapshot,\n type BrowserVoiceOwnershipPayload,\n type VoiceOwnershipCommand,\n type VoiceOwnershipSessionSnapshot,\n} from '../types/voiceOwnership';\n\nconst MAX_HANDLED_REQUESTS = 512;\nconst MAX_EVENT_EPOCH_LENGTH = 200;\n\nfunction parseVoiceMediaWake(value: unknown): VoiceMediaWake | undefined {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;\n const record = value as Record<string, unknown>;\n if (\n Object.keys(record).sort().join(',') !== 'eventEpoch,sequence' ||\n typeof record.eventEpoch !== 'string' ||\n record.eventEpoch.length === 0 ||\n record.eventEpoch.length > MAX_EVENT_EPOCH_LENGTH ||\n !Number.isSafeInteger(record.sequence) ||\n (record.sequence as number) < 0\n )\n return undefined;\n return { eventEpoch: record.eventEpoch, sequence: record.sequence as number };\n}\n\n/** Publishes session-owned media wakeups through the host's direct event bus. */\nexport function createVoiceMediaWakeChannel(): DoomHubChannel {\n return {\n frameType: VOICE_MEDIA_WAKE_TYPE,\n start(host) {\n const latest = new Map<string, VoiceMediaWake>();\n const subscriptions = new Map<string, () => void>();\n return {\n payloadFor(scope) {\n return latest.get(scope.sessionId);\n },\n sessionAdded(scope) {\n subscriptions.get(scope.sessionId)?.();\n const unsubscribe = host.directEvents.subscribe(\n VOICE_MEDIA_WAKE_TYPE,\n scope.sessionId,\n (payload) => {\n const wake = parseVoiceMediaWake(payload);\n if (wake === undefined) return;\n const current = latest.get(scope.sessionId);\n if (current?.eventEpoch === wake.eventEpoch && wake.sequence <= current.sequence) return;\n latest.set(scope.sessionId, wake);\n host.publish(scope.sessionId, wake);\n },\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n latest.clear();\n },\n } satisfies DoomHubChannelSource;\n },\n };\n}\n\n/** Coordinates browser voice ownership from session lifecycle events. */\nexport function createVoiceOwnershipChannel(): DoomHubChannel {\n return {\n frameType: VOICE_OWNERSHIP_FRAME_TYPE,\n lifecycle: 'hub',\n start(host) {\n const scopes = new Map<string, DoomHubSessionScope>();\n const subscriptions = new Map<string, () => void>();\n const handledRequests = new Set<string>();\n let catalogSignature = '';\n let catalogRun: Promise<void> | undefined;\n\n const publishSelection = (payload: BrowserVoiceOwnershipPayload): void => {\n for (const scope of scopes.values()) host.publish(scope.sessionId, payload);\n };\n\n const sendCommand = async (sessionId: string, command: VoiceOwnershipCommand) => {\n const scope = scopes.get(sessionId);\n if (scope === undefined) throw new Error(`Voice session ${sessionId} is unavailable.`);\n const response = await host.requestSessionApi(scope, {\n basePath: VOICE_MEDIA_API_BASE_PATH,\n path: VOICE_OWNERSHIP_ROUTES.command,\n method: 'POST',\n body: JSON.stringify(command),\n signal: AbortSignal.timeout(VOICE_OWNERSHIP_COMMAND_TIMEOUT_MS),\n });\n const acknowledgement = response.ok ? parseVoiceOwnershipAcknowledgement(await response.json()) : undefined;\n if (acknowledgement === undefined)\n throw new Error(`Voice ownership command failed with HTTP ${response.status}.`);\n return acknowledgement;\n };\n\n const coordinator = new VoiceOwnershipCoordinator({ send: sendCommand }, publishSelection, {\n now: () => Date.now(),\n createId: () => globalThis.crypto.randomUUID(),\n });\n\n const remember = (key: string): boolean => {\n if (handledRequests.has(key)) return false;\n handledRequests.add(key);\n if (handledRequests.size > MAX_HANDLED_REQUESTS) {\n const oldest = handledRequests.values().next().value;\n if (oldest !== undefined) handledRequests.delete(oldest);\n }\n return true;\n };\n\n const publishCatalogsOnce = async (): Promise<void> => {\n const nextSignature = [...scopes.keys()]\n .sort((left, right) => left.localeCompare(right))\n .map((sessionId) => `${sessionId}:${JSON.stringify(coordinator.catalog(sessionId))}`)\n .join('|');\n if (nextSignature === catalogSignature) return;\n await coordinator.publishCatalogs();\n catalogSignature = nextSignature;\n };\n\n /**\n * Serialises overlapping catalog refreshes so concurrent callers cannot both pass the\n * signature guard and publish the same state twice. Waiters do not share the in-flight run:\n * each takes its own turn afterwards and the signature guard in publishCatalogsOnce makes\n * that turn a no-op unless the state actually changed mid-flight, which is the trailing edge.\n */\n const refreshCatalogs = async (): Promise<void> => {\n // The run in flight reports its own failure to its own caller; a waiter only needs its turn.\n while (catalogRun !== undefined) await catalogRun.catch(() => undefined);\n const run = publishCatalogsOnce();\n catalogRun = run;\n try {\n await run;\n } finally {\n if (catalogRun === run) catalogRun = undefined;\n }\n };\n\n const processSnapshot = async (sessionId: string, snapshot: VoiceOwnershipSessionSnapshot): Promise<void> => {\n if (snapshot.handoff !== undefined) {\n const key = `${sessionId}:handoff:${snapshot.handoff.requestId}`;\n if (!remember(key)) return;\n try {\n if (!(await coordinator.handoff(sessionId, snapshot.handoff.handle)))\n host.onNotice(`voice handoff requested by ${sessionId} was rejected`);\n } catch (error) {\n host.onNotice(\n `voice handoff requested by ${sessionId} failed (${error instanceof Error ? error.message : String(error)})`,\n );\n }\n return;\n }\n if (snapshot.activation === undefined) return;\n const key = `${sessionId}:activate:${snapshot.activation.requestId}`;\n if (!remember(key)) return;\n try {\n if (!(await coordinator.activate(sessionId)))\n host.onNotice(`voice activation requested by ${sessionId} was rejected`);\n } catch (error) {\n host.onNotice(\n `voice activation requested by ${sessionId} failed (${error instanceof Error ? error.message : String(error)})`,\n );\n }\n };\n\n const applySnapshot = (sessionId: string, value: unknown): void => {\n const snapshot = parseVoiceOwnershipSessionSnapshot(value);\n if (snapshot?.registration === undefined) {\n coordinator.remove(sessionId);\n void refreshCatalogs().catch((error: unknown) =>\n host.onNotice(\n `voice ownership catalog update failed (${error instanceof Error ? error.message : String(error)})`,\n ),\n );\n return;\n }\n coordinator.update(sessionId, snapshot.registration);\n void processSnapshot(sessionId, snapshot);\n void refreshCatalogs().catch((error: unknown) =>\n host.onNotice(\n `voice ownership catalog update failed (${error instanceof Error ? error.message : String(error)})`,\n ),\n );\n };\n\n const source: DoomHubChannelSource = {\n payloadFor() {\n return coordinator.payload();\n },\n sessionAdded(scope) {\n scopes.set(scope.sessionId, scope);\n subscriptions.get(scope.sessionId)?.();\n const unsubscribe = host.directEvents.subscribe(\n VOICE_OWNERSHIP_FRAME_TYPE,\n scope.sessionId,\n (payload) => applySnapshot(scope.sessionId, payload),\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n void refreshCatalogs().catch((error: unknown) =>\n host.onNotice(\n `voice ownership catalog update failed (${error instanceof Error ? error.message : String(error)})`,\n ),\n );\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n scopes.delete(sessionId);\n coordinator.remove(sessionId);\n catalogSignature = '';\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n scopes.clear();\n handledRequests.clear();\n },\n };\n return source;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{VOICE_MEDIA_API_BASE_PATH as e,VOICE_MEDIA_WAKE_TYPE as t}from"../types/clientMedia.mjs";import{VOICE_OWNERSHIP_COMMAND_TIMEOUT_MS as n,VOICE_OWNERSHIP_FRAME_TYPE as r,VOICE_OWNERSHIP_ROUTES as i,parseVoiceOwnershipAcknowledgement as a,parseVoiceOwnershipSessionSnapshot as o}from"../types/voiceOwnership.mjs";import{VoiceOwnershipCoordinator as s}from"../services/voiceOwnershipCoordinator/index.mjs";function c(e){if(typeof e!=`object`||!e||Array.isArray(e))return;let t=e;if(!(Object.keys(t).sort().join(`,`)!==`eventEpoch,sequence`||typeof t.eventEpoch!=`string`||t.eventEpoch.length===0||t.eventEpoch.length>200||!Number.isSafeInteger(t.sequence)||t.sequence<0))return{eventEpoch:t.eventEpoch,sequence:t.sequence}}function l(){return{frameType:t,start(e){let n=new Map,r=new Map;return{payloadFor(e){return n.get(e.sessionId)},sessionAdded(i){r.get(i.sessionId)?.();let a=e.directEvents.subscribe(t,i.sessionId,t=>{let r=c(t);if(r===void 0)return;let a=n.get(i.sessionId);a?.eventEpoch===r.eventEpoch&&r.sequence<=a.sequence||(n.set(i.sessionId,r),e.publish(i.sessionId,r))},{replayLatest:!0});r.set(i.sessionId,a)},sessionRemoved(e){r.get(e)?.(),r.delete(e),n.delete(e)},close(){for(let e of r.values())e();r.clear(),n.clear()}}}}}function u(){return{frameType:r,lifecycle:`hub`,start(t){let c=new Map,l=new Map,u=new Set,d=``,f=new s({send:async(r,o)=>{let s=c.get(r);if(s===void 0)throw Error(`Voice session ${r} is unavailable.`);let l=await t.requestSessionApi(s,{basePath:e,path:i.command,method:`POST`,body:JSON.stringify(o),signal:AbortSignal.timeout(n)}),u=l.ok?a(await l.json()):void 0;if(u===void 0)throw Error(`Voice ownership command failed with HTTP ${l.status}.`);return u}},e=>{for(let n of c.values())t.publish(n.sessionId,e)},{now:()=>Date.now(),createId:()=>globalThis.crypto.randomUUID()}),
|
|
1
|
+
import{VOICE_MEDIA_API_BASE_PATH as e,VOICE_MEDIA_WAKE_TYPE as t}from"../types/clientMedia.mjs";import{VOICE_OWNERSHIP_COMMAND_TIMEOUT_MS as n,VOICE_OWNERSHIP_FRAME_TYPE as r,VOICE_OWNERSHIP_ROUTES as i,parseVoiceOwnershipAcknowledgement as a,parseVoiceOwnershipSessionSnapshot as o}from"../types/voiceOwnership.mjs";import{VoiceOwnershipCoordinator as s}from"../services/voiceOwnershipCoordinator/index.mjs";function c(e){if(typeof e!=`object`||!e||Array.isArray(e))return;let t=e;if(!(Object.keys(t).sort().join(`,`)!==`eventEpoch,sequence`||typeof t.eventEpoch!=`string`||t.eventEpoch.length===0||t.eventEpoch.length>200||!Number.isSafeInteger(t.sequence)||t.sequence<0))return{eventEpoch:t.eventEpoch,sequence:t.sequence}}function l(){return{frameType:t,start(e){let n=new Map,r=new Map;return{payloadFor(e){return n.get(e.sessionId)},sessionAdded(i){r.get(i.sessionId)?.();let a=e.directEvents.subscribe(t,i.sessionId,t=>{let r=c(t);if(r===void 0)return;let a=n.get(i.sessionId);a?.eventEpoch===r.eventEpoch&&r.sequence<=a.sequence||(n.set(i.sessionId,r),e.publish(i.sessionId,r))},{replayLatest:!0});r.set(i.sessionId,a)},sessionRemoved(e){r.get(e)?.(),r.delete(e),n.delete(e)},close(){for(let e of r.values())e();r.clear(),n.clear()}}}}}function u(){return{frameType:r,lifecycle:`hub`,start(t){let c=new Map,l=new Map,u=new Set,d=``,f,p=new s({send:async(r,o)=>{let s=c.get(r);if(s===void 0)throw Error(`Voice session ${r} is unavailable.`);let l=await t.requestSessionApi(s,{basePath:e,path:i.command,method:`POST`,body:JSON.stringify(o),signal:AbortSignal.timeout(n)}),u=l.ok?a(await l.json()):void 0;if(u===void 0)throw Error(`Voice ownership command failed with HTTP ${l.status}.`);return u}},e=>{for(let n of c.values())t.publish(n.sessionId,e)},{now:()=>Date.now(),createId:()=>globalThis.crypto.randomUUID()}),m=e=>{if(u.has(e))return!1;if(u.add(e),u.size>512){let e=u.values().next().value;e!==void 0&&u.delete(e)}return!0},h=async()=>{let e=[...c.keys()].sort((e,t)=>e.localeCompare(t)).map(e=>`${e}:${JSON.stringify(p.catalog(e))}`).join(`|`);e!==d&&(await p.publishCatalogs(),d=e)},g=async()=>{for(;f!==void 0;)await f.catch(()=>void 0);let e=h();f=e;try{await e}finally{f===e&&(f=void 0)}},_=async(e,n)=>{if(n.handoff!==void 0){let r=`${e}:handoff:${n.handoff.requestId}`;if(!m(r))return;try{await p.handoff(e,n.handoff.handle)||t.onNotice(`voice handoff requested by ${e} was rejected`)}catch(n){t.onNotice(`voice handoff requested by ${e} failed (${n instanceof Error?n.message:String(n)})`)}return}if(n.activation===void 0)return;let r=`${e}:activate:${n.activation.requestId}`;if(m(r))try{await p.activate(e)||t.onNotice(`voice activation requested by ${e} was rejected`)}catch(n){t.onNotice(`voice activation requested by ${e} failed (${n instanceof Error?n.message:String(n)})`)}},v=(e,n)=>{let r=o(n);if(r?.registration===void 0){p.remove(e),g().catch(e=>t.onNotice(`voice ownership catalog update failed (${e instanceof Error?e.message:String(e)})`));return}p.update(e,r.registration),_(e,r),g().catch(e=>t.onNotice(`voice ownership catalog update failed (${e instanceof Error?e.message:String(e)})`))};return{payloadFor(){return p.payload()},sessionAdded(e){c.set(e.sessionId,e),l.get(e.sessionId)?.();let n=t.directEvents.subscribe(r,e.sessionId,t=>v(e.sessionId,t),{replayLatest:!0});l.set(e.sessionId,n),g().catch(e=>t.onNotice(`voice ownership catalog update failed (${e instanceof Error?e.message:String(e)})`))},sessionRemoved(e){l.get(e)?.(),l.delete(e),c.delete(e),p.remove(e),d=``},close(){for(let e of l.values())e();l.clear(),c.clear(),u.clear()}}}}}export{l as createVoiceMediaWakeChannel,u as createVoiceOwnershipChannel};
|
|
2
2
|
//# sourceMappingURL=voiceMediaHubChannel.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voiceMediaHubChannel.mjs","names":[],"sources":["../../../src/controllers/voiceMediaHubChannel.ts"],"sourcesContent":["import type { DoomHubChannel, DoomHubChannelSource, DoomHubSessionScope } from '@agimon-ai/doompi-core/hub-channel';\n\nimport { VoiceOwnershipCoordinator } from '../services/voiceOwnershipCoordinator';\nimport { VOICE_MEDIA_API_BASE_PATH, VOICE_MEDIA_WAKE_TYPE, type VoiceMediaWake } from '../types/clientMedia';\nimport {\n VOICE_OWNERSHIP_COMMAND_TIMEOUT_MS,\n VOICE_OWNERSHIP_FRAME_TYPE,\n VOICE_OWNERSHIP_ROUTES,\n parseVoiceOwnershipAcknowledgement,\n parseVoiceOwnershipSessionSnapshot,\n type BrowserVoiceOwnershipPayload,\n type VoiceOwnershipCommand,\n type VoiceOwnershipSessionSnapshot,\n} from '../types/voiceOwnership';\n\nconst MAX_HANDLED_REQUESTS = 512;\nconst MAX_EVENT_EPOCH_LENGTH = 200;\n\nfunction parseVoiceMediaWake(value: unknown): VoiceMediaWake | undefined {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;\n const record = value as Record<string, unknown>;\n if (\n Object.keys(record).sort().join(',') !== 'eventEpoch,sequence' ||\n typeof record.eventEpoch !== 'string' ||\n record.eventEpoch.length === 0 ||\n record.eventEpoch.length > MAX_EVENT_EPOCH_LENGTH ||\n !Number.isSafeInteger(record.sequence) ||\n (record.sequence as number) < 0\n )\n return undefined;\n return { eventEpoch: record.eventEpoch, sequence: record.sequence as number };\n}\n\n/** Publishes session-owned media wakeups through the host's direct event bus. */\nexport function createVoiceMediaWakeChannel(): DoomHubChannel {\n return {\n frameType: VOICE_MEDIA_WAKE_TYPE,\n start(host) {\n const latest = new Map<string, VoiceMediaWake>();\n const subscriptions = new Map<string, () => void>();\n return {\n payloadFor(scope) {\n return latest.get(scope.sessionId);\n },\n sessionAdded(scope) {\n subscriptions.get(scope.sessionId)?.();\n const unsubscribe = host.directEvents.subscribe(\n VOICE_MEDIA_WAKE_TYPE,\n scope.sessionId,\n (payload) => {\n const wake = parseVoiceMediaWake(payload);\n if (wake === undefined) return;\n const current = latest.get(scope.sessionId);\n if (current?.eventEpoch === wake.eventEpoch && wake.sequence <= current.sequence) return;\n latest.set(scope.sessionId, wake);\n host.publish(scope.sessionId, wake);\n },\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n latest.clear();\n },\n } satisfies DoomHubChannelSource;\n },\n };\n}\n\n/** Coordinates browser voice ownership from session lifecycle events. */\nexport function createVoiceOwnershipChannel(): DoomHubChannel {\n return {\n frameType: VOICE_OWNERSHIP_FRAME_TYPE,\n lifecycle: 'hub',\n start(host) {\n const scopes = new Map<string, DoomHubSessionScope>();\n const subscriptions = new Map<string, () => void>();\n const handledRequests = new Set<string>();\n let catalogSignature = '';\n\n const publishSelection = (payload: BrowserVoiceOwnershipPayload): void => {\n for (const scope of scopes.values()) host.publish(scope.sessionId, payload);\n };\n\n const sendCommand = async (sessionId: string, command: VoiceOwnershipCommand) => {\n const scope = scopes.get(sessionId);\n if (scope === undefined) throw new Error(`Voice session ${sessionId} is unavailable.`);\n const response = await host.requestSessionApi(scope, {\n basePath: VOICE_MEDIA_API_BASE_PATH,\n path: VOICE_OWNERSHIP_ROUTES.command,\n method: 'POST',\n body: JSON.stringify(command),\n signal: AbortSignal.timeout(VOICE_OWNERSHIP_COMMAND_TIMEOUT_MS),\n });\n const acknowledgement = response.ok ? parseVoiceOwnershipAcknowledgement(await response.json()) : undefined;\n if (acknowledgement === undefined)\n throw new Error(`Voice ownership command failed with HTTP ${response.status}.`);\n return acknowledgement;\n };\n\n const coordinator = new VoiceOwnershipCoordinator({ send: sendCommand }, publishSelection, {\n now: () => Date.now(),\n createId: () => globalThis.crypto.randomUUID(),\n });\n\n const remember = (key: string): boolean => {\n if (handledRequests.has(key)) return false;\n handledRequests.add(key);\n if (handledRequests.size > MAX_HANDLED_REQUESTS) {\n const oldest = handledRequests.values().next().value;\n if (oldest !== undefined) handledRequests.delete(oldest);\n }\n return true;\n };\n\n const refreshCatalogs = async (): Promise<void> => {\n const nextSignature = [...scopes.keys()]\n .sort((left, right) => left.localeCompare(right))\n .map((sessionId) => `${sessionId}:${JSON.stringify(coordinator.catalog(sessionId))}`)\n .join('|');\n if (nextSignature === catalogSignature) return;\n await coordinator.publishCatalogs();\n catalogSignature = nextSignature;\n };\n\n const processSnapshot = async (sessionId: string, snapshot: VoiceOwnershipSessionSnapshot): Promise<void> => {\n if (snapshot.handoff !== undefined) {\n const key = `${sessionId}:handoff:${snapshot.handoff.requestId}`;\n if (!remember(key)) return;\n try {\n if (!(await coordinator.handoff(sessionId, snapshot.handoff.handle)))\n host.onNotice(`voice handoff requested by ${sessionId} was rejected`);\n } catch (error) {\n host.onNotice(\n `voice handoff requested by ${sessionId} failed (${error instanceof Error ? error.message : String(error)})`,\n );\n }\n return;\n }\n if (snapshot.activation === undefined) return;\n const key = `${sessionId}:activate:${snapshot.activation.requestId}`;\n if (!remember(key)) return;\n try {\n if (!(await coordinator.activate(sessionId)))\n host.onNotice(`voice activation requested by ${sessionId} was rejected`);\n } catch (error) {\n host.onNotice(\n `voice activation requested by ${sessionId} failed (${error instanceof Error ? error.message : String(error)})`,\n );\n }\n };\n\n const applySnapshot = (sessionId: string, value: unknown): void => {\n const snapshot = parseVoiceOwnershipSessionSnapshot(value);\n if (snapshot?.registration === undefined) {\n coordinator.remove(sessionId);\n void refreshCatalogs().catch((error: unknown) =>\n host.onNotice(\n `voice ownership catalog update failed (${error instanceof Error ? error.message : String(error)})`,\n ),\n );\n return;\n }\n coordinator.update(sessionId, snapshot.registration);\n void processSnapshot(sessionId, snapshot);\n void refreshCatalogs().catch((error: unknown) =>\n host.onNotice(\n `voice ownership catalog update failed (${error instanceof Error ? error.message : String(error)})`,\n ),\n );\n };\n\n const source: DoomHubChannelSource = {\n payloadFor() {\n return coordinator.payload();\n },\n sessionAdded(scope) {\n scopes.set(scope.sessionId, scope);\n subscriptions.get(scope.sessionId)?.();\n const unsubscribe = host.directEvents.subscribe(\n VOICE_OWNERSHIP_FRAME_TYPE,\n scope.sessionId,\n (payload) => applySnapshot(scope.sessionId, payload),\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n void refreshCatalogs().catch((error: unknown) =>\n host.onNotice(\n `voice ownership catalog update failed (${error instanceof Error ? error.message : String(error)})`,\n ),\n );\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n scopes.delete(sessionId);\n coordinator.remove(sessionId);\n catalogSignature = '';\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n scopes.clear();\n handledRequests.clear();\n },\n };\n return source;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"voiceMediaHubChannel.mjs","names":[],"sources":["../../../src/controllers/voiceMediaHubChannel.ts"],"sourcesContent":["import type { DoomHubChannel, DoomHubChannelSource, DoomHubSessionScope } from '@agimon-ai/doompi-core/hub-channel';\n\nimport { VoiceOwnershipCoordinator } from '../services/voiceOwnershipCoordinator';\nimport { VOICE_MEDIA_API_BASE_PATH, VOICE_MEDIA_WAKE_TYPE, type VoiceMediaWake } from '../types/clientMedia';\nimport {\n VOICE_OWNERSHIP_COMMAND_TIMEOUT_MS,\n VOICE_OWNERSHIP_FRAME_TYPE,\n VOICE_OWNERSHIP_ROUTES,\n parseVoiceOwnershipAcknowledgement,\n parseVoiceOwnershipSessionSnapshot,\n type BrowserVoiceOwnershipPayload,\n type VoiceOwnershipCommand,\n type VoiceOwnershipSessionSnapshot,\n} from '../types/voiceOwnership';\n\nconst MAX_HANDLED_REQUESTS = 512;\nconst MAX_EVENT_EPOCH_LENGTH = 200;\n\nfunction parseVoiceMediaWake(value: unknown): VoiceMediaWake | undefined {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;\n const record = value as Record<string, unknown>;\n if (\n Object.keys(record).sort().join(',') !== 'eventEpoch,sequence' ||\n typeof record.eventEpoch !== 'string' ||\n record.eventEpoch.length === 0 ||\n record.eventEpoch.length > MAX_EVENT_EPOCH_LENGTH ||\n !Number.isSafeInteger(record.sequence) ||\n (record.sequence as number) < 0\n )\n return undefined;\n return { eventEpoch: record.eventEpoch, sequence: record.sequence as number };\n}\n\n/** Publishes session-owned media wakeups through the host's direct event bus. */\nexport function createVoiceMediaWakeChannel(): DoomHubChannel {\n return {\n frameType: VOICE_MEDIA_WAKE_TYPE,\n start(host) {\n const latest = new Map<string, VoiceMediaWake>();\n const subscriptions = new Map<string, () => void>();\n return {\n payloadFor(scope) {\n return latest.get(scope.sessionId);\n },\n sessionAdded(scope) {\n subscriptions.get(scope.sessionId)?.();\n const unsubscribe = host.directEvents.subscribe(\n VOICE_MEDIA_WAKE_TYPE,\n scope.sessionId,\n (payload) => {\n const wake = parseVoiceMediaWake(payload);\n if (wake === undefined) return;\n const current = latest.get(scope.sessionId);\n if (current?.eventEpoch === wake.eventEpoch && wake.sequence <= current.sequence) return;\n latest.set(scope.sessionId, wake);\n host.publish(scope.sessionId, wake);\n },\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n latest.delete(sessionId);\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n latest.clear();\n },\n } satisfies DoomHubChannelSource;\n },\n };\n}\n\n/** Coordinates browser voice ownership from session lifecycle events. */\nexport function createVoiceOwnershipChannel(): DoomHubChannel {\n return {\n frameType: VOICE_OWNERSHIP_FRAME_TYPE,\n lifecycle: 'hub',\n start(host) {\n const scopes = new Map<string, DoomHubSessionScope>();\n const subscriptions = new Map<string, () => void>();\n const handledRequests = new Set<string>();\n let catalogSignature = '';\n let catalogRun: Promise<void> | undefined;\n\n const publishSelection = (payload: BrowserVoiceOwnershipPayload): void => {\n for (const scope of scopes.values()) host.publish(scope.sessionId, payload);\n };\n\n const sendCommand = async (sessionId: string, command: VoiceOwnershipCommand) => {\n const scope = scopes.get(sessionId);\n if (scope === undefined) throw new Error(`Voice session ${sessionId} is unavailable.`);\n const response = await host.requestSessionApi(scope, {\n basePath: VOICE_MEDIA_API_BASE_PATH,\n path: VOICE_OWNERSHIP_ROUTES.command,\n method: 'POST',\n body: JSON.stringify(command),\n signal: AbortSignal.timeout(VOICE_OWNERSHIP_COMMAND_TIMEOUT_MS),\n });\n const acknowledgement = response.ok ? parseVoiceOwnershipAcknowledgement(await response.json()) : undefined;\n if (acknowledgement === undefined)\n throw new Error(`Voice ownership command failed with HTTP ${response.status}.`);\n return acknowledgement;\n };\n\n const coordinator = new VoiceOwnershipCoordinator({ send: sendCommand }, publishSelection, {\n now: () => Date.now(),\n createId: () => globalThis.crypto.randomUUID(),\n });\n\n const remember = (key: string): boolean => {\n if (handledRequests.has(key)) return false;\n handledRequests.add(key);\n if (handledRequests.size > MAX_HANDLED_REQUESTS) {\n const oldest = handledRequests.values().next().value;\n if (oldest !== undefined) handledRequests.delete(oldest);\n }\n return true;\n };\n\n const publishCatalogsOnce = async (): Promise<void> => {\n const nextSignature = [...scopes.keys()]\n .sort((left, right) => left.localeCompare(right))\n .map((sessionId) => `${sessionId}:${JSON.stringify(coordinator.catalog(sessionId))}`)\n .join('|');\n if (nextSignature === catalogSignature) return;\n await coordinator.publishCatalogs();\n catalogSignature = nextSignature;\n };\n\n /**\n * Serialises overlapping catalog refreshes so concurrent callers cannot both pass the\n * signature guard and publish the same state twice. Waiters do not share the in-flight run:\n * each takes its own turn afterwards and the signature guard in publishCatalogsOnce makes\n * that turn a no-op unless the state actually changed mid-flight, which is the trailing edge.\n */\n const refreshCatalogs = async (): Promise<void> => {\n // The run in flight reports its own failure to its own caller; a waiter only needs its turn.\n while (catalogRun !== undefined) await catalogRun.catch(() => undefined);\n const run = publishCatalogsOnce();\n catalogRun = run;\n try {\n await run;\n } finally {\n if (catalogRun === run) catalogRun = undefined;\n }\n };\n\n const processSnapshot = async (sessionId: string, snapshot: VoiceOwnershipSessionSnapshot): Promise<void> => {\n if (snapshot.handoff !== undefined) {\n const key = `${sessionId}:handoff:${snapshot.handoff.requestId}`;\n if (!remember(key)) return;\n try {\n if (!(await coordinator.handoff(sessionId, snapshot.handoff.handle)))\n host.onNotice(`voice handoff requested by ${sessionId} was rejected`);\n } catch (error) {\n host.onNotice(\n `voice handoff requested by ${sessionId} failed (${error instanceof Error ? error.message : String(error)})`,\n );\n }\n return;\n }\n if (snapshot.activation === undefined) return;\n const key = `${sessionId}:activate:${snapshot.activation.requestId}`;\n if (!remember(key)) return;\n try {\n if (!(await coordinator.activate(sessionId)))\n host.onNotice(`voice activation requested by ${sessionId} was rejected`);\n } catch (error) {\n host.onNotice(\n `voice activation requested by ${sessionId} failed (${error instanceof Error ? error.message : String(error)})`,\n );\n }\n };\n\n const applySnapshot = (sessionId: string, value: unknown): void => {\n const snapshot = parseVoiceOwnershipSessionSnapshot(value);\n if (snapshot?.registration === undefined) {\n coordinator.remove(sessionId);\n void refreshCatalogs().catch((error: unknown) =>\n host.onNotice(\n `voice ownership catalog update failed (${error instanceof Error ? error.message : String(error)})`,\n ),\n );\n return;\n }\n coordinator.update(sessionId, snapshot.registration);\n void processSnapshot(sessionId, snapshot);\n void refreshCatalogs().catch((error: unknown) =>\n host.onNotice(\n `voice ownership catalog update failed (${error instanceof Error ? error.message : String(error)})`,\n ),\n );\n };\n\n const source: DoomHubChannelSource = {\n payloadFor() {\n return coordinator.payload();\n },\n sessionAdded(scope) {\n scopes.set(scope.sessionId, scope);\n subscriptions.get(scope.sessionId)?.();\n const unsubscribe = host.directEvents.subscribe(\n VOICE_OWNERSHIP_FRAME_TYPE,\n scope.sessionId,\n (payload) => applySnapshot(scope.sessionId, payload),\n { replayLatest: true },\n );\n subscriptions.set(scope.sessionId, unsubscribe);\n void refreshCatalogs().catch((error: unknown) =>\n host.onNotice(\n `voice ownership catalog update failed (${error instanceof Error ? error.message : String(error)})`,\n ),\n );\n },\n sessionRemoved(sessionId) {\n subscriptions.get(sessionId)?.();\n subscriptions.delete(sessionId);\n scopes.delete(sessionId);\n coordinator.remove(sessionId);\n catalogSignature = '';\n },\n close() {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n scopes.clear();\n handledRequests.clear();\n },\n };\n return source;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../services/infrastructure/index.cjs"),t=require("../services/whisper/index.cjs"),n=require("./clientMediaApi.cjs");require("../types/manualTranscription.cjs");const r=require("../services/encodedAudio/index.cjs"),i=require("../services/manualTranscription/index.cjs"),a=require("../services/manualTranscriptionConfig/index.cjs"),o=require("./manualTranscriptionApi.cjs");var s=class{media;manual;constructor(e,t){this.media=e,this.manual=new o.ManualTranscriptionApi(t)}fetch(e){return new URL(e.url).pathname===`/manual/transcribe`?this.manual.fetch(e):this.media.fetch(e)}close(){this.manual.close(),this.media.close()}};function c(n,o){let s=new e.ExecutableResolver,c=new e.NodeProcessSpawner,l=new t.TranscriberRegistry(new t.WhisperCppAdapter(s,c),new t.OpenAiWhisperAdapter(s,c),new t.MlxWhisperAdapter(s,c));return new i.ManualTranscriptionService(new a.ManualTranscriptionConfigLoader(n,o),new r.FfmpegEncodedAudioDecoder(s,c),l,new e.TemporaryWorkspace,new e.SystemClock)}function l(e){let{manualTranscription:t,projectRoot:r=process.cwd(),homeDirectory:i,media:a,...o}=e;return new s(a??n.createVoiceMediaApi(o),t??c(r,i))}exports.createVoiceSessionApi=l;
|
|
1
|
+
require("../types/clientMedia.cjs");const e=require("../services/infrastructure/index.cjs"),t=require("../services/whisper/index.cjs"),n=require("./clientMediaApi.cjs");require("../types/manualTranscription.cjs");const r=require("../services/encodedAudio/index.cjs"),i=require("../services/manualTranscription/index.cjs"),a=require("../services/manualTranscriptionConfig/index.cjs"),o=require("./manualTranscriptionApi.cjs");var s=class{media;manual;constructor(e,t){this.media=e,this.manual=new o.ManualTranscriptionApi(t)}fetch(e){return new URL(e.url).pathname===`/manual/transcribe`?this.manual.fetch(e):this.media.fetch(e)}close(){this.manual.close(),this.media.close()}};function c(n,o){let s=new e.ExecutableResolver,c=new e.NodeProcessSpawner,l=new t.TranscriberRegistry(new t.WhisperCppAdapter(s,c),new t.OpenAiWhisperAdapter(s,c),new t.MlxWhisperAdapter(s,c));return new i.ManualTranscriptionService(new a.ManualTranscriptionConfigLoader(n,o),new r.FfmpegEncodedAudioDecoder(s,c),l,new e.TemporaryWorkspace,new e.SystemClock)}function l(e){let{manualTranscription:t,projectRoot:r=process.cwd(),homeDirectory:i,media:a,...o}=e;return new s(a??n.createVoiceMediaApi(o),t??c(r,i))}exports.createVoiceSessionApi=l;
|
|
2
2
|
//# sourceMappingURL=voiceSessionApi.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voiceSessionApi.cjs","names":["ManualTranscriptionApi","ExecutableResolver","NodeProcessSpawner","TranscriberRegistry","WhisperCppAdapter","OpenAiWhisperAdapter","MlxWhisperAdapter","ManualTranscriptionService","ManualTranscriptionConfigLoader","FfmpegEncodedAudioDecoder","TemporaryWorkspace","SystemClock","createVoiceMediaApi"],"sources":["../../../src/controllers/voiceSessionApi.ts"],"sourcesContent":["import type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-core/package-api';\n\nimport { FfmpegEncodedAudioDecoder } from '../services/encodedAudio';\nimport { ExecutableResolver, NodeProcessSpawner, SystemClock, TemporaryWorkspace } from '../services/infrastructure';\nimport { ManualTranscriptionService } from '../services/manualTranscription';\nimport { ManualTranscriptionConfigLoader } from '../services/manualTranscriptionConfig';\nimport { MlxWhisperAdapter, OpenAiWhisperAdapter, TranscriberRegistry, WhisperCppAdapter } from '../services/whisper';\nimport { VOICE_MEDIA_API_BASE_PATH } from '../types/clientMedia';\nimport { MANUAL_TRANSCRIPTION_ROUTE, type IManualTranscriptionService } from '../types/manualTranscription';\nimport { api as voiceMediaApi, createVoiceMediaApi, type VoiceMediaApiOptions } from './clientMediaApi';\nimport { ManualTranscriptionApi } from './manualTranscriptionApi';\n\nexport interface VoiceSessionApiOptions extends VoiceMediaApiOptions {\n manualTranscription?: IManualTranscriptionService;\n projectRoot?: string;\n homeDirectory?: string;\n media?: DoomApiHandler;\n}\n\nclass VoiceSessionApi implements DoomApiHandler {\n private readonly manual: ManualTranscriptionApi;\n\n public constructor(\n private readonly media: DoomApiHandler,\n service: IManualTranscriptionService,\n ) {\n this.manual = new ManualTranscriptionApi(service);\n }\n\n public fetch(request: Request): Response | Promise<Response> {\n return new URL(request.url).pathname === MANUAL_TRANSCRIPTION_ROUTE\n ? this.manual.fetch(request)\n : this.media.fetch(request);\n }\n\n public close(): void {\n this.manual.close();\n this.media.close();\n }\n}\n\nfunction createDefaultManualTranscriptionService(\n projectRoot: string,\n homeDirectory?: string,\n): IManualTranscriptionService {\n const executables = new ExecutableResolver();\n const spawner = new NodeProcessSpawner();\n const registry = new TranscriberRegistry(\n new WhisperCppAdapter(executables, spawner),\n new OpenAiWhisperAdapter(executables, spawner),\n new MlxWhisperAdapter(executables, spawner),\n );\n return new ManualTranscriptionService(\n new ManualTranscriptionConfigLoader(projectRoot, homeDirectory),\n new FfmpegEncodedAudioDecoder(executables, spawner),\n registry,\n new TemporaryWorkspace(),\n new SystemClock(),\n );\n}\n\nexport function createVoiceSessionApi(options: VoiceSessionApiOptions): DoomApiHandler {\n const { manualTranscription, projectRoot = process.cwd(), homeDirectory, media, ...mediaOptions } = options;\n return new VoiceSessionApi(\n media ?? createVoiceMediaApi(mediaOptions),\n manualTranscription ?? createDefaultManualTranscriptionService(projectRoot, homeDirectory),\n );\n}\n\nexport const api: DoomApi = {\n basePath: VOICE_MEDIA_API_BASE_PATH,\n start(context: DoomApiContext): DoomApiHandler {\n return new VoiceSessionApi(\n voiceMediaApi.start(context),\n createDefaultManualTranscriptionService(context.cwd ?? process.cwd(), context.homeDirectory),\n );\n },\n};\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"voiceSessionApi.cjs","names":["ManualTranscriptionApi","ExecutableResolver","NodeProcessSpawner","TranscriberRegistry","WhisperCppAdapter","OpenAiWhisperAdapter","MlxWhisperAdapter","ManualTranscriptionService","ManualTranscriptionConfigLoader","FfmpegEncodedAudioDecoder","TemporaryWorkspace","SystemClock","createVoiceMediaApi"],"sources":["../../../src/controllers/voiceSessionApi.ts"],"sourcesContent":["import type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-core/package-api';\n\nimport { FfmpegEncodedAudioDecoder } from '../services/encodedAudio';\nimport { ExecutableResolver, NodeProcessSpawner, SystemClock, TemporaryWorkspace } from '../services/infrastructure';\nimport { ManualTranscriptionService } from '../services/manualTranscription';\nimport { ManualTranscriptionConfigLoader } from '../services/manualTranscriptionConfig';\nimport { MlxWhisperAdapter, OpenAiWhisperAdapter, TranscriberRegistry, WhisperCppAdapter } from '../services/whisper';\nimport { VOICE_MEDIA_API_BASE_PATH } from '../types/clientMedia';\nimport { MANUAL_TRANSCRIPTION_ROUTE, type IManualTranscriptionService } from '../types/manualTranscription';\nimport { api as voiceMediaApi, createVoiceMediaApi, type VoiceMediaApiOptions } from './clientMediaApi';\nimport { ManualTranscriptionApi } from './manualTranscriptionApi';\n\nexport interface VoiceSessionApiOptions extends VoiceMediaApiOptions {\n manualTranscription?: IManualTranscriptionService;\n projectRoot?: string;\n homeDirectory?: string;\n media?: DoomApiHandler;\n}\n\nclass VoiceSessionApi implements DoomApiHandler {\n private readonly manual: ManualTranscriptionApi;\n\n public constructor(\n private readonly media: DoomApiHandler,\n service: IManualTranscriptionService,\n ) {\n this.manual = new ManualTranscriptionApi(service);\n }\n\n public fetch(request: Request): Response | Promise<Response> {\n return new URL(request.url).pathname === MANUAL_TRANSCRIPTION_ROUTE\n ? this.manual.fetch(request)\n : this.media.fetch(request);\n }\n\n public close(): void {\n this.manual.close();\n this.media.close();\n }\n}\n\nfunction createDefaultManualTranscriptionService(\n projectRoot: string,\n homeDirectory?: string,\n): IManualTranscriptionService {\n const executables = new ExecutableResolver();\n const spawner = new NodeProcessSpawner();\n const registry = new TranscriberRegistry(\n new WhisperCppAdapter(executables, spawner),\n new OpenAiWhisperAdapter(executables, spawner),\n new MlxWhisperAdapter(executables, spawner),\n );\n return new ManualTranscriptionService(\n new ManualTranscriptionConfigLoader(projectRoot, homeDirectory),\n new FfmpegEncodedAudioDecoder(executables, spawner),\n registry,\n new TemporaryWorkspace(),\n new SystemClock(),\n );\n}\n\nexport function createVoiceSessionApi(options: VoiceSessionApiOptions): DoomApiHandler {\n const { manualTranscription, projectRoot = process.cwd(), homeDirectory, media, ...mediaOptions } = options;\n return new VoiceSessionApi(\n media ?? createVoiceMediaApi(mediaOptions),\n manualTranscription ?? createDefaultManualTranscriptionService(projectRoot, homeDirectory),\n );\n}\n\nexport const api: DoomApi = {\n basePath: VOICE_MEDIA_API_BASE_PATH,\n start(context: DoomApiContext): DoomApiHandler {\n return new VoiceSessionApi(\n voiceMediaApi.start(context),\n createDefaultManualTranscriptionService(context.cwd ?? process.cwd(), context.homeDirectory),\n );\n },\n};\n"],"mappings":"yaAmBA,IAAM,EAAN,KAAgD,CAI3B,MAHnB,OAEA,YACE,EACA,EACA,CAFiB,KAAA,MAAA,EAGjB,KAAK,OAAS,IAAIA,EAAAA,uBAAuB,CAAO,CAClD,CAEA,MAAa,EAAgD,CAC3D,OAAO,IAAI,IAAI,EAAQ,GAAG,CAAC,CAAC,WAAA,qBACxB,KAAK,OAAO,MAAM,CAAO,EACzB,KAAK,MAAM,MAAM,CAAO,CAC9B,CAEA,OAAqB,CACnB,KAAK,OAAO,MAAM,EAClB,KAAK,MAAM,MAAM,CACnB,CACF,EAEA,SAAS,EACP,EACA,EAC6B,CAC7B,IAAM,EAAc,IAAIC,EAAAA,mBAClB,EAAU,IAAIC,EAAAA,mBACd,EAAW,IAAIC,EAAAA,oBACnB,IAAIC,EAAAA,kBAAkB,EAAa,CAAO,EAC1C,IAAIC,EAAAA,qBAAqB,EAAa,CAAO,EAC7C,IAAIC,EAAAA,kBAAkB,EAAa,CAAO,CAC5C,EACA,OAAO,IAAIC,EAAAA,2BACT,IAAIC,EAAAA,gCAAgC,EAAa,CAAa,EAC9D,IAAIC,EAAAA,0BAA0B,EAAa,CAAO,EAClD,EACA,IAAIC,EAAAA,mBACJ,IAAIC,EAAAA,WACN,CACF,CAEA,SAAgB,EAAsB,EAAiD,CACrF,GAAM,CAAE,sBAAqB,cAAc,QAAQ,IAAI,EAAG,gBAAe,QAAO,GAAG,GAAiB,EACpG,OAAO,IAAI,EACT,GAASC,EAAAA,oBAAoB,CAAY,EACzC,GAAuB,EAAwC,EAAa,CAAa,CAC3F,CACF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{ExecutableResolver as e,NodeProcessSpawner as t,SystemClock as n,TemporaryWorkspace as r}from"../services/infrastructure/index.mjs";import{MlxWhisperAdapter as i,OpenAiWhisperAdapter as a,TranscriberRegistry as o,WhisperCppAdapter as s}from"../services/whisper/index.mjs";import{createVoiceMediaApi as c}from"./clientMediaApi.mjs";import"../types/manualTranscription.mjs";import{FfmpegEncodedAudioDecoder as l}from"../services/encodedAudio/index.mjs";import{ManualTranscriptionService as u}from"../services/manualTranscription/index.mjs";import{ManualTranscriptionConfigLoader as d}from"../services/manualTranscriptionConfig/index.mjs";import{ManualTranscriptionApi as f}from"./manualTranscriptionApi.mjs";var p=class{media;manual;constructor(e,t){this.media=e,this.manual=new f(t)}fetch(e){return new URL(e.url).pathname===`/manual/transcribe`?this.manual.fetch(e):this.media.fetch(e)}close(){this.manual.close(),this.media.close()}};function m(c,f){let p=new e,m=new t,h=new o(new s(p,m),new a(p,m),new i(p,m));return new u(new d(c,f),new l(p,m),h,new r,new n)}function h(e){let{manualTranscription:t,projectRoot:n=process.cwd(),homeDirectory:r,media:i,...a}=e;return new p(i??c(a),t??m(n,r))}export{h as createVoiceSessionApi};
|
|
1
|
+
import"../types/clientMedia.mjs";import{ExecutableResolver as e,NodeProcessSpawner as t,SystemClock as n,TemporaryWorkspace as r}from"../services/infrastructure/index.mjs";import{MlxWhisperAdapter as i,OpenAiWhisperAdapter as a,TranscriberRegistry as o,WhisperCppAdapter as s}from"../services/whisper/index.mjs";import{createVoiceMediaApi as c}from"./clientMediaApi.mjs";import"../types/manualTranscription.mjs";import{FfmpegEncodedAudioDecoder as l}from"../services/encodedAudio/index.mjs";import{ManualTranscriptionService as u}from"../services/manualTranscription/index.mjs";import{ManualTranscriptionConfigLoader as d}from"../services/manualTranscriptionConfig/index.mjs";import{ManualTranscriptionApi as f}from"./manualTranscriptionApi.mjs";var p=class{media;manual;constructor(e,t){this.media=e,this.manual=new f(t)}fetch(e){return new URL(e.url).pathname===`/manual/transcribe`?this.manual.fetch(e):this.media.fetch(e)}close(){this.manual.close(),this.media.close()}};function m(c,f){let p=new e,m=new t,h=new o(new s(p,m),new a(p,m),new i(p,m));return new u(new d(c,f),new l(p,m),h,new r,new n)}function h(e){let{manualTranscription:t,projectRoot:n=process.cwd(),homeDirectory:r,media:i,...a}=e;return new p(i??c(a),t??m(n,r))}export{h as createVoiceSessionApi};
|
|
2
2
|
//# sourceMappingURL=voiceSessionApi.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voiceSessionApi.mjs","names":[],"sources":["../../../src/controllers/voiceSessionApi.ts"],"sourcesContent":["import type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-core/package-api';\n\nimport { FfmpegEncodedAudioDecoder } from '../services/encodedAudio';\nimport { ExecutableResolver, NodeProcessSpawner, SystemClock, TemporaryWorkspace } from '../services/infrastructure';\nimport { ManualTranscriptionService } from '../services/manualTranscription';\nimport { ManualTranscriptionConfigLoader } from '../services/manualTranscriptionConfig';\nimport { MlxWhisperAdapter, OpenAiWhisperAdapter, TranscriberRegistry, WhisperCppAdapter } from '../services/whisper';\nimport { VOICE_MEDIA_API_BASE_PATH } from '../types/clientMedia';\nimport { MANUAL_TRANSCRIPTION_ROUTE, type IManualTranscriptionService } from '../types/manualTranscription';\nimport { api as voiceMediaApi, createVoiceMediaApi, type VoiceMediaApiOptions } from './clientMediaApi';\nimport { ManualTranscriptionApi } from './manualTranscriptionApi';\n\nexport interface VoiceSessionApiOptions extends VoiceMediaApiOptions {\n manualTranscription?: IManualTranscriptionService;\n projectRoot?: string;\n homeDirectory?: string;\n media?: DoomApiHandler;\n}\n\nclass VoiceSessionApi implements DoomApiHandler {\n private readonly manual: ManualTranscriptionApi;\n\n public constructor(\n private readonly media: DoomApiHandler,\n service: IManualTranscriptionService,\n ) {\n this.manual = new ManualTranscriptionApi(service);\n }\n\n public fetch(request: Request): Response | Promise<Response> {\n return new URL(request.url).pathname === MANUAL_TRANSCRIPTION_ROUTE\n ? this.manual.fetch(request)\n : this.media.fetch(request);\n }\n\n public close(): void {\n this.manual.close();\n this.media.close();\n }\n}\n\nfunction createDefaultManualTranscriptionService(\n projectRoot: string,\n homeDirectory?: string,\n): IManualTranscriptionService {\n const executables = new ExecutableResolver();\n const spawner = new NodeProcessSpawner();\n const registry = new TranscriberRegistry(\n new WhisperCppAdapter(executables, spawner),\n new OpenAiWhisperAdapter(executables, spawner),\n new MlxWhisperAdapter(executables, spawner),\n );\n return new ManualTranscriptionService(\n new ManualTranscriptionConfigLoader(projectRoot, homeDirectory),\n new FfmpegEncodedAudioDecoder(executables, spawner),\n registry,\n new TemporaryWorkspace(),\n new SystemClock(),\n );\n}\n\nexport function createVoiceSessionApi(options: VoiceSessionApiOptions): DoomApiHandler {\n const { manualTranscription, projectRoot = process.cwd(), homeDirectory, media, ...mediaOptions } = options;\n return new VoiceSessionApi(\n media ?? createVoiceMediaApi(mediaOptions),\n manualTranscription ?? createDefaultManualTranscriptionService(projectRoot, homeDirectory),\n );\n}\n\nexport const api: DoomApi = {\n basePath: VOICE_MEDIA_API_BASE_PATH,\n start(context: DoomApiContext): DoomApiHandler {\n return new VoiceSessionApi(\n voiceMediaApi.start(context),\n createDefaultManualTranscriptionService(context.cwd ?? process.cwd(), context.homeDirectory),\n );\n },\n};\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"voiceSessionApi.mjs","names":[],"sources":["../../../src/controllers/voiceSessionApi.ts"],"sourcesContent":["import type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-core/package-api';\n\nimport { FfmpegEncodedAudioDecoder } from '../services/encodedAudio';\nimport { ExecutableResolver, NodeProcessSpawner, SystemClock, TemporaryWorkspace } from '../services/infrastructure';\nimport { ManualTranscriptionService } from '../services/manualTranscription';\nimport { ManualTranscriptionConfigLoader } from '../services/manualTranscriptionConfig';\nimport { MlxWhisperAdapter, OpenAiWhisperAdapter, TranscriberRegistry, WhisperCppAdapter } from '../services/whisper';\nimport { VOICE_MEDIA_API_BASE_PATH } from '../types/clientMedia';\nimport { MANUAL_TRANSCRIPTION_ROUTE, type IManualTranscriptionService } from '../types/manualTranscription';\nimport { api as voiceMediaApi, createVoiceMediaApi, type VoiceMediaApiOptions } from './clientMediaApi';\nimport { ManualTranscriptionApi } from './manualTranscriptionApi';\n\nexport interface VoiceSessionApiOptions extends VoiceMediaApiOptions {\n manualTranscription?: IManualTranscriptionService;\n projectRoot?: string;\n homeDirectory?: string;\n media?: DoomApiHandler;\n}\n\nclass VoiceSessionApi implements DoomApiHandler {\n private readonly manual: ManualTranscriptionApi;\n\n public constructor(\n private readonly media: DoomApiHandler,\n service: IManualTranscriptionService,\n ) {\n this.manual = new ManualTranscriptionApi(service);\n }\n\n public fetch(request: Request): Response | Promise<Response> {\n return new URL(request.url).pathname === MANUAL_TRANSCRIPTION_ROUTE\n ? this.manual.fetch(request)\n : this.media.fetch(request);\n }\n\n public close(): void {\n this.manual.close();\n this.media.close();\n }\n}\n\nfunction createDefaultManualTranscriptionService(\n projectRoot: string,\n homeDirectory?: string,\n): IManualTranscriptionService {\n const executables = new ExecutableResolver();\n const spawner = new NodeProcessSpawner();\n const registry = new TranscriberRegistry(\n new WhisperCppAdapter(executables, spawner),\n new OpenAiWhisperAdapter(executables, spawner),\n new MlxWhisperAdapter(executables, spawner),\n );\n return new ManualTranscriptionService(\n new ManualTranscriptionConfigLoader(projectRoot, homeDirectory),\n new FfmpegEncodedAudioDecoder(executables, spawner),\n registry,\n new TemporaryWorkspace(),\n new SystemClock(),\n );\n}\n\nexport function createVoiceSessionApi(options: VoiceSessionApiOptions): DoomApiHandler {\n const { manualTranscription, projectRoot = process.cwd(), homeDirectory, media, ...mediaOptions } = options;\n return new VoiceSessionApi(\n media ?? createVoiceMediaApi(mediaOptions),\n manualTranscription ?? createDefaultManualTranscriptionService(projectRoot, homeDirectory),\n );\n}\n\nexport const api: DoomApi = {\n basePath: VOICE_MEDIA_API_BASE_PATH,\n start(context: DoomApiContext): DoomApiHandler {\n return new VoiceSessionApi(\n voiceMediaApi.start(context),\n createDefaultManualTranscriptionService(context.cwd ?? process.cwd(), context.homeDirectory),\n );\n },\n};\n"],"mappings":"0uBAmBA,IAAM,EAAN,KAAgD,CAI3B,MAHnB,OAEA,YACE,EACA,EACA,CAFiB,KAAA,MAAA,EAGjB,KAAK,OAAS,IAAI,EAAuB,CAAO,CAClD,CAEA,MAAa,EAAgD,CAC3D,OAAO,IAAI,IAAI,EAAQ,GAAG,CAAC,CAAC,WAAA,qBACxB,KAAK,OAAO,MAAM,CAAO,EACzB,KAAK,MAAM,MAAM,CAAO,CAC9B,CAEA,OAAqB,CACnB,KAAK,OAAO,MAAM,EAClB,KAAK,MAAM,MAAM,CACnB,CACF,EAEA,SAAS,EACP,EACA,EAC6B,CAC7B,IAAM,EAAc,IAAI,EAClB,EAAU,IAAI,EACd,EAAW,IAAI,EACnB,IAAI,EAAkB,EAAa,CAAO,EAC1C,IAAI,EAAqB,EAAa,CAAO,EAC7C,IAAI,EAAkB,EAAa,CAAO,CAC5C,EACA,OAAO,IAAI,EACT,IAAI,EAAgC,EAAa,CAAa,EAC9D,IAAI,EAA0B,EAAa,CAAO,EAClD,EACA,IAAI,EACJ,IAAI,CACN,CACF,CAEA,SAAgB,EAAsB,EAAiD,CACrF,GAAM,CAAE,sBAAqB,cAAc,QAAQ,IAAI,EAAG,gBAAe,QAAO,GAAG,GAAiB,EACpG,OAAO,IAAI,EACT,GAAS,EAAoB,CAAY,EACzC,GAAuB,EAAwC,EAAa,CAAa,CAC3F,CACF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const
|
|
1
|
+
let e=require("@agimon-ai/doompi-core/web");const t=`voice-media`,n={clientConnect:`/client/connect`,clientDisconnect:`/client/disconnect`,clientEvents:`/client/events`,clientHeartbeat:`/client/heartbeat`,clientAudio:`/client/audio`,clientCaptureStopped:`/client/capture-stopped`,clientPlaybackAudio:`/client/playback-audio`,clientPlaybackResult:`/client/playback-result`,hostCaptureStart:`/host/capture/start`,hostCaptureAudio:`/host/capture/audio`,hostCaptureStop:`/host/capture/stop`,hostCaptureAbort:`/host/capture/abort`,hostPlaybackStart:`/host/playback/start`,hostPlaybackAudio:`/host/playback/audio`,hostPlaybackAudioEnd:`/host/playback/audio-end`,hostPlaybackResult:`/host/playback/result`,hostPlaybackStop:`/host/playback/stop`,hostPlaybackAbort:`/host/playback/abort`};function r(n,r,i={}){let a=new URLSearchParams({session:n,...i});return`${(0,e.sessionApiPath)(n)}/plugins/${t}${r}?${a.toString()}`}exports.VOICE_MEDIA_ACTIVITY_ECHO_SPEECH_MS_HEADER=`x-doompi-voice-activity-echo-speech-ms`,exports.VOICE_MEDIA_ACTIVITY_ELAPSED_HEADER=`x-doompi-voice-activity-elapsed`,exports.VOICE_MEDIA_ACTIVITY_EPOCH_HEADER=`x-doompi-voice-activity-epoch`,exports.VOICE_MEDIA_ACTIVITY_LEVEL_HEADER=`x-doompi-voice-activity-level`,exports.VOICE_MEDIA_ACTIVITY_SPEECH_MS_HEADER=`x-doompi-voice-activity-speech-ms`,exports.VOICE_MEDIA_ACTIVITY_STATE_HEADER=`x-doompi-voice-activity-state`,exports.VOICE_MEDIA_API_BASE_PATH=t,exports.VOICE_MEDIA_BITS_PER_SAMPLE=16,exports.VOICE_MEDIA_CHANNELS=1,exports.VOICE_MEDIA_CONTENT_TYPE=`application/vnd.doompi.pcm-s16le`,exports.VOICE_MEDIA_EVENT_WAIT_NONE=`0`,exports.VOICE_MEDIA_HEARTBEAT_MS=5e3,exports.VOICE_MEDIA_PLAYBACK_STATE_HEADER=`x-doompi-playback-state`,exports.VOICE_MEDIA_PROTOCOL_VERSION=6,exports.VOICE_MEDIA_ROUTES=n,exports.VOICE_MEDIA_SAMPLE_RATE=16e3,exports.VOICE_MEDIA_WAKE_TYPE=`voice_media_wake`,exports.voiceMediaClientUrl=r;
|
|
2
2
|
//# sourceMappingURL=clientMedia.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clientMedia.cjs","names":[],"sources":["../../../src/types/clientMedia.ts"],"sourcesContent":["import type { SpeechPresenceDetector } from './clientCaptureActivity';\nimport type { RealtimeMediaCommand, RealtimeMediaTransport } from './realtime';\n\nexport const VOICE_MEDIA_API_BASE_PATH = 'voice-media';\nexport const VOICE_MEDIA_PROTOCOL_VERSION = 6;\nexport const VOICE_MEDIA_SAMPLE_RATE = 16_000;\nexport const VOICE_MEDIA_CHANNELS = 1;\nexport const VOICE_MEDIA_BITS_PER_SAMPLE = 16;\nexport const VOICE_MEDIA_CONTENT_TYPE = 'application/vnd.doompi.pcm-s16le';\nexport const VOICE_MEDIA_ACTIVITY_STATE_HEADER = 'x-doompi-voice-activity-state';\nexport const VOICE_MEDIA_PLAYBACK_STATE_HEADER = 'x-doompi-playback-state';\nexport const VOICE_MEDIA_ACTIVITY_LEVEL_HEADER = 'x-doompi-voice-activity-level';\nexport const VOICE_MEDIA_ACTIVITY_ELAPSED_HEADER = 'x-doompi-voice-activity-elapsed';\nexport const VOICE_MEDIA_ACTIVITY_EPOCH_HEADER = 'x-doompi-voice-activity-epoch';\nexport const VOICE_MEDIA_ACTIVITY_SPEECH_MS_HEADER = 'x-doompi-voice-activity-speech-ms';\nexport const VOICE_MEDIA_ACTIVITY_ECHO_SPEECH_MS_HEADER = 'x-doompi-voice-activity-echo-speech-ms';\nexport const VOICE_MEDIA_WAKE_TYPE = 'voice_media_wake';\nexport const VOICE_MEDIA_HEARTBEAT_MS = 5_000;\nexport const VOICE_MEDIA_EVENT_WAIT_NONE = '0';\n\nexport const VOICE_MEDIA_ROUTES = {\n clientConnect: '/client/connect',\n clientDisconnect: '/client/disconnect',\n clientEvents: '/client/events',\n clientHeartbeat: '/client/heartbeat',\n clientAudio: '/client/audio',\n clientCaptureStopped: '/client/capture-stopped',\n clientPlaybackAudio: '/client/playback-audio',\n clientPlaybackResult: '/client/playback-result',\n hostCaptureStart: '/host/capture/start',\n hostCaptureAudio: '/host/capture/audio',\n hostCaptureStop: '/host/capture/stop',\n hostCaptureAbort: '/host/capture/abort',\n hostPlaybackStart: '/host/playback/start',\n hostPlaybackAudio: '/host/playback/audio',\n hostPlaybackAudioEnd: '/host/playback/audio-end',\n hostPlaybackResult: '/host/playback/result',\n hostPlaybackStop: '/host/playback/stop',\n hostPlaybackAbort: '/host/playback/abort',\n} as const;\n\nexport type VoiceClientKind = 'browser' | 'native';\nexport type VoiceMediaControlLocation = 'local' | 'remote';\n\nexport interface VoiceMediaCapabilities {\n capture: boolean;\n playback: boolean;\n captureActivity: boolean;\n autonomousOrchestration: boolean;\n playbackDucking?: boolean;\n realtime?: boolean;\n}\n\nexport type VoiceMediaCaptureMode = 'manual' | 'autonomous';\nexport type VoiceMediaPlaybackDelivery = 'client' | 'streamed';\nexport type VoiceMediaCaptureActivityState = 'listening' | 'speech' | 'endpoint';\n\nexport interface VoiceMediaCaptureActivity {\n state: VoiceMediaCaptureActivityState;\n levelDbfs: number;\n elapsedMs: number;\n epoch?: number;\n classifiedSpeechMs?: number;\n echoDiscriminatedSpeechMs?: number;\n}\n\nexport interface VoiceMediaCaptureConfiguration {\n mode: VoiceMediaCaptureMode;\n activityControl: 'host' | 'client';\n endpointSilenceMs?: number;\n}\n\nexport interface VoiceMediaConnectRequest {\n version: typeof VOICE_MEDIA_PROTOCOL_VERSION;\n clientId: string;\n connectionId: string;\n clientKind: VoiceClientKind;\n controlLocation: VoiceMediaControlLocation;\n capabilities: VoiceMediaCapabilities;\n}\n\nexport interface VoiceMediaWake {\n eventEpoch: string;\n sequence: number;\n}\n\nexport interface VoiceMediaConnectResult {\n version: typeof VOICE_MEDIA_PROTOCOL_VERSION;\n cursor: number;\n eventEpoch?: string;\n heartbeatMs?: number;\n}\n\nexport type VoiceMediaHeartbeatResult = VoiceMediaWake;\n\nexport type VoiceMediaClientEvent =\n | RealtimeMediaCommand\n | {\n sequence: number;\n type: 'capture-start';\n captureId: string;\n sampleRate: typeof VOICE_MEDIA_SAMPLE_RATE;\n channels: typeof VOICE_MEDIA_CHANNELS;\n bitsPerSample: typeof VOICE_MEDIA_BITS_PER_SAMPLE;\n configuration: VoiceMediaCaptureConfiguration;\n }\n | { sequence: number; type: 'capture-stop'; captureId: string }\n | { sequence: number; type: 'capture-abort'; captureId: string }\n | {\n sequence: number;\n type: 'playback-start';\n playbackId: string;\n text: string;\n delivery?: VoiceMediaPlaybackDelivery;\n voice?: string;\n rate?: number;\n }\n | { sequence: number; type: 'playback-stop'; playbackId: string }\n | { sequence: number; type: 'playback-abort'; playbackId: string };\n\nexport type VoiceMediaPlaybackOutcome = 'completed' | 'stopped' | 'aborted' | 'failed';\n\nexport interface VoiceMediaPlaybackResult {\n playbackId: string;\n outcome: VoiceMediaPlaybackOutcome;\n error?: string;\n}\n\nexport interface VoiceMediaTransport extends Partial<RealtimeMediaTransport> {\n connect(\n clientId: string,\n connectionId: string,\n capabilities: VoiceMediaCapabilities,\n ): Promise<VoiceMediaConnectResult>;\n /** Refreshes optional capabilities without changing active work or event position. */\n refreshCapabilities?(clientId: string, connectionId: string, capabilities: VoiceMediaCapabilities): Promise<void>;\n disconnect(clientId: string, connectionId: string): Promise<void>;\n nextEvent(\n clientId: string,\n connectionId: string,\n after: number,\n signal: AbortSignal,\n ): Promise<VoiceMediaClientEvent | undefined>;\n sendAudio(\n clientId: string,\n connectionId: string,\n captureId: string,\n pcm: Uint8Array,\n activity?: VoiceMediaCaptureActivity,\n ): Promise<void>;\n captureStopped(clientId: string, connectionId: string, captureId: string, error?: string): Promise<void>;\n receivePlaybackAudio?(\n clientId: string,\n connectionId: string,\n playbackId: string,\n signal: AbortSignal,\n ): Promise<Uint8Array>;\n playbackFinished(clientId: string, connectionId: string, result: VoiceMediaPlaybackResult): Promise<void>;\n}\n\nexport interface VoiceMediaCapture {\n completion?: Promise<void>;\n stop(): Promise<void>;\n}\n\n/** Optional browser-local speech input. The original capture PCM remains authoritative for transport. */\nexport interface VoiceMediaCaptureSpeechAnalysis {\n speechPcm: Uint8Array;\n echoReferenceActive: boolean;\n echoDiscriminated: boolean;\n}\n\nexport interface VoiceMediaPlayback {\n readonly completion: Promise<VoiceMediaPlaybackResult>;\n stop(outcome: Extract<VoiceMediaPlaybackOutcome, 'stopped' | 'aborted'>): void;\n duck?(targetGain: number, fadeMs: number, holdMs: number): void;\n}\n\n/** Client hardware boundary. Browser and future native clients implement the same contract. */\nexport interface VoiceMediaDevice {\n readonly capabilities: VoiceMediaCapabilities;\n /** Initializes optional authoritative media processors after baseline capabilities are available. */\n prepare?(): Promise<void>;\n createSpeechPresenceDetector?(): SpeechPresenceDetector | undefined;\n startCapture(\n onPcm: (pcm: Uint8Array, speechAnalysis?: VoiceMediaCaptureSpeechAnalysis) => void,\n ): Promise<VoiceMediaCapture>;\n speak(\n request: Extract<VoiceMediaClientEvent, { type: 'playback-start' }>,\n audio?: Promise<Uint8Array>,\n ): VoiceMediaPlayback;\n close(): Promise<void>;\n}\n\nexport function voiceMediaClientUrl(sessionId: string, route: string, params: Record<string, string> = {}): string {\n const search = new URLSearchParams({ session: sessionId, ...params });\n return
|
|
1
|
+
{"version":3,"file":"clientMedia.cjs","names":["sessionApiPath"],"sources":["../../../src/types/clientMedia.ts"],"sourcesContent":["import { sessionApiPath } from '@agimon-ai/doompi-core/web';\n\nimport type { SpeechPresenceDetector } from './clientCaptureActivity';\nimport type { RealtimeMediaCommand, RealtimeMediaTransport } from './realtime';\n\nexport const VOICE_MEDIA_API_BASE_PATH = 'voice-media';\nexport const VOICE_MEDIA_PROTOCOL_VERSION = 6;\nexport const VOICE_MEDIA_SAMPLE_RATE = 16_000;\nexport const VOICE_MEDIA_CHANNELS = 1;\nexport const VOICE_MEDIA_BITS_PER_SAMPLE = 16;\nexport const VOICE_MEDIA_CONTENT_TYPE = 'application/vnd.doompi.pcm-s16le';\nexport const VOICE_MEDIA_ACTIVITY_STATE_HEADER = 'x-doompi-voice-activity-state';\nexport const VOICE_MEDIA_PLAYBACK_STATE_HEADER = 'x-doompi-playback-state';\nexport const VOICE_MEDIA_ACTIVITY_LEVEL_HEADER = 'x-doompi-voice-activity-level';\nexport const VOICE_MEDIA_ACTIVITY_ELAPSED_HEADER = 'x-doompi-voice-activity-elapsed';\nexport const VOICE_MEDIA_ACTIVITY_EPOCH_HEADER = 'x-doompi-voice-activity-epoch';\nexport const VOICE_MEDIA_ACTIVITY_SPEECH_MS_HEADER = 'x-doompi-voice-activity-speech-ms';\nexport const VOICE_MEDIA_ACTIVITY_ECHO_SPEECH_MS_HEADER = 'x-doompi-voice-activity-echo-speech-ms';\nexport const VOICE_MEDIA_WAKE_TYPE = 'voice_media_wake';\nexport const VOICE_MEDIA_HEARTBEAT_MS = 5_000;\nexport const VOICE_MEDIA_EVENT_WAIT_NONE = '0';\n\nexport const VOICE_MEDIA_ROUTES = {\n clientConnect: '/client/connect',\n clientDisconnect: '/client/disconnect',\n clientEvents: '/client/events',\n clientHeartbeat: '/client/heartbeat',\n clientAudio: '/client/audio',\n clientCaptureStopped: '/client/capture-stopped',\n clientPlaybackAudio: '/client/playback-audio',\n clientPlaybackResult: '/client/playback-result',\n hostCaptureStart: '/host/capture/start',\n hostCaptureAudio: '/host/capture/audio',\n hostCaptureStop: '/host/capture/stop',\n hostCaptureAbort: '/host/capture/abort',\n hostPlaybackStart: '/host/playback/start',\n hostPlaybackAudio: '/host/playback/audio',\n hostPlaybackAudioEnd: '/host/playback/audio-end',\n hostPlaybackResult: '/host/playback/result',\n hostPlaybackStop: '/host/playback/stop',\n hostPlaybackAbort: '/host/playback/abort',\n} as const;\n\nexport type VoiceClientKind = 'browser' | 'native';\nexport type VoiceMediaControlLocation = 'local' | 'remote';\n\nexport interface VoiceMediaCapabilities {\n capture: boolean;\n playback: boolean;\n captureActivity: boolean;\n autonomousOrchestration: boolean;\n playbackDucking?: boolean;\n realtime?: boolean;\n}\n\nexport type VoiceMediaCaptureMode = 'manual' | 'autonomous';\nexport type VoiceMediaPlaybackDelivery = 'client' | 'streamed';\nexport type VoiceMediaCaptureActivityState = 'listening' | 'speech' | 'endpoint';\n\nexport interface VoiceMediaCaptureActivity {\n state: VoiceMediaCaptureActivityState;\n levelDbfs: number;\n elapsedMs: number;\n epoch?: number;\n classifiedSpeechMs?: number;\n echoDiscriminatedSpeechMs?: number;\n}\n\nexport interface VoiceMediaCaptureConfiguration {\n mode: VoiceMediaCaptureMode;\n activityControl: 'host' | 'client';\n endpointSilenceMs?: number;\n}\n\nexport interface VoiceMediaConnectRequest {\n version: typeof VOICE_MEDIA_PROTOCOL_VERSION;\n clientId: string;\n connectionId: string;\n clientKind: VoiceClientKind;\n controlLocation: VoiceMediaControlLocation;\n capabilities: VoiceMediaCapabilities;\n}\n\nexport interface VoiceMediaWake {\n eventEpoch: string;\n sequence: number;\n}\n\nexport interface VoiceMediaConnectResult {\n version: typeof VOICE_MEDIA_PROTOCOL_VERSION;\n cursor: number;\n eventEpoch?: string;\n heartbeatMs?: number;\n}\n\nexport type VoiceMediaHeartbeatResult = VoiceMediaWake;\n\nexport type VoiceMediaClientEvent =\n | RealtimeMediaCommand\n | {\n sequence: number;\n type: 'capture-start';\n captureId: string;\n sampleRate: typeof VOICE_MEDIA_SAMPLE_RATE;\n channels: typeof VOICE_MEDIA_CHANNELS;\n bitsPerSample: typeof VOICE_MEDIA_BITS_PER_SAMPLE;\n configuration: VoiceMediaCaptureConfiguration;\n }\n | { sequence: number; type: 'capture-stop'; captureId: string }\n | { sequence: number; type: 'capture-abort'; captureId: string }\n | {\n sequence: number;\n type: 'playback-start';\n playbackId: string;\n text: string;\n delivery?: VoiceMediaPlaybackDelivery;\n voice?: string;\n rate?: number;\n }\n | { sequence: number; type: 'playback-stop'; playbackId: string }\n | { sequence: number; type: 'playback-abort'; playbackId: string };\n\nexport type VoiceMediaPlaybackOutcome = 'completed' | 'stopped' | 'aborted' | 'failed';\n\nexport interface VoiceMediaPlaybackResult {\n playbackId: string;\n outcome: VoiceMediaPlaybackOutcome;\n error?: string;\n}\n\nexport interface VoiceMediaTransport extends Partial<RealtimeMediaTransport> {\n connect(\n clientId: string,\n connectionId: string,\n capabilities: VoiceMediaCapabilities,\n ): Promise<VoiceMediaConnectResult>;\n /** Refreshes optional capabilities without changing active work or event position. */\n refreshCapabilities?(clientId: string, connectionId: string, capabilities: VoiceMediaCapabilities): Promise<void>;\n disconnect(clientId: string, connectionId: string): Promise<void>;\n nextEvent(\n clientId: string,\n connectionId: string,\n after: number,\n signal: AbortSignal,\n ): Promise<VoiceMediaClientEvent | undefined>;\n sendAudio(\n clientId: string,\n connectionId: string,\n captureId: string,\n pcm: Uint8Array,\n activity?: VoiceMediaCaptureActivity,\n ): Promise<void>;\n captureStopped(clientId: string, connectionId: string, captureId: string, error?: string): Promise<void>;\n receivePlaybackAudio?(\n clientId: string,\n connectionId: string,\n playbackId: string,\n signal: AbortSignal,\n ): Promise<Uint8Array>;\n playbackFinished(clientId: string, connectionId: string, result: VoiceMediaPlaybackResult): Promise<void>;\n}\n\nexport interface VoiceMediaCapture {\n completion?: Promise<void>;\n stop(): Promise<void>;\n}\n\n/** Optional browser-local speech input. The original capture PCM remains authoritative for transport. */\nexport interface VoiceMediaCaptureSpeechAnalysis {\n speechPcm: Uint8Array;\n echoReferenceActive: boolean;\n echoDiscriminated: boolean;\n}\n\nexport interface VoiceMediaPlayback {\n readonly completion: Promise<VoiceMediaPlaybackResult>;\n stop(outcome: Extract<VoiceMediaPlaybackOutcome, 'stopped' | 'aborted'>): void;\n duck?(targetGain: number, fadeMs: number, holdMs: number): void;\n}\n\n/** Client hardware boundary. Browser and future native clients implement the same contract. */\nexport interface VoiceMediaDevice {\n readonly capabilities: VoiceMediaCapabilities;\n /** Initializes optional authoritative media processors after baseline capabilities are available. */\n prepare?(): Promise<void>;\n createSpeechPresenceDetector?(): SpeechPresenceDetector | undefined;\n startCapture(\n onPcm: (pcm: Uint8Array, speechAnalysis?: VoiceMediaCaptureSpeechAnalysis) => void,\n ): Promise<VoiceMediaCapture>;\n speak(\n request: Extract<VoiceMediaClientEvent, { type: 'playback-start' }>,\n audio?: Promise<Uint8Array>,\n ): VoiceMediaPlayback;\n close(): Promise<void>;\n}\n\nexport function voiceMediaClientUrl(sessionId: string, route: string, params: Record<string, string> = {}): string {\n const search = new URLSearchParams({ session: sessionId, ...params });\n return `${sessionApiPath(sessionId)}/plugins/${VOICE_MEDIA_API_BASE_PATH}${route}?${search.toString()}`;\n}\n\nexport interface VoiceMicrophoneConstraints {\n audio:\n | boolean\n | {\n deviceId: { exact: string };\n channelCount: number;\n echoCancellation: boolean;\n noiseSuppression: boolean;\n autoGainControl: boolean;\n };\n video: false;\n}\n"],"mappings":"4CAKA,MAAa,EAA4B,cAiB5B,EAAqB,CAChC,cAAe,kBACf,iBAAkB,qBAClB,aAAc,iBACd,gBAAiB,oBACjB,YAAa,gBACb,qBAAsB,0BACtB,oBAAqB,yBACrB,qBAAsB,0BACtB,iBAAkB,sBAClB,iBAAkB,sBAClB,gBAAiB,qBACjB,iBAAkB,sBAClB,kBAAmB,uBACnB,kBAAmB,uBACnB,qBAAsB,2BACtB,mBAAoB,wBACpB,iBAAkB,sBAClB,kBAAmB,sBACrB,EA2JA,SAAgB,EAAoB,EAAmB,EAAe,EAAiC,CAAC,EAAW,CACjH,IAAM,EAAS,IAAI,gBAAgB,CAAE,QAAS,EAAW,GAAG,CAAO,CAAC,EACpE,MAAO,IAAA,EAAGA,EAAAA,eAAAA,CAAe,CAAS,EAAE,WAAW,IAA4B,EAAM,GAAG,EAAO,SAAS,GACtG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clientMedia.d.cts","names":[],"sources":["../../../src/types/clientMedia.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"clientMedia.d.cts","names":[],"sources":["../../../src/types/clientMedia.ts"],"mappings":";;;qBAKa;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBAEA;WACX;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;;YAGU;YACA;iBAEK;EACf;EACA;EACA;EACA;EACA;EACA;;YAGU;YACA;YACA;iBAEK;EACf,OAAO;EACP;EACA;EACA;EACA;EACA;;iBAGe;EACf,MAAM;EACN;EACA;;iBAGe;EACf,gBAAgB;EAChB;EACA;EACA,YAAY;EACZ,iBAAiB;EACjB,cAAc;;iBAGC;EACf;EACA;;iBAGe;EACf,gBAAgB;EAChB;EACA;EACA;;YAGU,4BAA4B;YAE5B,wBACR;EAEE;EACA;EACA;EACA,mBAAmB;EACnB,iBAAiB;EACjB,sBAAsB;EACtB,eAAe;;EAEf;EAAkB;EAAsB;;EACxC;EAAkB;EAAuB;;EAEzC;EACA;EACA;EACA;EACA,WAAW;EACX;EACA;;EAEA;EAAkB;EAAuB;;EACzC;EAAkB;EAAwB;;YAEpC;iBAEK;EACf;EACA,SAAS;EACT;;iBAGe,4BAA4B,QAAQ;EACnD,QACE,kBACA,sBACA,cAAc,yBACb,QAAQ;;EAEX,qBAAqB,kBAAkB,sBAAsB,cAAc,yBAAyB;EACpG,WAAW,kBAAkB,uBAAuB;EACpD,UACE,kBACA,sBACA,eACA,QAAQ,cACP,QAAQ;EACX,UACE,kBACA,sBACA,mBACA,KAAK,YACL,WAAW,4BACV;EACH,eAAe,kBAAkB,sBAAsB,mBAAmB,iBAAiB;EAC3F,sBACE,kBACA,sBACA,oBACA,QAAQ,cACP,QAAQ;EACX,iBAAiB,kBAAkB,sBAAsB,QAAQ,2BAA2B;;iBAG7E;EACf,aAAa;EACb,QAAQ;;;iBAIO;EACf,WAAW;EACX;EACA;;iBAGe;WACN,YAAY,QAAQ;EAC7B,KAAK,SAAS,QAAQ;EACtB,MAAM,oBAAoB,gBAAgB;;;iBAI3B;WACN,cAAc;;EAEvB,YAAY;EACZ,iCAAiC;EACjC,aACE,QAAQ,KAAK,YAAY,iBAAiB,2CACzC,QAAQ;EACX,MACE,SAAS,QAAQ;IAAyB;MAC1C,QAAQ,QAAQ,cACf;EACH,SAAS;;wBAGK,oBAAoB,mBAAmB,eAAe,SAAQ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clientMedia.d.mts","names":[],"sources":["../../../src/types/clientMedia.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"clientMedia.d.mts","names":[],"sources":["../../../src/types/clientMedia.ts"],"mappings":";;;qBAKa;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBACA;qBAEA;WACX;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;;YAGU;YACA;iBAEK;EACf;EACA;EACA;EACA;EACA;EACA;;YAGU;YACA;YACA;iBAEK;EACf,OAAO;EACP;EACA;EACA;EACA;EACA;;iBAGe;EACf,MAAM;EACN;EACA;;iBAGe;EACf,gBAAgB;EAChB;EACA;EACA,YAAY;EACZ,iBAAiB;EACjB,cAAc;;iBAGC;EACf;EACA;;iBAGe;EACf,gBAAgB;EAChB;EACA;EACA;;YAGU,4BAA4B;YAE5B,wBACR;EAEE;EACA;EACA;EACA,mBAAmB;EACnB,iBAAiB;EACjB,sBAAsB;EACtB,eAAe;;EAEf;EAAkB;EAAsB;;EACxC;EAAkB;EAAuB;;EAEzC;EACA;EACA;EACA;EACA,WAAW;EACX;EACA;;EAEA;EAAkB;EAAuB;;EACzC;EAAkB;EAAwB;;YAEpC;iBAEK;EACf;EACA,SAAS;EACT;;iBAGe,4BAA4B,QAAQ;EACnD,QACE,kBACA,sBACA,cAAc,yBACb,QAAQ;;EAEX,qBAAqB,kBAAkB,sBAAsB,cAAc,yBAAyB;EACpG,WAAW,kBAAkB,uBAAuB;EACpD,UACE,kBACA,sBACA,eACA,QAAQ,cACP,QAAQ;EACX,UACE,kBACA,sBACA,mBACA,KAAK,YACL,WAAW,4BACV;EACH,eAAe,kBAAkB,sBAAsB,mBAAmB,iBAAiB;EAC3F,sBACE,kBACA,sBACA,oBACA,QAAQ,cACP,QAAQ;EACX,iBAAiB,kBAAkB,sBAAsB,QAAQ,2BAA2B;;iBAG7E;EACf,aAAa;EACb,QAAQ;;;iBAIO;EACf,WAAW;EACX;EACA;;iBAGe;WACN,YAAY,QAAQ;EAC7B,KAAK,SAAS,QAAQ;EACtB,MAAM,oBAAoB,gBAAgB;;;iBAI3B;WACN,cAAc;;EAEvB,YAAY;EACZ,iCAAiC;EACjC,aACE,QAAQ,KAAK,YAAY,iBAAiB,2CACzC,QAAQ;EACX,MACE,SAAS,QAAQ;IAAyB;MAC1C,QAAQ,QAAQ,cACf;EACH,SAAS;;wBAGK,oBAAoB,mBAAmB,eAAe,SAAQ"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const
|
|
1
|
+
import{sessionApiPath as e}from"@agimon-ai/doompi-core/web";const t=`voice-media`,n=6,r=16e3,i=1,a=16,o=`application/vnd.doompi.pcm-s16le`,s=`x-doompi-voice-activity-state`,c=`x-doompi-playback-state`,l=`x-doompi-voice-activity-level`,u=`x-doompi-voice-activity-elapsed`,d=`x-doompi-voice-activity-epoch`,f=`x-doompi-voice-activity-speech-ms`,p=`x-doompi-voice-activity-echo-speech-ms`,m=`voice_media_wake`,h=5e3,g=`0`,_={clientConnect:`/client/connect`,clientDisconnect:`/client/disconnect`,clientEvents:`/client/events`,clientHeartbeat:`/client/heartbeat`,clientAudio:`/client/audio`,clientCaptureStopped:`/client/capture-stopped`,clientPlaybackAudio:`/client/playback-audio`,clientPlaybackResult:`/client/playback-result`,hostCaptureStart:`/host/capture/start`,hostCaptureAudio:`/host/capture/audio`,hostCaptureStop:`/host/capture/stop`,hostCaptureAbort:`/host/capture/abort`,hostPlaybackStart:`/host/playback/start`,hostPlaybackAudio:`/host/playback/audio`,hostPlaybackAudioEnd:`/host/playback/audio-end`,hostPlaybackResult:`/host/playback/result`,hostPlaybackStop:`/host/playback/stop`,hostPlaybackAbort:`/host/playback/abort`};function v(n,r,i={}){let a=new URLSearchParams({session:n,...i});return`${e(n)}/plugins/${t}${r}?${a.toString()}`}export{p as VOICE_MEDIA_ACTIVITY_ECHO_SPEECH_MS_HEADER,u as VOICE_MEDIA_ACTIVITY_ELAPSED_HEADER,d as VOICE_MEDIA_ACTIVITY_EPOCH_HEADER,l as VOICE_MEDIA_ACTIVITY_LEVEL_HEADER,f as VOICE_MEDIA_ACTIVITY_SPEECH_MS_HEADER,s as VOICE_MEDIA_ACTIVITY_STATE_HEADER,t as VOICE_MEDIA_API_BASE_PATH,a as VOICE_MEDIA_BITS_PER_SAMPLE,i as VOICE_MEDIA_CHANNELS,o as VOICE_MEDIA_CONTENT_TYPE,g as VOICE_MEDIA_EVENT_WAIT_NONE,h as VOICE_MEDIA_HEARTBEAT_MS,c as VOICE_MEDIA_PLAYBACK_STATE_HEADER,n as VOICE_MEDIA_PROTOCOL_VERSION,_ as VOICE_MEDIA_ROUTES,r as VOICE_MEDIA_SAMPLE_RATE,m as VOICE_MEDIA_WAKE_TYPE,v as voiceMediaClientUrl};
|
|
2
2
|
//# sourceMappingURL=clientMedia.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clientMedia.mjs","names":[],"sources":["../../../src/types/clientMedia.ts"],"sourcesContent":["import type { SpeechPresenceDetector } from './clientCaptureActivity';\nimport type { RealtimeMediaCommand, RealtimeMediaTransport } from './realtime';\n\nexport const VOICE_MEDIA_API_BASE_PATH = 'voice-media';\nexport const VOICE_MEDIA_PROTOCOL_VERSION = 6;\nexport const VOICE_MEDIA_SAMPLE_RATE = 16_000;\nexport const VOICE_MEDIA_CHANNELS = 1;\nexport const VOICE_MEDIA_BITS_PER_SAMPLE = 16;\nexport const VOICE_MEDIA_CONTENT_TYPE = 'application/vnd.doompi.pcm-s16le';\nexport const VOICE_MEDIA_ACTIVITY_STATE_HEADER = 'x-doompi-voice-activity-state';\nexport const VOICE_MEDIA_PLAYBACK_STATE_HEADER = 'x-doompi-playback-state';\nexport const VOICE_MEDIA_ACTIVITY_LEVEL_HEADER = 'x-doompi-voice-activity-level';\nexport const VOICE_MEDIA_ACTIVITY_ELAPSED_HEADER = 'x-doompi-voice-activity-elapsed';\nexport const VOICE_MEDIA_ACTIVITY_EPOCH_HEADER = 'x-doompi-voice-activity-epoch';\nexport const VOICE_MEDIA_ACTIVITY_SPEECH_MS_HEADER = 'x-doompi-voice-activity-speech-ms';\nexport const VOICE_MEDIA_ACTIVITY_ECHO_SPEECH_MS_HEADER = 'x-doompi-voice-activity-echo-speech-ms';\nexport const VOICE_MEDIA_WAKE_TYPE = 'voice_media_wake';\nexport const VOICE_MEDIA_HEARTBEAT_MS = 5_000;\nexport const VOICE_MEDIA_EVENT_WAIT_NONE = '0';\n\nexport const VOICE_MEDIA_ROUTES = {\n clientConnect: '/client/connect',\n clientDisconnect: '/client/disconnect',\n clientEvents: '/client/events',\n clientHeartbeat: '/client/heartbeat',\n clientAudio: '/client/audio',\n clientCaptureStopped: '/client/capture-stopped',\n clientPlaybackAudio: '/client/playback-audio',\n clientPlaybackResult: '/client/playback-result',\n hostCaptureStart: '/host/capture/start',\n hostCaptureAudio: '/host/capture/audio',\n hostCaptureStop: '/host/capture/stop',\n hostCaptureAbort: '/host/capture/abort',\n hostPlaybackStart: '/host/playback/start',\n hostPlaybackAudio: '/host/playback/audio',\n hostPlaybackAudioEnd: '/host/playback/audio-end',\n hostPlaybackResult: '/host/playback/result',\n hostPlaybackStop: '/host/playback/stop',\n hostPlaybackAbort: '/host/playback/abort',\n} as const;\n\nexport type VoiceClientKind = 'browser' | 'native';\nexport type VoiceMediaControlLocation = 'local' | 'remote';\n\nexport interface VoiceMediaCapabilities {\n capture: boolean;\n playback: boolean;\n captureActivity: boolean;\n autonomousOrchestration: boolean;\n playbackDucking?: boolean;\n realtime?: boolean;\n}\n\nexport type VoiceMediaCaptureMode = 'manual' | 'autonomous';\nexport type VoiceMediaPlaybackDelivery = 'client' | 'streamed';\nexport type VoiceMediaCaptureActivityState = 'listening' | 'speech' | 'endpoint';\n\nexport interface VoiceMediaCaptureActivity {\n state: VoiceMediaCaptureActivityState;\n levelDbfs: number;\n elapsedMs: number;\n epoch?: number;\n classifiedSpeechMs?: number;\n echoDiscriminatedSpeechMs?: number;\n}\n\nexport interface VoiceMediaCaptureConfiguration {\n mode: VoiceMediaCaptureMode;\n activityControl: 'host' | 'client';\n endpointSilenceMs?: number;\n}\n\nexport interface VoiceMediaConnectRequest {\n version: typeof VOICE_MEDIA_PROTOCOL_VERSION;\n clientId: string;\n connectionId: string;\n clientKind: VoiceClientKind;\n controlLocation: VoiceMediaControlLocation;\n capabilities: VoiceMediaCapabilities;\n}\n\nexport interface VoiceMediaWake {\n eventEpoch: string;\n sequence: number;\n}\n\nexport interface VoiceMediaConnectResult {\n version: typeof VOICE_MEDIA_PROTOCOL_VERSION;\n cursor: number;\n eventEpoch?: string;\n heartbeatMs?: number;\n}\n\nexport type VoiceMediaHeartbeatResult = VoiceMediaWake;\n\nexport type VoiceMediaClientEvent =\n | RealtimeMediaCommand\n | {\n sequence: number;\n type: 'capture-start';\n captureId: string;\n sampleRate: typeof VOICE_MEDIA_SAMPLE_RATE;\n channels: typeof VOICE_MEDIA_CHANNELS;\n bitsPerSample: typeof VOICE_MEDIA_BITS_PER_SAMPLE;\n configuration: VoiceMediaCaptureConfiguration;\n }\n | { sequence: number; type: 'capture-stop'; captureId: string }\n | { sequence: number; type: 'capture-abort'; captureId: string }\n | {\n sequence: number;\n type: 'playback-start';\n playbackId: string;\n text: string;\n delivery?: VoiceMediaPlaybackDelivery;\n voice?: string;\n rate?: number;\n }\n | { sequence: number; type: 'playback-stop'; playbackId: string }\n | { sequence: number; type: 'playback-abort'; playbackId: string };\n\nexport type VoiceMediaPlaybackOutcome = 'completed' | 'stopped' | 'aborted' | 'failed';\n\nexport interface VoiceMediaPlaybackResult {\n playbackId: string;\n outcome: VoiceMediaPlaybackOutcome;\n error?: string;\n}\n\nexport interface VoiceMediaTransport extends Partial<RealtimeMediaTransport> {\n connect(\n clientId: string,\n connectionId: string,\n capabilities: VoiceMediaCapabilities,\n ): Promise<VoiceMediaConnectResult>;\n /** Refreshes optional capabilities without changing active work or event position. */\n refreshCapabilities?(clientId: string, connectionId: string, capabilities: VoiceMediaCapabilities): Promise<void>;\n disconnect(clientId: string, connectionId: string): Promise<void>;\n nextEvent(\n clientId: string,\n connectionId: string,\n after: number,\n signal: AbortSignal,\n ): Promise<VoiceMediaClientEvent | undefined>;\n sendAudio(\n clientId: string,\n connectionId: string,\n captureId: string,\n pcm: Uint8Array,\n activity?: VoiceMediaCaptureActivity,\n ): Promise<void>;\n captureStopped(clientId: string, connectionId: string, captureId: string, error?: string): Promise<void>;\n receivePlaybackAudio?(\n clientId: string,\n connectionId: string,\n playbackId: string,\n signal: AbortSignal,\n ): Promise<Uint8Array>;\n playbackFinished(clientId: string, connectionId: string, result: VoiceMediaPlaybackResult): Promise<void>;\n}\n\nexport interface VoiceMediaCapture {\n completion?: Promise<void>;\n stop(): Promise<void>;\n}\n\n/** Optional browser-local speech input. The original capture PCM remains authoritative for transport. */\nexport interface VoiceMediaCaptureSpeechAnalysis {\n speechPcm: Uint8Array;\n echoReferenceActive: boolean;\n echoDiscriminated: boolean;\n}\n\nexport interface VoiceMediaPlayback {\n readonly completion: Promise<VoiceMediaPlaybackResult>;\n stop(outcome: Extract<VoiceMediaPlaybackOutcome, 'stopped' | 'aborted'>): void;\n duck?(targetGain: number, fadeMs: number, holdMs: number): void;\n}\n\n/** Client hardware boundary. Browser and future native clients implement the same contract. */\nexport interface VoiceMediaDevice {\n readonly capabilities: VoiceMediaCapabilities;\n /** Initializes optional authoritative media processors after baseline capabilities are available. */\n prepare?(): Promise<void>;\n createSpeechPresenceDetector?(): SpeechPresenceDetector | undefined;\n startCapture(\n onPcm: (pcm: Uint8Array, speechAnalysis?: VoiceMediaCaptureSpeechAnalysis) => void,\n ): Promise<VoiceMediaCapture>;\n speak(\n request: Extract<VoiceMediaClientEvent, { type: 'playback-start' }>,\n audio?: Promise<Uint8Array>,\n ): VoiceMediaPlayback;\n close(): Promise<void>;\n}\n\nexport function voiceMediaClientUrl(sessionId: string, route: string, params: Record<string, string> = {}): string {\n const search = new URLSearchParams({ session: sessionId, ...params });\n return
|
|
1
|
+
{"version":3,"file":"clientMedia.mjs","names":[],"sources":["../../../src/types/clientMedia.ts"],"sourcesContent":["import { sessionApiPath } from '@agimon-ai/doompi-core/web';\n\nimport type { SpeechPresenceDetector } from './clientCaptureActivity';\nimport type { RealtimeMediaCommand, RealtimeMediaTransport } from './realtime';\n\nexport const VOICE_MEDIA_API_BASE_PATH = 'voice-media';\nexport const VOICE_MEDIA_PROTOCOL_VERSION = 6;\nexport const VOICE_MEDIA_SAMPLE_RATE = 16_000;\nexport const VOICE_MEDIA_CHANNELS = 1;\nexport const VOICE_MEDIA_BITS_PER_SAMPLE = 16;\nexport const VOICE_MEDIA_CONTENT_TYPE = 'application/vnd.doompi.pcm-s16le';\nexport const VOICE_MEDIA_ACTIVITY_STATE_HEADER = 'x-doompi-voice-activity-state';\nexport const VOICE_MEDIA_PLAYBACK_STATE_HEADER = 'x-doompi-playback-state';\nexport const VOICE_MEDIA_ACTIVITY_LEVEL_HEADER = 'x-doompi-voice-activity-level';\nexport const VOICE_MEDIA_ACTIVITY_ELAPSED_HEADER = 'x-doompi-voice-activity-elapsed';\nexport const VOICE_MEDIA_ACTIVITY_EPOCH_HEADER = 'x-doompi-voice-activity-epoch';\nexport const VOICE_MEDIA_ACTIVITY_SPEECH_MS_HEADER = 'x-doompi-voice-activity-speech-ms';\nexport const VOICE_MEDIA_ACTIVITY_ECHO_SPEECH_MS_HEADER = 'x-doompi-voice-activity-echo-speech-ms';\nexport const VOICE_MEDIA_WAKE_TYPE = 'voice_media_wake';\nexport const VOICE_MEDIA_HEARTBEAT_MS = 5_000;\nexport const VOICE_MEDIA_EVENT_WAIT_NONE = '0';\n\nexport const VOICE_MEDIA_ROUTES = {\n clientConnect: '/client/connect',\n clientDisconnect: '/client/disconnect',\n clientEvents: '/client/events',\n clientHeartbeat: '/client/heartbeat',\n clientAudio: '/client/audio',\n clientCaptureStopped: '/client/capture-stopped',\n clientPlaybackAudio: '/client/playback-audio',\n clientPlaybackResult: '/client/playback-result',\n hostCaptureStart: '/host/capture/start',\n hostCaptureAudio: '/host/capture/audio',\n hostCaptureStop: '/host/capture/stop',\n hostCaptureAbort: '/host/capture/abort',\n hostPlaybackStart: '/host/playback/start',\n hostPlaybackAudio: '/host/playback/audio',\n hostPlaybackAudioEnd: '/host/playback/audio-end',\n hostPlaybackResult: '/host/playback/result',\n hostPlaybackStop: '/host/playback/stop',\n hostPlaybackAbort: '/host/playback/abort',\n} as const;\n\nexport type VoiceClientKind = 'browser' | 'native';\nexport type VoiceMediaControlLocation = 'local' | 'remote';\n\nexport interface VoiceMediaCapabilities {\n capture: boolean;\n playback: boolean;\n captureActivity: boolean;\n autonomousOrchestration: boolean;\n playbackDucking?: boolean;\n realtime?: boolean;\n}\n\nexport type VoiceMediaCaptureMode = 'manual' | 'autonomous';\nexport type VoiceMediaPlaybackDelivery = 'client' | 'streamed';\nexport type VoiceMediaCaptureActivityState = 'listening' | 'speech' | 'endpoint';\n\nexport interface VoiceMediaCaptureActivity {\n state: VoiceMediaCaptureActivityState;\n levelDbfs: number;\n elapsedMs: number;\n epoch?: number;\n classifiedSpeechMs?: number;\n echoDiscriminatedSpeechMs?: number;\n}\n\nexport interface VoiceMediaCaptureConfiguration {\n mode: VoiceMediaCaptureMode;\n activityControl: 'host' | 'client';\n endpointSilenceMs?: number;\n}\n\nexport interface VoiceMediaConnectRequest {\n version: typeof VOICE_MEDIA_PROTOCOL_VERSION;\n clientId: string;\n connectionId: string;\n clientKind: VoiceClientKind;\n controlLocation: VoiceMediaControlLocation;\n capabilities: VoiceMediaCapabilities;\n}\n\nexport interface VoiceMediaWake {\n eventEpoch: string;\n sequence: number;\n}\n\nexport interface VoiceMediaConnectResult {\n version: typeof VOICE_MEDIA_PROTOCOL_VERSION;\n cursor: number;\n eventEpoch?: string;\n heartbeatMs?: number;\n}\n\nexport type VoiceMediaHeartbeatResult = VoiceMediaWake;\n\nexport type VoiceMediaClientEvent =\n | RealtimeMediaCommand\n | {\n sequence: number;\n type: 'capture-start';\n captureId: string;\n sampleRate: typeof VOICE_MEDIA_SAMPLE_RATE;\n channels: typeof VOICE_MEDIA_CHANNELS;\n bitsPerSample: typeof VOICE_MEDIA_BITS_PER_SAMPLE;\n configuration: VoiceMediaCaptureConfiguration;\n }\n | { sequence: number; type: 'capture-stop'; captureId: string }\n | { sequence: number; type: 'capture-abort'; captureId: string }\n | {\n sequence: number;\n type: 'playback-start';\n playbackId: string;\n text: string;\n delivery?: VoiceMediaPlaybackDelivery;\n voice?: string;\n rate?: number;\n }\n | { sequence: number; type: 'playback-stop'; playbackId: string }\n | { sequence: number; type: 'playback-abort'; playbackId: string };\n\nexport type VoiceMediaPlaybackOutcome = 'completed' | 'stopped' | 'aborted' | 'failed';\n\nexport interface VoiceMediaPlaybackResult {\n playbackId: string;\n outcome: VoiceMediaPlaybackOutcome;\n error?: string;\n}\n\nexport interface VoiceMediaTransport extends Partial<RealtimeMediaTransport> {\n connect(\n clientId: string,\n connectionId: string,\n capabilities: VoiceMediaCapabilities,\n ): Promise<VoiceMediaConnectResult>;\n /** Refreshes optional capabilities without changing active work or event position. */\n refreshCapabilities?(clientId: string, connectionId: string, capabilities: VoiceMediaCapabilities): Promise<void>;\n disconnect(clientId: string, connectionId: string): Promise<void>;\n nextEvent(\n clientId: string,\n connectionId: string,\n after: number,\n signal: AbortSignal,\n ): Promise<VoiceMediaClientEvent | undefined>;\n sendAudio(\n clientId: string,\n connectionId: string,\n captureId: string,\n pcm: Uint8Array,\n activity?: VoiceMediaCaptureActivity,\n ): Promise<void>;\n captureStopped(clientId: string, connectionId: string, captureId: string, error?: string): Promise<void>;\n receivePlaybackAudio?(\n clientId: string,\n connectionId: string,\n playbackId: string,\n signal: AbortSignal,\n ): Promise<Uint8Array>;\n playbackFinished(clientId: string, connectionId: string, result: VoiceMediaPlaybackResult): Promise<void>;\n}\n\nexport interface VoiceMediaCapture {\n completion?: Promise<void>;\n stop(): Promise<void>;\n}\n\n/** Optional browser-local speech input. The original capture PCM remains authoritative for transport. */\nexport interface VoiceMediaCaptureSpeechAnalysis {\n speechPcm: Uint8Array;\n echoReferenceActive: boolean;\n echoDiscriminated: boolean;\n}\n\nexport interface VoiceMediaPlayback {\n readonly completion: Promise<VoiceMediaPlaybackResult>;\n stop(outcome: Extract<VoiceMediaPlaybackOutcome, 'stopped' | 'aborted'>): void;\n duck?(targetGain: number, fadeMs: number, holdMs: number): void;\n}\n\n/** Client hardware boundary. Browser and future native clients implement the same contract. */\nexport interface VoiceMediaDevice {\n readonly capabilities: VoiceMediaCapabilities;\n /** Initializes optional authoritative media processors after baseline capabilities are available. */\n prepare?(): Promise<void>;\n createSpeechPresenceDetector?(): SpeechPresenceDetector | undefined;\n startCapture(\n onPcm: (pcm: Uint8Array, speechAnalysis?: VoiceMediaCaptureSpeechAnalysis) => void,\n ): Promise<VoiceMediaCapture>;\n speak(\n request: Extract<VoiceMediaClientEvent, { type: 'playback-start' }>,\n audio?: Promise<Uint8Array>,\n ): VoiceMediaPlayback;\n close(): Promise<void>;\n}\n\nexport function voiceMediaClientUrl(sessionId: string, route: string, params: Record<string, string> = {}): string {\n const search = new URLSearchParams({ session: sessionId, ...params });\n return `${sessionApiPath(sessionId)}/plugins/${VOICE_MEDIA_API_BASE_PATH}${route}?${search.toString()}`;\n}\n\nexport interface VoiceMicrophoneConstraints {\n audio:\n | boolean\n | {\n deviceId: { exact: string };\n channelCount: number;\n echoCancellation: boolean;\n noiseSuppression: boolean;\n autoGainControl: boolean;\n };\n video: false;\n}\n"],"mappings":"4DAKA,MAAa,EAA4B,cAC5B,EAA+B,EAC/B,EAA0B,KAC1B,EAAuB,EACvB,EAA8B,GAC9B,EAA2B,mCAC3B,EAAoC,gCACpC,EAAoC,0BACpC,EAAoC,gCACpC,EAAsC,kCACtC,EAAoC,gCACpC,EAAwC,oCACxC,EAA6C,yCAC7C,EAAwB,mBACxB,EAA2B,IAC3B,EAA8B,IAE9B,EAAqB,CAChC,cAAe,kBACf,iBAAkB,qBAClB,aAAc,iBACd,gBAAiB,oBACjB,YAAa,gBACb,qBAAsB,0BACtB,oBAAqB,yBACrB,qBAAsB,0BACtB,iBAAkB,sBAClB,iBAAkB,sBAClB,gBAAiB,qBACjB,iBAAkB,sBAClB,kBAAmB,uBACnB,kBAAmB,uBACnB,qBAAsB,2BACtB,mBAAoB,wBACpB,iBAAkB,sBAClB,kBAAmB,sBACrB,EA2JA,SAAgB,EAAoB,EAAmB,EAAe,EAAiC,CAAC,EAAW,CACjH,IAAM,EAAS,IAAI,gBAAgB,CAAE,QAAS,EAAW,GAAG,CAAO,CAAC,EACpE,MAAO,GAAG,EAAe,CAAS,EAAE,WAAW,IAA4B,EAAM,GAAG,EAAO,SAAS,GACtG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/doompi-voice",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.73",
|
|
4
4
|
"description": "Client audio capture, host transcription, and autonomous narration for Pi agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -85,28 +85,28 @@
|
|
|
85
85
|
"registry": "https://registry.npmjs.org/"
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
|
+
"@agimon-ai/doompi-config": "0.0.1-alpha.71",
|
|
89
|
+
"@agimon-ai/doompi-core": "0.0.1-alpha.72",
|
|
90
|
+
"@agimon-ai/doompi-minor-mode": "0.0.1-alpha.72",
|
|
91
|
+
"@agimon-ai/doompi-telemetry": "0.0.1-alpha.69",
|
|
92
|
+
"@agimon-ai/doompi-ui": "0.0.1-alpha.72",
|
|
93
|
+
"@agimon-ai/doompi-web-components": "0.0.1-alpha.30",
|
|
94
|
+
"@agimon-ai/doompi-web-security": "0.0.1-alpha.32",
|
|
88
95
|
"@deepseek-ai/cordis": "4.0.2",
|
|
89
96
|
"onnxruntime-web": "1.29.0",
|
|
90
|
-
"sherpa-onnx-node": "1.13.
|
|
91
|
-
"typebox": "1.3.
|
|
92
|
-
"xstate": "5.32.6"
|
|
93
|
-
"@agimon-ai/doompi-minor-mode": "0.0.1-alpha.70",
|
|
94
|
-
"@agimon-ai/doompi-telemetry": "0.0.1-alpha.68",
|
|
95
|
-
"@agimon-ai/doompi-ui": "0.0.1-alpha.70",
|
|
96
|
-
"@agimon-ai/doompi-web-components": "0.0.1-alpha.29",
|
|
97
|
-
"@agimon-ai/doompi-core": "0.0.1-alpha.70",
|
|
98
|
-
"@agimon-ai/doompi-web-security": "0.0.1-alpha.31",
|
|
99
|
-
"@agimon-ai/doompi-config": "0.0.1-alpha.69"
|
|
97
|
+
"sherpa-onnx-node": "1.13.8",
|
|
98
|
+
"typebox": "1.3.30",
|
|
99
|
+
"xstate": "5.32.6"
|
|
100
100
|
},
|
|
101
101
|
"devDependencies": {
|
|
102
102
|
"@earendil-works/pi-coding-agent": "0.85.1",
|
|
103
103
|
"@tanstack/react-store": "0.11.1",
|
|
104
104
|
"@tanstack/store": "0.11.1",
|
|
105
|
-
"@types/node": "26.
|
|
106
|
-
"@types/react": "19.
|
|
105
|
+
"@types/node": "26.5.1",
|
|
106
|
+
"@types/react": "19.3.0",
|
|
107
107
|
"@vitest/coverage-v8": "5.0.0",
|
|
108
|
-
"react": "19.
|
|
109
|
-
"react-dom": "19.
|
|
108
|
+
"react": "19.3.0",
|
|
109
|
+
"react-dom": "19.3.0",
|
|
110
110
|
"tsdown": "0.23.0",
|
|
111
111
|
"typescript": "7.0.2",
|
|
112
112
|
"vitest": "5.0.0"
|
package/src/types/clientMedia.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sessionApiPath } from '@agimon-ai/doompi-core/web';
|
|
2
|
+
|
|
1
3
|
import type { SpeechPresenceDetector } from './clientCaptureActivity';
|
|
2
4
|
import type { RealtimeMediaCommand, RealtimeMediaTransport } from './realtime';
|
|
3
5
|
|
|
@@ -194,7 +196,7 @@ export interface VoiceMediaDevice {
|
|
|
194
196
|
|
|
195
197
|
export function voiceMediaClientUrl(sessionId: string, route: string, params: Record<string, string> = {}): string {
|
|
196
198
|
const search = new URLSearchParams({ session: sessionId, ...params });
|
|
197
|
-
return
|
|
199
|
+
return `${sessionApiPath(sessionId)}/plugins/${VOICE_MEDIA_API_BASE_PATH}${route}?${search.toString()}`;
|
|
198
200
|
}
|
|
199
201
|
|
|
200
202
|
export interface VoiceMicrophoneConstraints {
|
|
@@ -47,7 +47,7 @@ function clientUrl(): string {
|
|
|
47
47
|
id = `browser-${crypto.randomUUID()}`;
|
|
48
48
|
browser.localStorage.setItem(key, id);
|
|
49
49
|
}
|
|
50
|
-
return `/api/
|
|
50
|
+
return `/api/plugins/voice/clients/${encodeURIComponent(id)}`;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
async function request(path: string, init?: RequestInit): Promise<Pick<MicrophoneState, 'inputs' | 'deviceId'>> {
|