rtpbench 0.3.0__tar.gz → 0.4.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.
- {rtpbench-0.3.0 → rtpbench-0.4.0}/PKG-INFO +13 -9
- {rtpbench-0.3.0 → rtpbench-0.4.0}/README.md +12 -8
- {rtpbench-0.3.0 → rtpbench-0.4.0}/pyproject.toml +1 -1
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/bench.py +74 -3
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/cli.py +19 -4
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/realtime.py +2 -1
- rtpbench-0.4.0/targets/aivc/source.yaml +9 -0
- rtpbench-0.4.0/targets/ava/source.yaml +8 -0
- rtpbench-0.4.0/targets/livekit/source.yaml +10 -0
- rtpbench-0.4.0/tests/test_images.py +124 -0
- rtpbench-0.3.0/tests/test_images.py +0 -64
- {rtpbench-0.3.0 → rtpbench-0.4.0}/.dockerignore +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/.gitignore +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/Dockerfile +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/LICENSE +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/carrier/compose.yaml +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/carrier/opensips.cfg +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/carrier/tls.cfg +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/carrier/tls.yaml +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/compose.yaml +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/README.md +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/cough-c1-0.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/cough-c1-1.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/cough-c2-0.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/cough-c2-1.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/cough-man-0.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/cough-man-1.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/cough-short-0.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/cough-short-1.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/cough-woman_three_times-0.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/human-en-dr-0.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/human-en-dr-1.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/human-en-sc-0.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/human-en-sc-1.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/human-en-sc-2.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/human-es-f012-0.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/human-es-f012-1.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/human-es-f012-2.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/human-es-f016-0.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/human-es-f016-1.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/human-es-f016-2.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/interrupt-00.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/interrupt-01.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/interrupt-02.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/interrupt-03.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/prompt-00.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/prompt-01.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/prompt-02.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/prompt-03.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/prompt-04.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/prompt-05.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/prompt-06.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/prompt-07.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/prompt-08.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/prompt-09.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/prompt.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/transfer.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/turn-00.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/turn-01.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/turn-02.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/turn-03.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/turn-04.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/turn-05.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/turn-06.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/turn-07.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/turn-08.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/turn-09.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/turn-10.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/lines/en/turn-11.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-ald-bc.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-ald-long.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-ald-s03.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-ald-s07.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-ald-s11.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-cori-bc.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-cori-long.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-cori-s01.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-cori-s05.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-cori-s09.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-davefx-bc.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-davefx-long.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-davefx-s00.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-davefx-s04.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-davefx-s08.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-libritts0-bc.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-libritts0-long.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-libritts0-s02.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-libritts0-s06.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-libritts0-s10.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-libritts3-bc.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-libritts3-long.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-libritts3-s03.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-libritts3-s07.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-libritts3-s11.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-ljspeech-bc.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-ljspeech-long.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-ljspeech-s00.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-ljspeech-s04.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-ljspeech-s08.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-sharvardf-bc.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-sharvardf-long.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-sharvardf-s02.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-sharvardf-s06.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-sharvardf-s10.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-sharvardm-bc.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-sharvardm-long.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-sharvardm-s01.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-sharvardm-s05.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/corpus/tts-sharvardm-s09.wav +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/mock/compose.yaml +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/__init__.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/__main__.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/agent.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/analyze.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/bargein.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/caller.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/capture.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/cases.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/check.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/clock.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/detect.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/g711.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/pcap.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/progress.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/report.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/rtp.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/sip.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/speech.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/speechcal.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/srtp.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/tls.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/src/rtpbench/voice.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/targets/aivc/aivc.ini +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/targets/aivc/compose.yaml +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/targets/aivc/constraints.txt +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/targets/aivc/opensips.cfg +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/targets/ava/ai-agent.yaml +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/targets/ava/ari.conf +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/targets/ava/compose.yaml +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/targets/ava/constraints.txt +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/targets/ava/extensions.conf +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/targets/ava/http.conf +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/targets/ava/pjsip.conf +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/targets/ava/websocket_client.conf +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/targets/livekit/agent.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/targets/livekit/compose.yaml +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/targets/livekit/constraints.txt +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/tests/test_bargein.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/tests/test_cases.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/tests/test_check.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/tests/test_cli.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/tests/test_clock.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/tests/test_progress.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/tests/test_realtime.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/tests/test_report.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/tests/test_speech.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/tests/test_speechcal.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/tests/test_srtp.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/tests/test_timing.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/tests/test_tls.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/tests/test_wheel.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/tests/test_wire.py +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/uri/advertise.yaml +0 -0
- {rtpbench-0.3.0 → rtpbench-0.4.0}/uri/network.yaml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: rtpbench
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Wire-level latency and conformance tests for voice agents over SIP/RTP
|
|
5
5
|
Author-email: Juanjo Aroztegi <juanjo@aroztegi.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -33,11 +33,12 @@ without a carrier in front, on a mock engine that holds the AI engine's share of
|
|
|
33
33
|
| | [LiveKit](https://github.com/livekit/sip) | [AI Voice Connector CE](https://github.com/OpenSIPS/opensips-ai-voice-connector-ce) at b95f5af | Asterisk + [AVA](https://github.com/hkjarral/AVA-AI-Voice-Agent-for-Asterisk) |
|
|
34
34
|
| --- | --- | --- | --- |
|
|
35
35
|
| cases failed of 24 | 5, and 2 unsupported | 5, 3 unsupported and 1 inconclusive | 4, and 3 inconclusive |
|
|
36
|
-
| turn latency, clean, p50 | 1833 ms | 1511 ms |
|
|
36
|
+
| turn latency, clean, p50 | 1833 ms | 1511 ms | 1589 ms |
|
|
37
37
|
| its own media path per turn, p50 | 335 ms | 9 ms | 88 ms |
|
|
38
38
|
| stops for an interruption, clean, p50 | 306 ms | 24 ms | never |
|
|
39
39
|
|
|
40
|
-
The counts take every profile, direct and through the carrier
|
|
40
|
+
The counts take every profile, direct and through the carrier, and AVA as it ships, over
|
|
41
|
+
AudioSocket. Run direct only, as
|
|
41
42
|
`target NAME --mock` does by default, LiveKit fails 4: `late-offer` fails only through the
|
|
42
43
|
carrier. All three pass the 3 cases for lost SIP messages, run direct on a clean network.
|
|
43
44
|
Unsupported is an optional feature declined as its RFC allows, with the call unharmed:
|
|
@@ -47,7 +48,8 @@ the digit never reaches the engine: a failure.
|
|
|
47
48
|
|
|
48
49
|
- LiveKit keeps sending audio to a held caller in every kind of hold, AI Voice Connector CE when
|
|
49
50
|
held with `c=0.0.0.0`, and both answer a re-INVITE that changes codec with the old codec. AVA
|
|
50
|
-
sends RTP only while it speaks, so whether its media resumes after a hold cannot be judged
|
|
51
|
+
sends RTP only while it speaks, so whether its media resumes after a hold cannot be judged,
|
|
52
|
+
except over Asterisk's WebSocket channel, where Asterisk keeps sending to a held caller.
|
|
51
53
|
- LiveKit's extra latency is its own media path: 326 ms per turn more than AI Voice Connector
|
|
52
54
|
CE's, against 322 ms between their turn latencies.
|
|
53
55
|
- LiveKit's RTP falls 20 ms behind its own timestamps 4 s into every call, so a receiver needs a
|
|
@@ -55,10 +57,12 @@ the digit never reaches the engine: a failure.
|
|
|
55
57
|
- AI Voice Connector CE delivers the engine's speech sample for sample. LiveKit re-encodes it on
|
|
56
58
|
the way, through its WebRTC room, and on a clean network stretches or cuts a pause by 20-40 ms
|
|
57
59
|
in 6% of answers (10% in a second wave).
|
|
58
|
-
- AVA, as its golden configuration ships, cuts the last 0.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
- AVA, as its golden configuration ships, cuts the last 0.6 s of speech from every answer, over
|
|
61
|
+
each of its three transports, and over Asterisk's WebSocket channel its pacer also breaks every
|
|
62
|
+
answer with 7-10 short silences. It never stops for an interruption: it keeps the caller's
|
|
63
|
+
audio from the engine while it speaks. Over AudioSocket under packet loss, the engine sometimes
|
|
64
|
+
got silence instead of the caller (a squelch AVA has fixed since), so 5 more cases flip now
|
|
65
|
+
and then.
|
|
62
66
|
- AI Voice Connector CE's main branch fixed 4 of the 10 cases it failed in the baseline (a failed
|
|
63
67
|
transfer no longer hangs up on the caller; compact headers, a bare `a=ssrc` and SIP over TCP
|
|
64
68
|
are taken), and now declines a DTMF INFO with 501 where it left it unanswered. It still
|
|
@@ -101,7 +105,7 @@ defaults and valid names.
|
|
|
101
105
|
|
|
102
106
|
- [Calibration](docs/calibration.md): error bars, clock steps, network profiles and their sources
|
|
103
107
|
- [Conformance cases](docs/cases.md): what each case checks
|
|
104
|
-
- [Targets, your own agent, the mock engine and the carrier](docs/targets.md)
|
|
108
|
+
- [Targets (and adding one), your own agent, the mock engine and the carrier](docs/targets.md)
|
|
105
109
|
- [CI](docs/ci.md): pass or fail your agent's run against a policy
|
|
106
110
|
|
|
107
111
|
## Development
|
|
@@ -18,11 +18,12 @@ without a carrier in front, on a mock engine that holds the AI engine's share of
|
|
|
18
18
|
| | [LiveKit](https://github.com/livekit/sip) | [AI Voice Connector CE](https://github.com/OpenSIPS/opensips-ai-voice-connector-ce) at b95f5af | Asterisk + [AVA](https://github.com/hkjarral/AVA-AI-Voice-Agent-for-Asterisk) |
|
|
19
19
|
| --- | --- | --- | --- |
|
|
20
20
|
| cases failed of 24 | 5, and 2 unsupported | 5, 3 unsupported and 1 inconclusive | 4, and 3 inconclusive |
|
|
21
|
-
| turn latency, clean, p50 | 1833 ms | 1511 ms |
|
|
21
|
+
| turn latency, clean, p50 | 1833 ms | 1511 ms | 1589 ms |
|
|
22
22
|
| its own media path per turn, p50 | 335 ms | 9 ms | 88 ms |
|
|
23
23
|
| stops for an interruption, clean, p50 | 306 ms | 24 ms | never |
|
|
24
24
|
|
|
25
|
-
The counts take every profile, direct and through the carrier
|
|
25
|
+
The counts take every profile, direct and through the carrier, and AVA as it ships, over
|
|
26
|
+
AudioSocket. Run direct only, as
|
|
26
27
|
`target NAME --mock` does by default, LiveKit fails 4: `late-offer` fails only through the
|
|
27
28
|
carrier. All three pass the 3 cases for lost SIP messages, run direct on a clean network.
|
|
28
29
|
Unsupported is an optional feature declined as its RFC allows, with the call unharmed:
|
|
@@ -32,7 +33,8 @@ the digit never reaches the engine: a failure.
|
|
|
32
33
|
|
|
33
34
|
- LiveKit keeps sending audio to a held caller in every kind of hold, AI Voice Connector CE when
|
|
34
35
|
held with `c=0.0.0.0`, and both answer a re-INVITE that changes codec with the old codec. AVA
|
|
35
|
-
sends RTP only while it speaks, so whether its media resumes after a hold cannot be judged
|
|
36
|
+
sends RTP only while it speaks, so whether its media resumes after a hold cannot be judged,
|
|
37
|
+
except over Asterisk's WebSocket channel, where Asterisk keeps sending to a held caller.
|
|
36
38
|
- LiveKit's extra latency is its own media path: 326 ms per turn more than AI Voice Connector
|
|
37
39
|
CE's, against 322 ms between their turn latencies.
|
|
38
40
|
- LiveKit's RTP falls 20 ms behind its own timestamps 4 s into every call, so a receiver needs a
|
|
@@ -40,10 +42,12 @@ the digit never reaches the engine: a failure.
|
|
|
40
42
|
- AI Voice Connector CE delivers the engine's speech sample for sample. LiveKit re-encodes it on
|
|
41
43
|
the way, through its WebRTC room, and on a clean network stretches or cuts a pause by 20-40 ms
|
|
42
44
|
in 6% of answers (10% in a second wave).
|
|
43
|
-
- AVA, as its golden configuration ships, cuts the last 0.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
- AVA, as its golden configuration ships, cuts the last 0.6 s of speech from every answer, over
|
|
46
|
+
each of its three transports, and over Asterisk's WebSocket channel its pacer also breaks every
|
|
47
|
+
answer with 7-10 short silences. It never stops for an interruption: it keeps the caller's
|
|
48
|
+
audio from the engine while it speaks. Over AudioSocket under packet loss, the engine sometimes
|
|
49
|
+
got silence instead of the caller (a squelch AVA has fixed since), so 5 more cases flip now
|
|
50
|
+
and then.
|
|
47
51
|
- AI Voice Connector CE's main branch fixed 4 of the 10 cases it failed in the baseline (a failed
|
|
48
52
|
transfer no longer hangs up on the caller; compact headers, a bare `a=ssrc` and SIP over TCP
|
|
49
53
|
are taken), and now declines a DTMF INFO with 501 where it left it unanswered. It still
|
|
@@ -86,7 +90,7 @@ defaults and valid names.
|
|
|
86
90
|
|
|
87
91
|
- [Calibration](docs/calibration.md): error bars, clock steps, network profiles and their sources
|
|
88
92
|
- [Conformance cases](docs/cases.md): what each case checks
|
|
89
|
-
- [Targets, your own agent, the mock engine and the carrier](docs/targets.md)
|
|
93
|
+
- [Targets (and adding one), your own agent, the mock engine and the carrier](docs/targets.md)
|
|
90
94
|
- [CI](docs/ci.md): pass or fail your agent's run against a policy
|
|
91
95
|
|
|
92
96
|
## Development
|
|
@@ -9,6 +9,7 @@ import secrets
|
|
|
9
9
|
import shlex
|
|
10
10
|
import shutil
|
|
11
11
|
import subprocess
|
|
12
|
+
import tempfile
|
|
12
13
|
import textwrap
|
|
13
14
|
import time
|
|
14
15
|
from collections.abc import Callable, Iterator, Sequence
|
|
@@ -196,6 +197,57 @@ def _image_tag() -> str:
|
|
|
196
197
|
return "rtpbench:" + h.hexdigest()[:12]
|
|
197
198
|
|
|
198
199
|
|
|
200
|
+
def _git(path: Path, *args: str) -> str:
|
|
201
|
+
res = subprocess.run(["git", "-C", str(path), *args], capture_output=True, text=True)
|
|
202
|
+
if res.returncode:
|
|
203
|
+
raise ValueError(f"not a git checkout: {res.stderr.strip()}")
|
|
204
|
+
return res.stdout
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _aside(path: Path, out: Path | None) -> Callable[[str], bool]:
|
|
208
|
+
"""Whether a path in the checkout `path`, as git gives it, is in the results directory
|
|
209
|
+
`out`: a run writes there, so it is no part of the target."""
|
|
210
|
+
try:
|
|
211
|
+
rel = out.resolve().relative_to(path.resolve()).as_posix() + "/" if out else None
|
|
212
|
+
except ValueError: # not inside the checkout
|
|
213
|
+
rel = None
|
|
214
|
+
return lambda name: rel is not None and (name + "/").startswith(rel)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def checkout(path: Path, out: Path | None = None) -> dict[str, Any]:
|
|
218
|
+
"""The commit a git checkout is at, and whether it has changes on top (the results
|
|
219
|
+
directory `out` aside): what a target built from it (`--source`) records."""
|
|
220
|
+
commit = _git(path, "rev-parse", "HEAD").strip()
|
|
221
|
+
aside = _aside(path, out)
|
|
222
|
+
fields = iter(_git(path, "status", "--porcelain", "-z", "--untracked-files=all").split("\0"))
|
|
223
|
+
changed = []
|
|
224
|
+
for e in fields:
|
|
225
|
+
if e[3:]:
|
|
226
|
+
changed.append(e[3:])
|
|
227
|
+
if set(e[:2]) & {"R", "C"} and (origin := next(fields, "")): # a field of its own
|
|
228
|
+
changed.append(origin)
|
|
229
|
+
return {"commit": commit, "dirty": any(not aside(name) for name in changed)}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def snapshot(path: Path, out: Path | None, dest: Path) -> None:
|
|
233
|
+
"""Copy into `dest` what a target builds from the checkout `path`: the files git tracks and
|
|
234
|
+
those it would (untracked, not ignored), as they are now; not .git, not the results
|
|
235
|
+
directory `out`, not __pycache__ (which _build_hash leaves out too)."""
|
|
236
|
+
aside = _aside(path, out)
|
|
237
|
+
names = _git(path, "ls-files", "-z", "--cached", "--others", "--exclude-standard")
|
|
238
|
+
for name in dict.fromkeys(names.split("\0")):
|
|
239
|
+
src = path / name
|
|
240
|
+
if not name or aside(name) or "__pycache__" in name.split("/"):
|
|
241
|
+
continue
|
|
242
|
+
if not (src.exists() or src.is_symlink()): # deleted
|
|
243
|
+
continue
|
|
244
|
+
(dest / name).parent.mkdir(parents=True, exist_ok=True)
|
|
245
|
+
if src.is_dir() and not src.is_symlink(): # a submodule
|
|
246
|
+
shutil.copytree(src, dest / name, symlinks=True, ignore=shutil.ignore_patterns(".git"))
|
|
247
|
+
else:
|
|
248
|
+
shutil.copy2(src, dest / name, follow_symlinks=False)
|
|
249
|
+
|
|
250
|
+
|
|
199
251
|
def _build_hash(build: dict[str, Any]) -> str:
|
|
200
252
|
"""A hash of a compose `build` section as `config` resolves it, with each local context's
|
|
201
253
|
files (all of them: the build may copy any) in place of its path, so the same from any
|
|
@@ -223,7 +275,9 @@ class Stack:
|
|
|
223
275
|
`network`, and with `ports` (first-last) the host publishes that range on the router. With
|
|
224
276
|
`tls`, out/tls holds a test certificate for the stack's TLS servers, and ca.pem, what the
|
|
225
277
|
caller trusts: `ca`, or with no `uri` the stack's test CA; else the caller trusts the
|
|
226
|
-
system's certificates. With `srtp`, the caller offers SRTP with these suites.
|
|
278
|
+
system's certificates. With `srtp`, the caller offers SRTP with these suites. With
|
|
279
|
+
`source`, a git checkout, the target builds from a copy of its files that `down` removes
|
|
280
|
+
(targets/<name>/source.yaml)."""
|
|
227
281
|
|
|
228
282
|
def __init__(
|
|
229
283
|
self,
|
|
@@ -237,6 +291,7 @@ class Stack:
|
|
|
237
291
|
tls: bool = False,
|
|
238
292
|
ca: Path | None = None,
|
|
239
293
|
srtp: list[str] | None = None,
|
|
294
|
+
source: Path | None = None,
|
|
240
295
|
) -> None:
|
|
241
296
|
self.project = f"rtpbench-{secrets.token_hex(3)}"
|
|
242
297
|
self.env = os.environ | {
|
|
@@ -259,6 +314,15 @@ class Stack:
|
|
|
259
314
|
if target and mock:
|
|
260
315
|
self.files.append(ROOT / "mock" / "compose.yaml")
|
|
261
316
|
self.env |= MOCK[target]
|
|
317
|
+
self.source: Path | None = None # the copy built from
|
|
318
|
+
self.checkout: dict[str, Any] = {} # what it was copied from, as it was then
|
|
319
|
+
if target and source:
|
|
320
|
+
self.files.append(ROOT / "targets" / target / "source.yaml")
|
|
321
|
+
self.checkout = checkout(source, out)
|
|
322
|
+
self.source = Path(tempfile.mkdtemp(prefix="rtpbench-source-"))
|
|
323
|
+
snapshot(source, out, self.source)
|
|
324
|
+
self.env["RTPBENCH_SOURCE"] = str(self.source)
|
|
325
|
+
self.env["RTPBENCH_SOURCE_COMMIT"] = self.checkout["commit"][:7]
|
|
262
326
|
if carrier:
|
|
263
327
|
self.files.append(ROOT / "carrier" / "compose.yaml")
|
|
264
328
|
self.env["RTPBENCH_CARRIER_NEXT"] = carrier
|
|
@@ -385,6 +449,8 @@ class Stack:
|
|
|
385
449
|
|
|
386
450
|
def down(self) -> None:
|
|
387
451
|
self.compose("down", "-t", "0", "-v", "--remove-orphans", check=False)
|
|
452
|
+
if self.source:
|
|
453
|
+
shutil.rmtree(self.source, ignore_errors=True)
|
|
388
454
|
# the test certificate's key is no use to judging; the CAs stay with the results
|
|
389
455
|
(Path(self.env["RTPBENCH_OUT"]) / "tls" / "server.key").unlink(missing_ok=True)
|
|
390
456
|
|
|
@@ -809,6 +875,7 @@ def target(
|
|
|
809
875
|
ca: Path | None = None,
|
|
810
876
|
srtp: list[str] | None = None,
|
|
811
877
|
tls: bool = False,
|
|
878
|
+
source: Path | None = None,
|
|
812
879
|
*,
|
|
813
880
|
progress: Progress,
|
|
814
881
|
) -> str:
|
|
@@ -823,7 +890,8 @@ def target(
|
|
|
823
890
|
next few to it), else with `network` its router's address there; over TLS (a sips: URI or
|
|
824
891
|
proxy, or transport=tls) it trusts the certificates in `ca`, else the system's; with `srtp`
|
|
825
892
|
the caller offers SRTP with these suites. A compose target takes `tls` and `srtp` with
|
|
826
|
-
`carrier` only, which takes them on the caller's leg
|
|
893
|
+
`carrier` only, which takes them on the caller's leg, and builds from the git checkout
|
|
894
|
+
`source` if given."""
|
|
827
895
|
from rtpbench import cases as cs
|
|
828
896
|
from rtpbench import realtime
|
|
829
897
|
from rtpbench.caller import ADVERTISE_PORTS, parse_uri
|
|
@@ -875,7 +943,8 @@ def target(
|
|
|
875
943
|
stack.env["RTPBENCH_CARRIER_TRANSPORT"] = transport(name).lower()
|
|
876
944
|
else:
|
|
877
945
|
proxy_tls = False
|
|
878
|
-
|
|
946
|
+
next_hop = "target" if carrier else None
|
|
947
|
+
stack = Stack(out, name, next_hop, mock, tls=tls, srtp=srtp, source=source)
|
|
879
948
|
logs = None
|
|
880
949
|
try:
|
|
881
950
|
stack.up(progress.phase, profiles)
|
|
@@ -912,6 +981,8 @@ def target(
|
|
|
912
981
|
router_ip = stack.sh("target", "getent hosts router").split()[0]
|
|
913
982
|
stack.sh("target", f"ip route replace {caller_ip} via {router_ip}")
|
|
914
983
|
conds["engine"] = realtime.conditions() if mock else _engine(settings)
|
|
984
|
+
if source:
|
|
985
|
+
conds["source"] = stack.checkout
|
|
915
986
|
if mock and (own := {k: v for k, v in settings.items() if k not in MOCK[name]}):
|
|
916
987
|
conds["settings"] = _engine(own) # the target's own, such as AVA's pacer
|
|
917
988
|
uri, proxy = f"sip:{user}@{target_ip}", stack.entry("operator")
|
|
@@ -8,8 +8,8 @@ import textwrap
|
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
-
from rtpbench import analyze, cases, pcap
|
|
12
|
-
from rtpbench.bench import PROFILES, TARGETS, docker, is_uri, network_problem
|
|
11
|
+
from rtpbench import ROOT, analyze, cases, pcap
|
|
12
|
+
from rtpbench.bench import PROFILES, TARGETS, checkout, docker, is_uri, network_problem
|
|
13
13
|
from rtpbench.caller import GAP_SECONDS, LEAD_SECONDS
|
|
14
14
|
from rtpbench.srtp import SUITES
|
|
15
15
|
|
|
@@ -62,9 +62,16 @@ def _target_args(args: argparse.Namespace) -> str | None:
|
|
|
62
62
|
return "--tls, --srtp: with a URI, or through the carrier (--carrier)"
|
|
63
63
|
if args.lines and args.mock:
|
|
64
64
|
return "--lines: not with --mock, which recognises only the bundled lines"
|
|
65
|
+
if args.source:
|
|
66
|
+
if not (ROOT / "targets" / args.name / "source.yaml").exists():
|
|
67
|
+
return f"--source: {args.name} does not build from a checkout"
|
|
68
|
+
try:
|
|
69
|
+
checkout(args.source)
|
|
70
|
+
except ValueError as e:
|
|
71
|
+
return f"--source {args.source}: {e}"
|
|
65
72
|
return None
|
|
66
|
-
if args.mock or args.tls:
|
|
67
|
-
return "--mock, --tls: with a bundled target only (a URI gives TLS as sips:)"
|
|
73
|
+
if args.mock or args.tls or args.source:
|
|
74
|
+
return "--mock, --tls, --source: with a bundled target only (a URI gives TLS as sips:)"
|
|
68
75
|
if args.carrier:
|
|
69
76
|
from rtpbench.sip import transport
|
|
70
77
|
|
|
@@ -236,6 +243,13 @@ examples:
|
|
|
236
243
|
p.add_argument("--seed", type=int, default=1, help=seed)
|
|
237
244
|
p.add_argument("--carrier", action="store_true", help="call through the carrier (Docker)")
|
|
238
245
|
p.add_argument("--mock", action="store_true", help="the mock engine instead of the AI engine")
|
|
246
|
+
p.add_argument(
|
|
247
|
+
"--source",
|
|
248
|
+
type=Path,
|
|
249
|
+
metavar="DIR",
|
|
250
|
+
help="a bundled target built from this git checkout instead of its pinned version: of"
|
|
251
|
+
" AI Voice Connector CE (aivc), AVA (ava) or livekit/sip (livekit)",
|
|
252
|
+
)
|
|
239
253
|
p.add_argument(
|
|
240
254
|
"--lines",
|
|
241
255
|
type=Path,
|
|
@@ -507,6 +521,7 @@ example:
|
|
|
507
521
|
args.ca,
|
|
508
522
|
_suites(args.srtp),
|
|
509
523
|
args.tls,
|
|
524
|
+
args.source,
|
|
510
525
|
progress=progress,
|
|
511
526
|
)
|
|
512
527
|
print(summary)
|
|
@@ -935,7 +935,8 @@ def uplink(out: Path) -> str:
|
|
|
935
935
|
continue
|
|
936
936
|
st = analyze.stream(up, w.log["pt"])
|
|
937
937
|
for turn in w.log["turns"]:
|
|
938
|
-
|
|
938
|
+
# a barge-in turn whose prompt got no reply has no interjection
|
|
939
|
+
for spans in (turn.get("prompt_speech"), turn.get("speech")):
|
|
939
940
|
if not spans:
|
|
940
941
|
continue
|
|
941
942
|
a, b = st.index(w.log["seq0"], spans[0][0]), st.index(w.log["seq0"], spans[-1][1])
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# `rtpbench target aivc --source DIR`: the connector built from the checkout DIR with upstream's
|
|
2
|
+
# own Dockerfile, as the project builds it, in place of the pinned commit. Merged over
|
|
3
|
+
# compose.yaml; the tag follows the checkout's commit and, as every build's, a hash of the build.
|
|
4
|
+
services:
|
|
5
|
+
engine:
|
|
6
|
+
build: !override
|
|
7
|
+
context: ${RTPBENCH_SOURCE:?}
|
|
8
|
+
dockerfile: docker/Dockerfile
|
|
9
|
+
image: ${RTPBENCH_REGISTRY:?}/rtpbench-aivc:${RTPBENCH_SOURCE_COMMIT:?}${RTPBENCH_BUILD_ENGINE:-}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# `rtpbench target ava --source DIR`: AVA built from the checkout DIR with upstream's own
|
|
2
|
+
# Dockerfile, as the project builds it, in place of the pinned commit. Merged over compose.yaml;
|
|
3
|
+
# the tag follows the checkout's commit and, as every build's, a hash of the build.
|
|
4
|
+
services:
|
|
5
|
+
engine:
|
|
6
|
+
build: !override
|
|
7
|
+
context: ${RTPBENCH_SOURCE:?}
|
|
8
|
+
image: ${RTPBENCH_REGISTRY:?}/rtpbench-ava:${RTPBENCH_SOURCE_COMMIT:?}${RTPBENCH_BUILD_ENGINE:-}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# `rtpbench target livekit --source DIR`: the SIP bridge built from a checkout DIR of
|
|
2
|
+
# livekit/sip with upstream's own Dockerfile, in place of its pinned image; the server and the
|
|
3
|
+
# Agents worker stay pinned. Merged over compose.yaml; the tag follows the checkout's commit and,
|
|
4
|
+
# as every build's, a hash of the build.
|
|
5
|
+
services:
|
|
6
|
+
sip:
|
|
7
|
+
build:
|
|
8
|
+
context: ${RTPBENCH_SOURCE:?}
|
|
9
|
+
dockerfile: build/sip/Dockerfile
|
|
10
|
+
image: ${RTPBENCH_REGISTRY:?}/rtpbench-livekit-sip:${RTPBENCH_SOURCE_COMMIT:?}${RTPBENCH_BUILD_SIP:-}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from rtpbench import ROOT, bench
|
|
10
|
+
from rtpbench.bench import TARGETS, Stack
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
15
|
+
"""A copy of the checkout's stack files and package, which bench reads instead."""
|
|
16
|
+
root = tmp_path / "root"
|
|
17
|
+
root.mkdir()
|
|
18
|
+
for name in ("compose.yaml", "Dockerfile"):
|
|
19
|
+
shutil.copyfile(ROOT / name, root / name)
|
|
20
|
+
for name in ("corpus", "uri", "mock", "carrier", "targets", "src"):
|
|
21
|
+
shutil.copytree(ROOT / name, root / name)
|
|
22
|
+
monkeypatch.setattr(bench, "ROOT", root)
|
|
23
|
+
monkeypatch.setattr(bench, "PACKAGE", root / "src" / "rtpbench")
|
|
24
|
+
for k in ("OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_API_KEY"):
|
|
25
|
+
monkeypatch.setenv(k, "-")
|
|
26
|
+
return root
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def builds(root: Path, target: str | None) -> dict[str, str]:
|
|
30
|
+
return Stack(root / "out", target, carrier="agent").builds()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.parametrize("target", [None, *TARGETS])
|
|
34
|
+
def test_compose_tags_each_built_image_with_the_hash_of_its_build(
|
|
35
|
+
root: Path, target: str | None
|
|
36
|
+
) -> None:
|
|
37
|
+
stack = Stack(root / "out", target, carrier="agent")
|
|
38
|
+
built = stack.builds()
|
|
39
|
+
services = json.loads(stack.compose("config", "--format", "json"))["services"]
|
|
40
|
+
assert built == {name: s["image"] for name, s in services.items() if "build" in s}
|
|
41
|
+
assert built["router"] == f"{bench.REGISTRY}/{bench._image_tag()}"
|
|
42
|
+
for name, image in built.items():
|
|
43
|
+
if name != "router": # its own content hash
|
|
44
|
+
assert re.fullmatch(r"[^:]+:[^:]+-[0-9a-f]{12}", image), image
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_editing_a_build_changes_its_image_s_tag_and_an_unrelated_file_none(root: Path) -> None:
|
|
48
|
+
before = builds(root, "ava")
|
|
49
|
+
agent = root / "targets" / "livekit" / "agent.py" # not in this stack
|
|
50
|
+
agent.write_text(agent.read_text() + "# edited\n")
|
|
51
|
+
assert builds(root, "ava") == before
|
|
52
|
+
|
|
53
|
+
compose = root / "targets" / "ava" / "compose.yaml"
|
|
54
|
+
old = "ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1" # the engine's dockerfile_inline
|
|
55
|
+
assert old in compose.read_text()
|
|
56
|
+
compose.write_text(compose.read_text().replace(old, "ENV PYTHONUNBUFFERED=1"))
|
|
57
|
+
after = builds(root, "ava")
|
|
58
|
+
assert after["engine"] != before["engine"]
|
|
59
|
+
assert after["engine"].startswith(before["engine"].rpartition("-")[0] + "-") # pin kept
|
|
60
|
+
for name in ("router", "carrier-media", "carrier-sip"):
|
|
61
|
+
assert after[name] == before[name]
|
|
62
|
+
|
|
63
|
+
constraints = root / "targets" / "ava" / "constraints.txt" # the engine's pins context
|
|
64
|
+
constraints.write_text(constraints.read_text() + "\n")
|
|
65
|
+
assert builds(root, "ava")["engine"] != after["engine"]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.parametrize(
|
|
69
|
+
("target", "service"), [("aivc", "engine"), ("ava", "engine"), ("livekit", "sip")]
|
|
70
|
+
)
|
|
71
|
+
def test_a_target_built_from_a_checkout_is_tagged_by_its_commit_and_files(
|
|
72
|
+
root: Path, tmp_path: Path, target: str, service: str
|
|
73
|
+
) -> None:
|
|
74
|
+
src = tmp_path / "checkout"
|
|
75
|
+
src.mkdir()
|
|
76
|
+
(src / "main.py").write_text("print('hi')\n")
|
|
77
|
+
(src / ".gitignore").write_text(".venv/\n")
|
|
78
|
+
git = ["git", "-C", str(src), "-c", "user.name=t", "-c", "user.email=t@t"]
|
|
79
|
+
for cmd in (["init", "-q"], ["add", "."], ["commit", "-qm", "x"]):
|
|
80
|
+
subprocess.run([*git, *cmd], check=True)
|
|
81
|
+
out = src / "results" / "run" # a run's results inside the checkout: no part of it
|
|
82
|
+
out.mkdir(parents=True)
|
|
83
|
+
|
|
84
|
+
def files(stack: Stack) -> list[Path]:
|
|
85
|
+
return [f for f in (stack.source or Path()).rglob("*") if f.is_file()]
|
|
86
|
+
|
|
87
|
+
def tag() -> str:
|
|
88
|
+
stack = Stack(out, target, carrier="agent", source=src)
|
|
89
|
+
try:
|
|
90
|
+
copied = {f.relative_to(stack.source or "").as_posix() for f in files(stack)}
|
|
91
|
+
assert copied == {".gitignore", "main.py"} # not .git, .venv or the results
|
|
92
|
+
return stack.builds()[service]
|
|
93
|
+
finally:
|
|
94
|
+
shutil.rmtree(stack.source or "")
|
|
95
|
+
|
|
96
|
+
pinned = builds(root, target).get(service, "")
|
|
97
|
+
clean = tag()
|
|
98
|
+
commit = bench.checkout(src, out)
|
|
99
|
+
assert re.fullmatch(rf".+:{commit['commit'][:7]}-[0-9a-f]{{12}}", clean), clean
|
|
100
|
+
assert clean != pinned
|
|
101
|
+
(out / "caller.pcap").write_bytes(b"x")
|
|
102
|
+
(src / ".venv").mkdir()
|
|
103
|
+
(src / ".venv" / "lib.py").write_text("ignored\n")
|
|
104
|
+
assert tag() == clean
|
|
105
|
+
assert bench.checkout(src, out)["dirty"] is False
|
|
106
|
+
assert bench.checkout(src)["dirty"] is True # the results are untracked files to git
|
|
107
|
+
|
|
108
|
+
(src / "__pycache__").mkdir()
|
|
109
|
+
(src / "__pycache__" / "main.pyc").write_bytes(b"x")
|
|
110
|
+
subprocess.run([*git, "add", "-f", "__pycache__"], check=True) # tracked: still left out
|
|
111
|
+
assert tag() == clean
|
|
112
|
+
subprocess.run([*git, "rm", "-rq", "--cached", "__pycache__"], check=True)
|
|
113
|
+
shutil.rmtree(src / "__pycache__")
|
|
114
|
+
(out / "a.txt").write_text("a\n") # a rename within the results: two fields, both aside
|
|
115
|
+
subprocess.run([*git, "add", "-f", str(out / "a.txt")], check=True)
|
|
116
|
+
subprocess.run([*git, "commit", "-qm", "results"], check=True)
|
|
117
|
+
subprocess.run([*git, "mv", str(out / "a.txt"), str(out / "b.txt")], check=True)
|
|
118
|
+
assert bench.checkout(src, out)["dirty"] is False
|
|
119
|
+
|
|
120
|
+
(src / "main.py").write_text("print('changed')\n") # uncommitted: same commit, new files
|
|
121
|
+
assert bench.checkout(src, out)["dirty"] is True
|
|
122
|
+
assert tag() != clean
|
|
123
|
+
with pytest.raises(ValueError, match="not a git checkout"):
|
|
124
|
+
bench.checkout(tmp_path)
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import re
|
|
3
|
-
import shutil
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
import pytest
|
|
7
|
-
|
|
8
|
-
from rtpbench import ROOT, bench
|
|
9
|
-
from rtpbench.bench import TARGETS, Stack
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.fixture
|
|
13
|
-
def root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
14
|
-
"""A copy of the checkout's stack files and package, which bench reads instead."""
|
|
15
|
-
root = tmp_path / "root"
|
|
16
|
-
root.mkdir()
|
|
17
|
-
for name in ("compose.yaml", "Dockerfile"):
|
|
18
|
-
shutil.copyfile(ROOT / name, root / name)
|
|
19
|
-
for name in ("corpus", "uri", "mock", "carrier", "targets", "src"):
|
|
20
|
-
shutil.copytree(ROOT / name, root / name)
|
|
21
|
-
monkeypatch.setattr(bench, "ROOT", root)
|
|
22
|
-
monkeypatch.setattr(bench, "PACKAGE", root / "src" / "rtpbench")
|
|
23
|
-
for k in ("OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_API_KEY"):
|
|
24
|
-
monkeypatch.setenv(k, "-")
|
|
25
|
-
return root
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def builds(root: Path, target: str | None) -> dict[str, str]:
|
|
29
|
-
return Stack(root / "out", target, carrier="agent").builds()
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@pytest.mark.parametrize("target", [None, *TARGETS])
|
|
33
|
-
def test_compose_tags_each_built_image_with_the_hash_of_its_build(
|
|
34
|
-
root: Path, target: str | None
|
|
35
|
-
) -> None:
|
|
36
|
-
stack = Stack(root / "out", target, carrier="agent")
|
|
37
|
-
built = stack.builds()
|
|
38
|
-
services = json.loads(stack.compose("config", "--format", "json"))["services"]
|
|
39
|
-
assert built == {name: s["image"] for name, s in services.items() if "build" in s}
|
|
40
|
-
assert built["router"] == f"{bench.REGISTRY}/{bench._image_tag()}"
|
|
41
|
-
for name, image in built.items():
|
|
42
|
-
if name != "router": # its own content hash
|
|
43
|
-
assert re.fullmatch(r"[^:]+:[^:]+-[0-9a-f]{12}", image), image
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_editing_a_build_changes_its_image_s_tag_and_an_unrelated_file_none(root: Path) -> None:
|
|
47
|
-
before = builds(root, "ava")
|
|
48
|
-
agent = root / "targets" / "livekit" / "agent.py" # not in this stack
|
|
49
|
-
agent.write_text(agent.read_text() + "# edited\n")
|
|
50
|
-
assert builds(root, "ava") == before
|
|
51
|
-
|
|
52
|
-
compose = root / "targets" / "ava" / "compose.yaml"
|
|
53
|
-
old = "ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1" # the engine's dockerfile_inline
|
|
54
|
-
assert old in compose.read_text()
|
|
55
|
-
compose.write_text(compose.read_text().replace(old, "ENV PYTHONUNBUFFERED=1"))
|
|
56
|
-
after = builds(root, "ava")
|
|
57
|
-
assert after["engine"] != before["engine"]
|
|
58
|
-
assert after["engine"].startswith(before["engine"].rpartition("-")[0] + "-") # pin kept
|
|
59
|
-
for name in ("router", "carrier-media", "carrier-sip"):
|
|
60
|
-
assert after[name] == before[name]
|
|
61
|
-
|
|
62
|
-
constraints = root / "targets" / "ava" / "constraints.txt" # the engine's pins context
|
|
63
|
-
constraints.write_text(constraints.read_text() + "\n")
|
|
64
|
-
assert builds(root, "ava")["engine"] != after["engine"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|