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.
- aamio_earshot-0.3.0/LICENSE +21 -0
- aamio_earshot-0.3.0/PKG-INFO +146 -0
- aamio_earshot-0.3.0/README.md +128 -0
- aamio_earshot-0.3.0/pyproject.toml +39 -0
- aamio_earshot-0.3.0/setup.cfg +4 -0
- aamio_earshot-0.3.0/src/aamio_earshot/__init__.py +25 -0
- aamio_earshot-0.3.0/src/aamio_earshot/aamio-addon.json +101 -0
- aamio_earshot-0.3.0/src/aamio_earshot/audio.py +150 -0
- aamio_earshot-0.3.0/src/aamio_earshot/card.py +100 -0
- aamio_earshot-0.3.0/src/aamio_earshot/codec.py +635 -0
- aamio_earshot-0.3.0/src/aamio_earshot/process.py +353 -0
- aamio_earshot-0.3.0/src/aamio_earshot/service.py +134 -0
- aamio_earshot-0.3.0/src/aamio_earshot.egg-info/PKG-INFO +146 -0
- aamio_earshot-0.3.0/src/aamio_earshot.egg-info/SOURCES.txt +16 -0
- aamio_earshot-0.3.0/src/aamio_earshot.egg-info/dependency_links.txt +1 -0
- aamio_earshot-0.3.0/src/aamio_earshot.egg-info/entry_points.txt +2 -0
- aamio_earshot-0.3.0/src/aamio_earshot.egg-info/requires.txt +2 -0
- aamio_earshot-0.3.0/src/aamio_earshot.egg-info/top_level.txt +1 -0
|
@@ -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,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)
|