1use serde::{Deserialize, Serialize};
2
3pub const VERSION: &str = crate::node::V1;
4pub const NODE_TYPE: &str = "audiobridge";
5pub const APP_TARGET_PREFIX: &str = "/app/plugin/audiobridge/";
6pub const RUNTIME_TARGET_PREFIX: &str = "/audiobridge/app/";
7pub const DEVICE_SNAPSHOT: &str = "device/snapshot";
8pub const DEVICE_REFRESH: &str = "device/refresh";
9pub const DEVICE_PAIR: &str = "device/pair";
10pub const DEVICE_UNPAIR: &str = "device/unpair";
11pub const DEVICE_TEST: &str = "device/test";
12pub const DEVICE_LISTEN_SET: &str = "device/listen";
13pub const SMART_SPEAKER_SNAPSHOT: &str = "smart-speaker/snapshot";
14pub const SMART_SPEAKER_CREATE: &str = "smart-speaker/create";
15pub const SMART_SPEAKER_UPDATE: &str = "smart-speaker/update";
16pub const SMART_SPEAKER_REMOVE: &str = "smart-speaker/remove";
17pub const AGENT_REPLY_TARGET: &str = "/audiobridge/agent/reply";
18
19#[derive(Debug, Clone, Default, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase", deny_unknown_fields)]
21pub struct EmptyRequest {}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase", deny_unknown_fields)]
25pub struct DeviceRequest {
26 pub device_id: String,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30#[serde(rename_all = "camelCase", deny_unknown_fields)]
31pub struct DeviceListenSetRequest {
32 pub device_id: String,
33 pub enabled: bool,
34}
35
36#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
37#[serde(rename_all = "lowercase")]
38pub enum AudioDeviceFeature {
39 Speaker,
40 Microphone,
41}
42
43#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
44#[serde(rename_all = "camelCase")]
45pub enum MicrophonePermission {
46 NotDetermined,
47 Granted,
48 Denied,
49 Restricted,
50 Unavailable,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
54#[serde(rename_all = "camelCase")]
55pub struct AudioBridgeDevice {
56 pub device_id: String,
57 pub display_name: String,
58 pub transport: String,
59 pub features: Vec<AudioDeviceFeature>,
60 pub online: bool,
61 pub listen_enabled: bool,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65#[serde(rename_all = "camelCase")]
66pub struct AudioBridgeCandidate {
67 pub device_id: String,
68 pub display_name: String,
69 pub transport: String,
70 pub features: Vec<AudioDeviceFeature>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct DeviceSnapshot {
76 pub microphone_permission: MicrophonePermission,
77 #[serde(default)]
78 pub devices: Vec<AudioBridgeDevice>,
79 #[serde(default)]
80 pub candidates: Vec<AudioBridgeCandidate>,
81}
82
83#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
84#[serde(rename_all = "camelCase")]
85pub enum SmartSpeakerRuntimeState {
86 Disabled,
87 Starting,
88 Listening,
89 PlaybackMuted,
90 Capturing,
91 Submitting,
92 WaitingForReply,
93 ReplyPlaying,
94 FollowUpListening,
95 InputOffline,
96 OutputOffline,
97 PermissionDenied,
98 ModelError,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
102#[serde(rename_all = "camelCase", deny_unknown_fields)]
103pub struct SmartSpeaker {
104 pub id: String,
105 pub display_name: String,
106 pub input_device_id: String,
107 pub output_device_id: String,
108 pub enabled: bool,
109 pub online: bool,
110 pub state: SmartSpeakerRuntimeState,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub reason: Option<String>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
116#[serde(rename_all = "camelCase", deny_unknown_fields)]
117pub struct SmartSpeakerSnapshot {
118 #[serde(default)]
119 pub smart_speakers: Vec<SmartSpeaker>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
123#[serde(rename_all = "camelCase", deny_unknown_fields)]
124pub struct SmartSpeakerCreateRequest {
125 pub display_name: String,
126 pub input_device_id: String,
127 pub output_device_id: String,
128 #[serde(default = "default_true")]
129 pub enabled: bool,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
133#[serde(rename_all = "camelCase", deny_unknown_fields)]
134pub struct SmartSpeakerUpdateRequest {
135 pub id: String,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub display_name: Option<String>,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub input_device_id: Option<String>,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub output_device_id: Option<String>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub enabled: Option<bool>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
147#[serde(rename_all = "camelCase", deny_unknown_fields)]
148pub struct SmartSpeakerRemoveRequest {
149 pub id: String,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
153#[serde(rename_all = "camelCase", deny_unknown_fields)]
154pub struct SmartSpeakerMutationResponse {
155 pub changed: bool,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub smart_speaker: Option<SmartSpeaker>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
161#[serde(rename_all = "camelCase", deny_unknown_fields)]
162pub struct AgentReplyRequest {
163 pub interaction_id: String,
164 pub smart_speaker_id: String,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub conversation_id: Option<String>,
167 pub audio: AgentAudioReference,
168 #[serde(default = "default_true")]
169 pub final_reply: bool,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
173#[serde(rename_all = "camelCase", deny_unknown_fields)]
174pub struct AgentAudioReference {
175 pub uri: String,
176 pub mime_type: String,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub duration_ms: Option<u64>,
179}
180
181const fn default_true() -> bool {
182 true
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct PairingResponse {
188 pub changed: bool,
189 pub device: AudioBridgeDevice,
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn manifest_and_rust_audiobridge_contract_agree() {
198 let manifest: serde_json::Value =
199 serde_json::from_str(include_str!("../../../manifest/core.v1.json")).unwrap();
200 let contract = &manifest["protocols"]["audioBridgePlugin"];
201 assert_eq!(contract["version"], VERSION);
202 assert_eq!(contract["nodeType"], NODE_TYPE);
203 assert_eq!(contract["appTargetPrefix"], APP_TARGET_PREFIX);
204 assert_eq!(contract["runtimeTargetPrefix"], RUNTIME_TARGET_PREFIX);
205 assert_eq!(contract["routes"]["deviceSnapshot"], DEVICE_SNAPSHOT);
206 assert_eq!(contract["routes"]["deviceRefresh"], DEVICE_REFRESH);
207 assert_eq!(contract["routes"]["devicePair"], DEVICE_PAIR);
208 assert_eq!(contract["routes"]["deviceUnpair"], DEVICE_UNPAIR);
209 assert_eq!(contract["routes"]["deviceTest"], DEVICE_TEST);
210 assert_eq!(contract["routes"]["deviceListenSet"], DEVICE_LISTEN_SET);
211 assert_eq!(
212 contract["routes"]["smartSpeakerSnapshot"],
213 SMART_SPEAKER_SNAPSHOT
214 );
215 assert_eq!(
216 contract["routes"]["smartSpeakerCreate"],
217 SMART_SPEAKER_CREATE
218 );
219 assert_eq!(
220 contract["routes"]["smartSpeakerUpdate"],
221 SMART_SPEAKER_UPDATE
222 );
223 assert_eq!(
224 contract["routes"]["smartSpeakerRemove"],
225 SMART_SPEAKER_REMOVE
226 );
227 assert_eq!(contract["agentReplyTarget"], AGENT_REPLY_TARGET);
228 assert_eq!(
229 contract["agentAudioSubmitTarget"],
230 crate::node::contracts::agent_audio::SUBMIT_TARGET
231 );
232 }
233
234 #[test]
235 fn public_contract_does_not_expose_platform_identity() {
236 let value = serde_json::to_value(AudioBridgeDevice {
237 device_id: "device-1".to_string(),
238 display_name: "Living Room Speaker".to_string(),
239 transport: "bluetooth".to_string(),
240 features: vec![AudioDeviceFeature::Speaker, AudioDeviceFeature::Microphone],
241 online: true,
242 listen_enabled: false,
243 })
244 .expect("serialize device");
245 assert!(value.get("platformKey").is_none());
246 assert!(value.get("endpointId").is_none());
247 assert_eq!(
248 value["features"],
249 serde_json::json!(["speaker", "microphone"])
250 );
251 }
252
253 #[test]
254 fn device_snapshot_is_the_direct_response_payload() {
255 let value = serde_json::to_value(DeviceSnapshot {
256 microphone_permission: MicrophonePermission::NotDetermined,
257 devices: Vec::new(),
258 candidates: Vec::new(),
259 })
260 .expect("serialize device snapshot");
261 assert!(value.get("devices").is_some());
262 assert!(value.get("candidates").is_some());
263 assert!(value.get("snapshot").is_none());
264 }
265
266 #[test]
267 fn smart_speaker_contract_keeps_input_and_output_bindings_explicit() {
268 let value = serde_json::to_value(SmartSpeakerCreateRequest {
269 display_name: "Kitchen".to_string(),
270 input_device_id: "microphone-1".to_string(),
271 output_device_id: "speaker-1".to_string(),
272 enabled: true,
273 })
274 .expect("serialize smart speaker request");
275 assert_eq!(value["inputDeviceId"], "microphone-1");
276 assert_eq!(value["outputDeviceId"], "speaker-1");
277 assert_eq!(value["enabled"], true);
278 }
279}