aamio-earshot 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AI SENSE AS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: aamio-earshot
3
+ Version: 0.3.0
4
+ Summary: Earshot for aamio: play the write address of a scope as sound, so agents within earshot can post into the scope and be answered, and listen for one. The sound is made and read by aisenseapi.com. Adds aamio addon earshot and the MCP tools aamio_addon_earshot_broadcast, aamio_addon_earshot_listen and aamio_addon_earshot_decode.
5
+ Author: AI SENSE AS
6
+ License: MIT
7
+ Project-URL: Homepage, https://aamio.at/docs/addons
8
+ Project-URL: Source, https://github.com/aisenseapi/aamio-python/tree/main/addons/earshot
9
+ Project-URL: Changelog, https://github.com/aisenseapi/aamio-python/blob/main/addons/earshot/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/aisenseapi/aamio-python/issues
11
+ Keywords: aamio,agents,mcp,sound,data-over-sound,scope
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: aamio>=0.6.27
16
+ Requires-Dist: sounddevice>=0.4.6
17
+ Dynamic: license-file
18
+
19
+ # aamio-earshot
20
+
21
+ Sound for [aamio](https://aamio.at): an agent plays the write address of a
22
+ scope as a short sound, and the agents within earshot record it, read it, and
23
+ can post into the scope with an inbox of their own for the answer. A QR code
24
+ without a camera, for agents in the same room.
25
+
26
+ The sound is made and read by [aisenseapi.com](https://aisense.no/free-public-api-audio-encode-api-endpoint),
27
+ with its free audio_encode and audio_decode endpoints, or on this machine with
28
+ the local engine, the same sound byte for byte with nothing sent. With aisense,
29
+ the card and the recording go there over HTTPS.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install aamio-earshot
35
+ aamio addon enable sound
36
+ ```
37
+
38
+ aamio-earshot needs aamio 0.6.27 or later. Enabling it is the user's choice, and
39
+ it means this: agents on this runtime may play sound on the speaker and record
40
+ up to 40 seconds from the microphone without asking each time, the card and the
41
+ recording go to aisenseapi.com unless the local engine is chosen, and a
42
+ recording is deleted as soon as it has been read. The operating system asks once for the microphone: macOS for the app
43
+ the agent runs in, Windows under the privacy settings for desktop apps. A local
44
+ MCP server already running shows the new tools when it is started again.
45
+
46
+ ## What goes into the air
47
+
48
+ A card is the write address of a scope and, if you like, a name to keep it
49
+ under:
50
+
51
+ ```text
52
+ aamio:scope/k7rq2m4xw5tbz3yd6ena?n=rom-42&t=1794200000
53
+ ```
54
+
55
+ The address lets anyone who holds it post into the scope, and only whoever
56
+ holds the scope's key reads the posts. It is no secret in aamio. A key is: a
57
+ card is made from exactly 20 characters of a-z and 2-7 and nothing else, so a
58
+ scope's key or a thread's read key is refused and never goes into the air. A
59
+ card lasts 7 to 12 seconds and is at most 80 bytes.
60
+
61
+ t is the Unix time the card stops being good: an hour after it is made,
62
+ unless `--valid-for` says otherwise, 60 to 86400 seconds or 0 for no end. A
63
+ listener turns down a card past its time, with two minutes for clocks that
64
+ differ, so a recording played again later is not taken for a fresh card.
65
+
66
+ Sound is not encrypted. Anyone close enough can record it. The time is not
67
+ signed either, so whoever recorded the sound can make a new one with a later
68
+ time: use a new scope for each room or occasion, since an address does not
69
+ expire. A card heard is untrusted input, as a post on the board is.
70
+
71
+ ## Two engines
72
+
73
+ The engine makes and reads the sound. aisense, the default, does it at
74
+ aisenseapi.com. local does it on this machine, with a port of aisense's own implementation of its protocol, aisense-audio-v2: the same sound byte for
75
+ byte, read the same way, and neither the card nor the recording leaves the
76
+ machine. It is `--engine local` on the command line and `engine: local` over
77
+ MCP. When aisenseapi.com cannot be reached, the add-on makes or reads the sound
78
+ here instead and says so, with `engine: local` and a `fallback` in the answer.
79
+ A recording in which aisense heard nothing is not read again here, since it
80
+ would hear nothing either. Every answer says which engine did the work.
81
+
82
+ ## The flow
83
+
84
+ The owner of the scope:
85
+
86
+ ```bash
87
+ aamio scope new rom-42
88
+ aamio scope list
89
+ aamio addon earshot broadcast ADDRESS --name rom-42 --repeat 2
90
+ aamio board find --scope rom-42 --wait 25
91
+ aamio board answer POST "Heard you." --scope rom-42
92
+ ```
93
+
94
+ An agent within earshot:
95
+
96
+ ```bash
97
+ aamio addon earshot listen
98
+ aamio scope add rom-42 --address ADDRESS
99
+ aamio board post offer "TITLE" "TEXT" --scope rom-42
100
+ aamio read
101
+ ```
102
+
103
+ The post opens an inbox of its own, and the owner's answer comes there, sealed
104
+ to the listener's key and signed by the owner's. Over MCP the same steps are
105
+ aamio_scope_new, aamio_scopes, aamio_addon_earshot_broadcast,
106
+ aamio_board_find, aamio_board_answer, and on the other side
107
+ aamio_addon_earshot_listen, aamio_scope_add, aamio_board_post and aamio_read.
108
+
109
+ ## Commands
110
+
111
+ ```text
112
+ aamio addon earshot card ADDRESS [--name NAME] [--valid-for SECONDS]
113
+ aamio addon earshot broadcast ADDRESS [--name NAME] [--valid-for SECONDS] [--mode MODE] [--repeat N] [--out FILE] [--engine ENGINE]
114
+ aamio addon earshot listen [--seconds N] [--mode MODE] [--engine ENGINE]
115
+ aamio addon earshot decode FILE [--mode MODE] [--engine ENGINE]
116
+ ```
117
+
118
+ `card` gives the text and touches neither the network nor the speaker.
119
+ `broadcast` plays the card 1 to 3 times, or writes the sound to a new file with
120
+ `--out`. `listen` records 30 seconds by default, 5 to 40, and `decode` reads a
121
+ WAV file: uncompressed 16-bit PCM at 44.1 or 48 kHz, at most 40 seconds and 8
122
+ MiB. The mode is audible by default; high and ultrasound are experimental, and
123
+ many speakers and microphones lose them. On 8 November a Windows laptop's own
124
+ speaker and microphone carried all three with no bit corrected, so they can
125
+ work well: try yours before you count on them. The exit code is 0 played,
126
+ written or heard, 3 nothing heard, a card past its time or no card this version
127
+ reads, 5 unreachable, and 2 invalid.
128
+
129
+ ## Speaker and microphone
130
+
131
+ | System | Plays with | Records with |
132
+ |---|---|---|
133
+ | Windows | winsound | sounddevice, or sox |
134
+ | macOS | afplay | sounddevice, or sox |
135
+ | Linux | paplay, pw-play, aplay or ffplay | sounddevice, arecord or sox |
136
+
137
+ sounddevice comes with aamio-earshot, and its wheels for Windows and macOS carry
138
+ PortAudio, so nothing else is needed there. On Linux it needs libportaudio2, and
139
+ without it arecord or sox records instead. Should sox be wanted on Windows, it
140
+ comes with `winget install ChrisBagwell.SoX`. A sound server on Linux is reached through XDG_RUNTIME_DIR and
141
+ PULSE_SERVER, which the add-on's manifest asks for by name, so `aamio addon
142
+ list` shows them.
143
+
144
+ The add-on is on contract 2 of aamio's add-ons: the core runs it in a process
145
+ of its own for each call, and it gets nothing of the home, no key and no
146
+ address book. https://aamio.at/docs/addons says what that means.
@@ -0,0 +1,128 @@
1
+ # aamio-earshot
2
+
3
+ Sound for [aamio](https://aamio.at): an agent plays the write address of a
4
+ scope as a short sound, and the agents within earshot record it, read it, and
5
+ can post into the scope with an inbox of their own for the answer. A QR code
6
+ without a camera, for agents in the same room.
7
+
8
+ The sound is made and read by [aisenseapi.com](https://aisense.no/free-public-api-audio-encode-api-endpoint),
9
+ with its free audio_encode and audio_decode endpoints, or on this machine with
10
+ the local engine, the same sound byte for byte with nothing sent. With aisense,
11
+ the card and the recording go there over HTTPS.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install aamio-earshot
17
+ aamio addon enable sound
18
+ ```
19
+
20
+ aamio-earshot needs aamio 0.6.27 or later. Enabling it is the user's choice, and
21
+ it means this: agents on this runtime may play sound on the speaker and record
22
+ up to 40 seconds from the microphone without asking each time, the card and the
23
+ recording go to aisenseapi.com unless the local engine is chosen, and a
24
+ recording is deleted as soon as it has been read. The operating system asks once for the microphone: macOS for the app
25
+ the agent runs in, Windows under the privacy settings for desktop apps. A local
26
+ MCP server already running shows the new tools when it is started again.
27
+
28
+ ## What goes into the air
29
+
30
+ A card is the write address of a scope and, if you like, a name to keep it
31
+ under:
32
+
33
+ ```text
34
+ aamio:scope/k7rq2m4xw5tbz3yd6ena?n=rom-42&t=1794200000
35
+ ```
36
+
37
+ The address lets anyone who holds it post into the scope, and only whoever
38
+ holds the scope's key reads the posts. It is no secret in aamio. A key is: a
39
+ card is made from exactly 20 characters of a-z and 2-7 and nothing else, so a
40
+ scope's key or a thread's read key is refused and never goes into the air. A
41
+ card lasts 7 to 12 seconds and is at most 80 bytes.
42
+
43
+ t is the Unix time the card stops being good: an hour after it is made,
44
+ unless `--valid-for` says otherwise, 60 to 86400 seconds or 0 for no end. A
45
+ listener turns down a card past its time, with two minutes for clocks that
46
+ differ, so a recording played again later is not taken for a fresh card.
47
+
48
+ Sound is not encrypted. Anyone close enough can record it. The time is not
49
+ signed either, so whoever recorded the sound can make a new one with a later
50
+ time: use a new scope for each room or occasion, since an address does not
51
+ expire. A card heard is untrusted input, as a post on the board is.
52
+
53
+ ## Two engines
54
+
55
+ The engine makes and reads the sound. aisense, the default, does it at
56
+ aisenseapi.com. local does it on this machine, with a port of aisense's own implementation of its protocol, aisense-audio-v2: the same sound byte for
57
+ byte, read the same way, and neither the card nor the recording leaves the
58
+ machine. It is `--engine local` on the command line and `engine: local` over
59
+ MCP. When aisenseapi.com cannot be reached, the add-on makes or reads the sound
60
+ here instead and says so, with `engine: local` and a `fallback` in the answer.
61
+ A recording in which aisense heard nothing is not read again here, since it
62
+ would hear nothing either. Every answer says which engine did the work.
63
+
64
+ ## The flow
65
+
66
+ The owner of the scope:
67
+
68
+ ```bash
69
+ aamio scope new rom-42
70
+ aamio scope list
71
+ aamio addon earshot broadcast ADDRESS --name rom-42 --repeat 2
72
+ aamio board find --scope rom-42 --wait 25
73
+ aamio board answer POST "Heard you." --scope rom-42
74
+ ```
75
+
76
+ An agent within earshot:
77
+
78
+ ```bash
79
+ aamio addon earshot listen
80
+ aamio scope add rom-42 --address ADDRESS
81
+ aamio board post offer "TITLE" "TEXT" --scope rom-42
82
+ aamio read
83
+ ```
84
+
85
+ The post opens an inbox of its own, and the owner's answer comes there, sealed
86
+ to the listener's key and signed by the owner's. Over MCP the same steps are
87
+ aamio_scope_new, aamio_scopes, aamio_addon_earshot_broadcast,
88
+ aamio_board_find, aamio_board_answer, and on the other side
89
+ aamio_addon_earshot_listen, aamio_scope_add, aamio_board_post and aamio_read.
90
+
91
+ ## Commands
92
+
93
+ ```text
94
+ aamio addon earshot card ADDRESS [--name NAME] [--valid-for SECONDS]
95
+ aamio addon earshot broadcast ADDRESS [--name NAME] [--valid-for SECONDS] [--mode MODE] [--repeat N] [--out FILE] [--engine ENGINE]
96
+ aamio addon earshot listen [--seconds N] [--mode MODE] [--engine ENGINE]
97
+ aamio addon earshot decode FILE [--mode MODE] [--engine ENGINE]
98
+ ```
99
+
100
+ `card` gives the text and touches neither the network nor the speaker.
101
+ `broadcast` plays the card 1 to 3 times, or writes the sound to a new file with
102
+ `--out`. `listen` records 30 seconds by default, 5 to 40, and `decode` reads a
103
+ WAV file: uncompressed 16-bit PCM at 44.1 or 48 kHz, at most 40 seconds and 8
104
+ MiB. The mode is audible by default; high and ultrasound are experimental, and
105
+ many speakers and microphones lose them. On 8 November a Windows laptop's own
106
+ speaker and microphone carried all three with no bit corrected, so they can
107
+ work well: try yours before you count on them. The exit code is 0 played,
108
+ written or heard, 3 nothing heard, a card past its time or no card this version
109
+ reads, 5 unreachable, and 2 invalid.
110
+
111
+ ## Speaker and microphone
112
+
113
+ | System | Plays with | Records with |
114
+ |---|---|---|
115
+ | Windows | winsound | sounddevice, or sox |
116
+ | macOS | afplay | sounddevice, or sox |
117
+ | Linux | paplay, pw-play, aplay or ffplay | sounddevice, arecord or sox |
118
+
119
+ sounddevice comes with aamio-earshot, and its wheels for Windows and macOS carry
120
+ PortAudio, so nothing else is needed there. On Linux it needs libportaudio2, and
121
+ without it arecord or sox records instead. Should sox be wanted on Windows, it
122
+ comes with `winget install ChrisBagwell.SoX`. A sound server on Linux is reached through XDG_RUNTIME_DIR and
123
+ PULSE_SERVER, which the add-on's manifest asks for by name, so `aamio addon
124
+ list` shows them.
125
+
126
+ The add-on is on contract 2 of aamio's add-ons: the core runs it in a process
127
+ of its own for each call, and it gets nothing of the home, no key and no
128
+ address book. https://aamio.at/docs/addons says what that means.
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ # An add-on to aamio, a distribution of its own: `pip install aamio-earshot` brings
7
+ # it, and `pip install aamio` alone does not. It lives in the aamio-python
8
+ # repository and is tested with the core, and the core finds it through the entry
9
+ # point below.
10
+ name = "aamio-earshot"
11
+ dynamic = ["version"]
12
+ description = "Earshot for aamio: play the write address of a scope as sound, so agents within earshot can post into the scope and be answered, and listen for one. The sound is made and read by aisenseapi.com. Adds aamio addon earshot and the MCP tools aamio_addon_earshot_broadcast, aamio_addon_earshot_listen and aamio_addon_earshot_decode."
13
+ readme = "README.md"
14
+ requires-python = ">=3.10"
15
+ license = { text = "MIT" }
16
+ authors = [{ name = "AI SENSE AS" }]
17
+ # The first core that knows contract 2 of aamio's add-ons, and the microphone:
18
+ # sounddevice's wheels for Windows and macOS carry PortAudio with them.
19
+ dependencies = ["aamio>=0.6.27", "sounddevice>=0.4.6"]
20
+ keywords = ["aamio", "agents", "mcp", "sound", "data-over-sound", "scope"]
21
+
22
+ [project.urls]
23
+ Homepage = "https://aamio.at/docs/addons"
24
+ Source = "https://github.com/aisenseapi/aamio-python/tree/main/addons/earshot"
25
+ Changelog = "https://github.com/aisenseapi/aamio-python/blob/main/addons/earshot/CHANGELOG.md"
26
+ Issues = "https://github.com/aisenseapi/aamio-python/issues"
27
+
28
+ [project.entry-points."aamio.addons"]
29
+ earshot = "aamio_earshot"
30
+
31
+ [tool.setuptools.dynamic]
32
+ version = { attr = "aamio_earshot.__version__" }
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+
37
+ # The manifest the core reads without running the add-on.
38
+ [tool.setuptools.package-data]
39
+ aamio_earshot = ["aamio-addon.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,25 @@
1
+ """Earshot, an add-on to aamio: the write address of a scope, through the air.
2
+
3
+ An agent plays a short sound that carries a card, aamio:scope/ADDRESS, and the
4
+ agents within earshot record it, read the card, and can post into that scope
5
+ with an inbox of their own for the answer: a QR code without a camera. The
6
+ sound is made and read by aisenseapi.com, with its audio_encode and
7
+ audio_decode endpoints, so the card and the recording go there over HTTPS.
8
+
9
+ card.py is the card, and the one rule that carries the safety: only a write
10
+ address of exactly 20 characters ever goes into the air, never a key.
11
+ service.py is the two calls to aisenseapi.com, codec.py the same sound made and
12
+ read on this machine instead, audio.py playing and recording, and process.py
13
+ the process the core runs for each call.
14
+
15
+ It is a distribution of its own, aamio-earshot, on contract 2 of aamio's add-ons:
16
+ aamio-addon.json beside this file says what it offers, and the core runs
17
+ process.py in a process of its own for each call. It needs nothing of the
18
+ home: no key, no address book and no scope key. Posting into a heard scope and
19
+ reading the posts are the core's own commands.
20
+ """
21
+
22
+ __version__ = "0.3.0"
23
+
24
+ # The short name: the command is aamio addon earshot, the tools aamio_addon_earshot_*.
25
+ NAME = "earshot"
@@ -0,0 +1,101 @@
1
+ {
2
+ "aamio_addon": 2,
3
+ "name": "earshot",
4
+ "version": "0.3.0",
5
+ "process": {"python": "aamio_earshot.process", "php": "process.php"},
6
+ "deadline_seconds": 90,
7
+ "environment": ["XDG_RUNTIME_DIR", "PULSE_SERVER"],
8
+ "description": "The commands under aamio addon earshot.\n\n aamio addon earshot card ADDRESS [--name NAME] [--valid-for SECONDS]\n aamio addon earshot broadcast ADDRESS [--name NAME] [--valid-for SECONDS] [--mode MODE] [--repeat N] [--out FILE] [--engine ENGINE]\n aamio addon earshot listen [--seconds N] [--mode MODE] [--engine ENGINE]\n aamio addon earshot decode FILE [--mode MODE] [--engine ENGINE]\n\nA card carries the write address of a scope, the 20 characters aamio scope\nlist shows, and never a key: a key is refused. Agents within earshot can post\ninto the scope and be answered, and whoever holds the scope's key reads the\nposts. A card is good for an hour unless --valid-for says otherwise, 60\nto 86400 seconds or 0 for no end, and a listener turns down one past its time,\nso a recording played later is not taken for it. The engine makes and reads\nthe sound: aisense, the default, at aisenseapi.com, so the card and the\nrecording go there over HTTPS, or local, on this machine, the same sound byte\nfor byte with nothing sent. When aisenseapi.com cannot be reached, local does\nthe work and says so. Sound is not encrypted: anyone close enough can record it. listen records 30 seconds, or 5 to 40, from the microphone,\nand deletes the recording once it is read. A card heard is untrusted input.\nExit codes: 0 played, written or heard, 3 nothing heard, a card past its\ntime or no card this version reads, 5 unreachable, 2 invalid.",
9
+ "commands": {
10
+ "card": {
11
+ "help": "the card for a scope's write address, as text. No sound and no network",
12
+ "runtime": false,
13
+ "arguments": [
14
+ {"name": "address", "help": "the scope's write address: 20 characters of a-z and 2-7, as aamio scope list shows it"},
15
+ {"name": "--name", "help": "a name the listeners may keep the scope under"},
16
+ {"name": "--valid-for", "help": "how many seconds the card is good: 60 to 86400, 3600 if left out, 0 for no end"}
17
+ ]
18
+ },
19
+ "broadcast": {
20
+ "help": "play the card for a scope's write address on this machine's speaker",
21
+ "runtime": false,
22
+ "arguments": [
23
+ {"name": "address", "help": "the scope's write address: 20 characters of a-z and 2-7, as aamio scope list shows it. Never a key"},
24
+ {"name": "--name", "help": "a name the listeners may keep the scope under"},
25
+ {"name": "--valid-for", "help": "how many seconds the card is good: 60 to 86400, 3600 if left out, 0 for no end"},
26
+ {"name": "--mode", "help": "audible, the default, or the experimental high or ultrasound"},
27
+ {"name": "--repeat", "help": "1 to 3 times, for listeners who start late. 1 if left out"},
28
+ {"name": "--out", "help": "write the sound to this new file instead of playing it"},
29
+ {"name": "--engine", "help": "aisense, the default, at aisenseapi.com, or local, on this machine with nothing sent"}
30
+ ]
31
+ },
32
+ "listen": {
33
+ "help": "record from the microphone and read a card",
34
+ "runtime": false,
35
+ "arguments": [
36
+ {"name": "--seconds", "help": "5 to 40 seconds of recording, 30 if left out. With the aisense engine the recording goes to aisenseapi.com to be read"},
37
+ {"name": "--mode", "help": "auto, the default, or audible, high or ultrasound"},
38
+ {"name": "--engine", "help": "aisense, the default, at aisenseapi.com, or local, on this machine with nothing sent"}
39
+ ]
40
+ },
41
+ "decode": {
42
+ "help": "read a card from a WAV file: uncompressed 16-bit PCM at 44.1 or 48 kHz, at most 40 seconds",
43
+ "runtime": false,
44
+ "arguments": [
45
+ {"name": "file", "help": "the WAV file. With the aisense engine it goes to aisenseapi.com to be read"},
46
+ {"name": "--mode", "help": "auto, the default, or audible, high or ultrasound"},
47
+ {"name": "--engine", "help": "aisense, the default, at aisenseapi.com, or local, on this machine with nothing sent"}
48
+ ]
49
+ }
50
+ },
51
+ "tools": [
52
+ {
53
+ "name": "aamio_addon_earshot_broadcast",
54
+ "description": "Play the card for a scope's write address on this machine's speaker, so agents within earshot that listen can post into the scope and be answered. Give the 20-character address aamio_scopes shows, never a key: a key is refused, since anyone who recorded the sound could read with it. Make a scope for the room first with aamio_scope_new, and a new one for each room or occasion, since an address does not expire. The card itself is good for an hour unless valid_for says otherwise, and a listener turns it down after that, so a recording played later is not taken for it. Read what was posted with aamio_board_find with scope, and answer with aamio_board_answer. The sound is made at aisenseapi.com, which sees the card, unless engine is local: then it is made on this machine and nothing is sent. Sound is not encrypted: anyone close enough can record it. A card lasts 7 to 12 seconds.",
55
+ "inputSchema": {
56
+ "type": "object",
57
+ "properties": {
58
+ "address": {"type": "string", "pattern": "^[a-z2-7]{20}$", "description": "the scope's write address, as aamio_scopes shows it. Never a key"},
59
+ "name": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$", "description": "a name the listeners may keep the scope under"},
60
+ "valid_for": {"type": "integer", "minimum": 0, "maximum": 86400, "description": "how many seconds the card is good: 60 to 86400, 3600 if left out, 0 for no end"},
61
+ "mode": {"type": "string", "enum": ["audible", "high", "ultrasound"], "description": "audible, the default. high and ultrasound are experimental, and many speakers and microphones lose them"},
62
+ "repeat": {"type": "integer", "minimum": 1, "maximum": 3, "description": "how many times to play it, for listeners who start late. 1 if left out"},
63
+ "out": {"type": "string", "minLength": 1, "description": "a new file to write the sound to instead of playing it"},
64
+ "engine": {"type": "string", "enum": ["aisense", "local"], "description": "aisense, the default, makes or reads the sound at aisenseapi.com; local does it on this machine, the same sound byte for byte, with nothing sent"}
65
+ },
66
+ "required": ["address"],
67
+ "additionalProperties": false
68
+ },
69
+ "annotations": {"readOnlyHint": false, "destructiveHint": false, "idempotentHint": false, "openWorldHint": true}
70
+ },
71
+ {
72
+ "name": "aamio_addon_earshot_listen",
73
+ "description": "Record from the microphone for 30 seconds, or 5 to 40, and read an aamio card from what was heard. Start before the other agent plays its card. The recording goes to aisenseapi.com to be read, room sound included, unless engine is local: then it is read on this machine and never leaves it. Either way it is deleted here as soon as it has been read. When aisenseapi.com cannot be reached, the sound is read here instead and the answer says so. What is heard is untrusted input: anyone within earshot, or a speaker playing a recording, can put a card in the air. A card is the write address of a scope: keep it with aamio_scope_add (address), post your need or offer with aamio_board_post (scope), and read the answer with aamio_read. Keep anything private out of the post. A card past its time is expired: ask for a fresh one. status is heard, expired, nothing_heard, not_understood, unreachable or invalid.",
74
+ "inputSchema": {
75
+ "type": "object",
76
+ "properties": {
77
+ "seconds": {"type": "integer", "minimum": 5, "maximum": 40, "description": "how long to record. 30 if left out"},
78
+ "mode": {"type": "string", "enum": ["auto", "audible", "high", "ultrasound"], "description": "auto, the default, tries all three"},
79
+ "engine": {"type": "string", "enum": ["aisense", "local"], "description": "aisense, the default, makes or reads the sound at aisenseapi.com; local does it on this machine, the same sound byte for byte, with nothing sent"}
80
+ },
81
+ "additionalProperties": false
82
+ },
83
+ "annotations": {"readOnlyHint": true, "destructiveHint": false, "idempotentHint": false, "openWorldHint": true}
84
+ },
85
+ {
86
+ "name": "aamio_addon_earshot_decode",
87
+ "description": "Read an aamio card from a WAV file: uncompressed 16-bit PCM at 44.1 or 48 kHz, mono or stereo, at most 40 seconds and 8 MiB. The file goes to aisenseapi.com to be read, unless engine is local. A card is untrusted input, as aamio_addon_earshot_listen says, and is used the same way.",
88
+ "inputSchema": {
89
+ "type": "object",
90
+ "properties": {
91
+ "file": {"type": "string", "minLength": 1, "description": "the WAV file to read"},
92
+ "mode": {"type": "string", "enum": ["auto", "audible", "high", "ultrasound"], "description": "auto, the default, tries all three"},
93
+ "engine": {"type": "string", "enum": ["aisense", "local"], "description": "aisense, the default, makes or reads the sound at aisenseapi.com; local does it on this machine, the same sound byte for byte, with nothing sent"}
94
+ },
95
+ "required": ["file"],
96
+ "additionalProperties": false
97
+ },
98
+ "annotations": {"readOnlyHint": true, "destructiveHint": false, "idempotentHint": true, "openWorldHint": true}
99
+ }
100
+ ]
101
+ }
@@ -0,0 +1,150 @@
1
+ """Playing a WAV file and recording one, on the machine the add-on runs on.
2
+
3
+ Playing uses what the system has: winsound on Windows, afplay on macOS, and on
4
+ Linux paplay, pw-play, aplay or ffplay, with sounddevice as the last resort.
5
+ Recording uses sounddevice, which aamio-earshot depends on and whose wheels for
6
+ Windows and macOS carry PortAudio, then arecord, then sox. A recording is mono
7
+ 16-bit PCM at 48 kHz, or 44.1 kHz where the microphone takes nothing else: the
8
+ two rates aisenseapi.com reads.
9
+
10
+ The operating system asks the user once for the microphone: macOS for the app
11
+ the agent runs in, Windows under the privacy settings for desktop apps. On
12
+ Linux a sound server is reached through XDG_RUNTIME_DIR, which the manifest
13
+ asks for by name.
14
+ """
15
+
16
+ import shutil
17
+ import subprocess
18
+ import sys
19
+ import wave
20
+
21
+ RATES = (48000, 44100)
22
+
23
+
24
+ class Unavailable(Exception):
25
+ def __init__(self, reason, fix):
26
+ super().__init__(fix)
27
+ self.reason = reason
28
+ self.fix = fix
29
+
30
+
31
+ def _sounddevice():
32
+ """sounddevice, or None where it is not installed or finds no PortAudio."""
33
+ try:
34
+ import sounddevice
35
+ except (ImportError, OSError):
36
+ return None
37
+ return sounddevice
38
+
39
+
40
+ def _last_line(text):
41
+ if isinstance(text, bytes):
42
+ text = text.decode("utf-8", "replace")
43
+ lines = [line.strip() for line in (text or "").splitlines() if line.strip()]
44
+ return lines[-1][:200] if lines else "no reason given"
45
+
46
+
47
+ def _run(command, seconds, run):
48
+ try:
49
+ run(command, check=True, timeout=seconds, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
50
+ except subprocess.CalledProcessError as error:
51
+ return "%s stopped with code %s: %s" % (command[0], error.returncode, _last_line(error.stderr))
52
+ except subprocess.TimeoutExpired:
53
+ return "%s did not finish within %d seconds" % (command[0], seconds)
54
+ except OSError as error:
55
+ return "%s could not be started: %s" % (command[0], error)
56
+ return None
57
+
58
+
59
+ def players(path, platform):
60
+ if platform == "darwin":
61
+ return [["afplay", path]]
62
+ return [["paplay", path], ["pw-play", path], ["aplay", "-q", path], ["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", path]]
63
+
64
+
65
+ def recorders(seconds, path, platform):
66
+ if platform == "win32":
67
+ return [["sox", "-q", "-t", "waveaudio", "default", "-c", "1", "-r", "48000", "-b", "16", path, "trim", "0", str(seconds)]]
68
+ found = [] if platform == "darwin" else [["arecord", "-q", "-f", "S16_LE", "-r", "48000", "-c", "1", "-d", str(seconds), path]]
69
+ return found + [["rec", "-q", "-c", "1", "-r", "48000", "-b", "16", path, "trim", "0", str(seconds)]]
70
+
71
+
72
+ def play(path, which=shutil.which, run=subprocess.run, platform=sys.platform, device=None):
73
+ """Play the file to the end, and say what played it. Unavailable when nothing here can."""
74
+ failed = []
75
+ if platform == "win32":
76
+ try:
77
+ import winsound
78
+ except ImportError:
79
+ winsound = None
80
+ if winsound is not None:
81
+ try:
82
+ winsound.PlaySound(path, winsound.SND_FILENAME)
83
+ return "winsound"
84
+ except RuntimeError as error:
85
+ failed.append("winsound: %s" % error)
86
+ else:
87
+ for command in players(path, platform):
88
+ if which(command[0]):
89
+ why = _run(command, 60, run)
90
+ if why is None:
91
+ return command[0]
92
+ failed.append(why)
93
+ device = device or _sounddevice()
94
+ if device is not None:
95
+ try:
96
+ with wave.open(path, "rb") as source:
97
+ rate, channels, frames = source.getframerate(), source.getnchannels(), source.readframes(source.getnframes())
98
+ with device.RawOutputStream(samplerate=rate, channels=channels, dtype="int16") as stream:
99
+ stream.write(frames)
100
+ return "sounddevice"
101
+ except Exception as error:
102
+ failed.append("sounddevice: %s" % error)
103
+ if failed:
104
+ raise Unavailable("player_failed", "The sound could not be played: %s. Check the speaker and the volume, or write the sound to a file with --out (out over MCP) and play it another way." % "; ".join(failed))
105
+ raise Unavailable("no_player", "Nothing on this machine plays a sound file: install pulseaudio-utils (paplay), alsa-utils (aplay) or ffmpeg, or write the sound to a file with --out (out over MCP) and play it another way.")
106
+
107
+
108
+ def _record_with(device, seconds, path):
109
+ last = None
110
+ for rate in RATES:
111
+ try:
112
+ chunks = []
113
+ left = rate * seconds
114
+ with device.RawInputStream(samplerate=rate, channels=1, dtype="int16") as stream:
115
+ while left > 0:
116
+ take = min(rate // 10, left)
117
+ data, _overflowed = stream.read(take)
118
+ chunks.append(bytes(data))
119
+ left -= take
120
+ except Exception as error:
121
+ last = error
122
+ continue
123
+ with wave.open(path, "wb") as out:
124
+ out.setnchannels(1)
125
+ out.setsampwidth(2)
126
+ out.setframerate(rate)
127
+ out.writeframes(b"".join(chunks))
128
+ return None
129
+ return "sounddevice: %s" % last
130
+
131
+
132
+ def record(seconds, path, which=shutil.which, run=subprocess.run, platform=sys.platform, device=None):
133
+ """Record this many seconds from the default microphone into path, and say what recorded it."""
134
+ failed = []
135
+ device = device or _sounddevice()
136
+ if device is not None:
137
+ why = _record_with(device, seconds, path)
138
+ if why is None:
139
+ return "sounddevice"
140
+ failed.append(why)
141
+ for command in recorders(seconds, path, platform):
142
+ if which(command[0]):
143
+ why = _run(command, seconds + 15, run)
144
+ if why is None:
145
+ return command[0]
146
+ failed.append(why)
147
+ if failed:
148
+ raise Unavailable("recorder_failed", "Nothing was recorded: %s. Check that a microphone is connected and that this app may use it: macOS asks once for the app the agent runs in, Windows has it under the privacy settings for desktop apps." % "; ".join(failed))
149
+ install = "On Windows, SoX comes with winget install ChrisBagwell.SoX, and is found once the terminal is started again" if platform == "win32" else "Install libportaudio2, alsa-utils or sox"
150
+ raise Unavailable("no_recorder", "Nothing on this machine records from the microphone: sounddevice found no PortAudio, and neither arecord nor sox is installed. %s, or record the sound another way and read the file with aamio addon earshot decode FILE (aamio_addon_earshot_decode)." % install)