VoIP 0.1.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.
voip-0.1.0/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2026, Johannes Maron
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
voip-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: VoIP
3
+ Version: 0.1.0
4
+ Summary: Python asyncio library for VoIP calls.
5
+ Keywords: VoIP,SIP,RTP,WebRTC,STUN,asyncio
6
+ Author-email: Johannes Maron <johannes@maron.family>
7
+ Requires-Python: >=3.13
8
+ Description-Content-Type: text/markdown
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Information Technology
13
+ Classifier: Intended Audience :: Telecommunications Industry
14
+ Classifier: License :: OSI Approved :: BSD License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Framework :: AsyncIO
22
+ Classifier: Topic :: Communications :: Internet Phone
23
+ Classifier: Topic :: Communications :: Telephony
24
+ Classifier: Topic :: Multimedia :: Sound/Audio
25
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
26
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
27
+ Classifier: Topic :: Software Development :: Libraries
28
+ Classifier: Topic :: System :: Networking
29
+ Classifier: Topic :: Home Automation
30
+ License-File: LICENSE
31
+ Requires-Dist: cryptography
32
+ Requires-Dist: faster-whisper ; extra == "audio"
33
+ Requires-Dist: numpy ; extra == "audio"
34
+ Requires-Dist: av ; extra == "audio"
35
+ Requires-Dist: click ; extra == "cli"
36
+ Requires-Dist: pygments ; extra == "cli"
37
+ Requires-Dist: faster-whisper ; extra == "cli"
38
+ Requires-Dist: numpy ; extra == "cli"
39
+ Requires-Dist: av ; extra == "cli"
40
+ Requires-Dist: Pygments ; extra == "pygments"
41
+ Project-URL: Changelog, https://github.com/codingjoe/VoIP/releases
42
+ Project-URL: Project-URL, https://github.com/codingjoe/VoIP
43
+ Provides-Extra: audio
44
+ Provides-Extra: cli
45
+ Provides-Extra: pygments
46
+
47
+ <p align="center">
48
+ <picture>
49
+ <source media="(prefers-color-scheme: dark)" srcset="https://github.com/codingjoe/VoIP/raw/main/docs/images/logo-dark.svg">
50
+ <source media="(prefers-color-scheme: light)" srcset="https://github.com/codingjoe/VoIP/raw/main/docs/images/logo-light.svg">
51
+ <img alt="Python VoIP" src="https://github.com/codingjoe/VoIP/raw/main/docs/images/logo-light.svg">
52
+ </picture>
53
+ <br>
54
+ <a href="https://github.com/codingjoe">Documentation</a> |
55
+ <a href="https://github.com/codingjoe/VoIP/issues/new/choose">Issues</a> |
56
+ <a href="https://github.com/codingjoe/VoIP/releases">Changelog</a> |
57
+ <a href="https://github.com/sponsors/codingjoe">Funding</a> ๐Ÿ’š
58
+ </p>
59
+
60
+ # Python VoIP library
61
+
62
+ > [!WARNING]
63
+ > This library is in early development and may contain breaking changes. Use with caution.
64
+
65
+ Python asyncio library for SIP telephony ([RFC 3261](https://tools.ietf.org/html/rfc3261)).
66
+
67
+ All signalling uses **SIP over TLS** (SIPS, RFC 3261 ยง26) and all media is
68
+ protected with **SRTP** ([RFC 3711](https://tools.ietf.org/html/rfc3711))
69
+ using the `AES_CM_128_HMAC_SHA1_80` cipher suite with SDES key exchange
70
+ ([RFC 4568](https://tools.ietf.org/html/rfc4568)).
71
+
72
+ ## Setup
73
+
74
+ ```console
75
+ pip install voip[audio,cli,pygments]
76
+ ```
77
+
78
+ ## Usage
79
+
80
+ ### CLI
81
+
82
+ Answer calls and transcribe them live from the terminal:
83
+
84
+ ```console
85
+ voip sip transcribe sips:alice@sip.example.com --password secret
86
+ ```
87
+
88
+ ### Python API
89
+
90
+ Subclass `WhisperCall` and override `transcription_received` to handle results.
91
+ Pass it as `call_class` when answering an incoming call:
92
+
93
+ ```python
94
+ import asyncio
95
+ import ssl
96
+ from voip.audio import WhisperCall
97
+ from voip.sip.protocol import SIP
98
+
99
+
100
+ class MyCall(WhisperCall):
101
+ def transcription_received(self, text: str) -> None:
102
+ print(f"[{self.caller}] {text}")
103
+
104
+
105
+ class MySession(SIP):
106
+ def call_received(self, request) -> None:
107
+ asyncio.create_task(self.answer(request=request, call_class=MyCall))
108
+
109
+
110
+ async def main():
111
+ loop = asyncio.get_running_loop()
112
+ ssl_context = ssl.create_default_context()
113
+ await loop.create_connection(
114
+ lambda: MySession(
115
+ aor="sips:alice@example.com",
116
+ username="alice",
117
+ password="secret",
118
+ ),
119
+ host="sip.example.com",
120
+ port=5061,
121
+ ssl=ssl_context,
122
+ )
123
+ await asyncio.Future()
124
+
125
+
126
+ asyncio.run(main())
127
+ ```
128
+
129
+ For raw audio access without transcription, subclass `AudioCall` and override
130
+ `audio_received(self, audio: np.ndarray)` instead.
131
+
voip-0.1.0/README.md ADDED
@@ -0,0 +1,84 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://github.com/codingjoe/VoIP/raw/main/docs/images/logo-dark.svg">
4
+ <source media="(prefers-color-scheme: light)" srcset="https://github.com/codingjoe/VoIP/raw/main/docs/images/logo-light.svg">
5
+ <img alt="Python VoIP" src="https://github.com/codingjoe/VoIP/raw/main/docs/images/logo-light.svg">
6
+ </picture>
7
+ <br>
8
+ <a href="https://github.com/codingjoe">Documentation</a> |
9
+ <a href="https://github.com/codingjoe/VoIP/issues/new/choose">Issues</a> |
10
+ <a href="https://github.com/codingjoe/VoIP/releases">Changelog</a> |
11
+ <a href="https://github.com/sponsors/codingjoe">Funding</a> ๐Ÿ’š
12
+ </p>
13
+
14
+ # Python VoIP library
15
+
16
+ > [!WARNING]
17
+ > This library is in early development and may contain breaking changes. Use with caution.
18
+
19
+ Python asyncio library for SIP telephony ([RFC 3261](https://tools.ietf.org/html/rfc3261)).
20
+
21
+ All signalling uses **SIP over TLS** (SIPS, RFC 3261 ยง26) and all media is
22
+ protected with **SRTP** ([RFC 3711](https://tools.ietf.org/html/rfc3711))
23
+ using the `AES_CM_128_HMAC_SHA1_80` cipher suite with SDES key exchange
24
+ ([RFC 4568](https://tools.ietf.org/html/rfc4568)).
25
+
26
+ ## Setup
27
+
28
+ ```console
29
+ pip install voip[audio,cli,pygments]
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ### CLI
35
+
36
+ Answer calls and transcribe them live from the terminal:
37
+
38
+ ```console
39
+ voip sip transcribe sips:alice@sip.example.com --password secret
40
+ ```
41
+
42
+ ### Python API
43
+
44
+ Subclass `WhisperCall` and override `transcription_received` to handle results.
45
+ Pass it as `call_class` when answering an incoming call:
46
+
47
+ ```python
48
+ import asyncio
49
+ import ssl
50
+ from voip.audio import WhisperCall
51
+ from voip.sip.protocol import SIP
52
+
53
+
54
+ class MyCall(WhisperCall):
55
+ def transcription_received(self, text: str) -> None:
56
+ print(f"[{self.caller}] {text}")
57
+
58
+
59
+ class MySession(SIP):
60
+ def call_received(self, request) -> None:
61
+ asyncio.create_task(self.answer(request=request, call_class=MyCall))
62
+
63
+
64
+ async def main():
65
+ loop = asyncio.get_running_loop()
66
+ ssl_context = ssl.create_default_context()
67
+ await loop.create_connection(
68
+ lambda: MySession(
69
+ aor="sips:alice@example.com",
70
+ username="alice",
71
+ password="secret",
72
+ ),
73
+ host="sip.example.com",
74
+ port=5061,
75
+ ssl=ssl_context,
76
+ )
77
+ await asyncio.Future()
78
+
79
+
80
+ asyncio.run(main())
81
+ ```
82
+
83
+ For raw audio access without transcription, subclass `AudioCall` and override
84
+ `audio_received(self, audio: np.ndarray)` instead.
@@ -0,0 +1,113 @@
1
+ [build-system]
2
+ requires = ["flit_core>=3.2", "flit_scm", "wheel"]
3
+ build-backend = "flit_scm:buildapi"
4
+
5
+ [project]
6
+ name = "VoIP"
7
+ authors = [
8
+ { name = "Johannes Maron", email = "johannes@maron.family" },
9
+ ]
10
+ readme = "README.md"
11
+ license = { file = "LICENSE" }
12
+ keywords = ["VoIP", "SIP", "RTP", "WebRTC", "STUN", "asyncio"]
13
+ dynamic = ["version", "description"]
14
+ classifiers = [
15
+ # https://pypi.org/classifiers/
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: Information Technology",
20
+ "Intended Audience :: Telecommunications Industry",
21
+ "License :: OSI Approved :: BSD License",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Framework :: AsyncIO",
29
+ "Topic :: Communications :: Internet Phone",
30
+ "Topic :: Communications :: Telephony",
31
+ "Topic :: Multimedia :: Sound/Audio",
32
+ "Topic :: Multimedia :: Sound/Audio :: Speech",
33
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
34
+ "Topic :: Software Development :: Libraries",
35
+ "Topic :: System :: Networking",
36
+ "Topic :: Home Automation",
37
+ ]
38
+ requires-python = ">=3.13"
39
+ dependencies = ["cryptography"]
40
+
41
+ [project.optional-dependencies]
42
+ audio = ["faster-whisper", "numpy", "av"]
43
+ cli = ["click", "pygments", "faster-whisper", "numpy", "av"]
44
+ pygments = ["Pygments"]
45
+
46
+ [project.scripts]
47
+ voip = "voip.__main__:main"
48
+
49
+ [project.entry-points."pygments.lexers"]
50
+ sip = "voip.sip.lexers:SIPLexer"
51
+ sdp = "voip.sdp.lexers:SDPLexer"
52
+
53
+ [tool.flit.module]
54
+ name = "voip"
55
+
56
+ [dependency-groups]
57
+ dev = [
58
+ { include-group = "test" },
59
+ ]
60
+ test = [
61
+ "pytest",
62
+ "pytest-cov",
63
+ "pytest-asyncio>=1.3.0",
64
+ ]
65
+
66
+ [project.urls]
67
+ Project-URL = "https://github.com/codingjoe/VoIP"
68
+ Changelog = "https://github.com/codingjoe/VoIP/releases"
69
+
70
+ [tool.setuptools_scm]
71
+ write_to = "voip/_version.py"
72
+
73
+ [tool.pytest.ini_options]
74
+ minversion = "6.0"
75
+ addopts = "--cov --strict-markers --cov-report=xml --cov-report=term"
76
+ asyncio_mode = "auto"
77
+ testpaths = ["tests"]
78
+ markers = [
79
+ "integration: marks tests as integration tests requiring network access (deselect with '-m \"not integration\"')",
80
+ ]
81
+
82
+ [tool.coverage.run]
83
+ source = ["voip"]
84
+
85
+ [tool.coverage.report]
86
+ show_missing = true
87
+
88
+ [tool.ruff]
89
+ src = ["voip", "tests"]
90
+
91
+ [tool.ruff.lint]
92
+ select = [
93
+ "E", # pycodestyle errors
94
+ "W", # pycodestyle warnings
95
+ "F", # pyflakes
96
+ "I", # isort
97
+ "S", # flake8-bandit
98
+ "D", # pydocstyle
99
+ "UP", # pyupgrade
100
+ "B", # flake8-bugbear
101
+ "C", # flake8-comprehensions
102
+ ]
103
+
104
+ ignore = ["B904", "D1", "E501", "S101"]
105
+
106
+ [tool.ruff.lint.isort]
107
+ combine-as-imports = true
108
+ split-on-trailing-comma = true
109
+ section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
110
+ force-wrap-aliases = true
111
+
112
+ [tool.ruff.lint.pydocstyle]
113
+ convention = "pep257"
@@ -0,0 +1,6 @@
1
+ """Python asyncio library for VoIP calls."""
2
+
3
+ from . import _version
4
+
5
+ __version__ = _version.version
6
+ VERSION = _version.version_tuple
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env python3
2
+ import asyncio
3
+ import dataclasses
4
+ import logging
5
+ import ssl
6
+ import time
7
+
8
+ from voip.sip import messages
9
+
10
+ try:
11
+ import click
12
+ from pygments import formatters, highlight
13
+
14
+ from voip.sip.lexers import SIPLexer
15
+ except ImportError as e:
16
+ raise ImportError(
17
+ "The VoIP CLI requires extra dependencies. Install via `pip install voip[cli]`."
18
+ ) from e
19
+
20
+
21
+ class ConsoleMessageProcessor:
22
+ """Protocol mixin that prints messages to stdout."""
23
+
24
+ def request_received(self, request: messages.Request, addr: tuple[str, int]):
25
+ self.pprint(request)
26
+ super().request_received(request, addr)
27
+
28
+ def response_received(self, response: messages.Response, addr: tuple[str, int]):
29
+ self.pprint(response)
30
+ super().response_received(response, addr)
31
+
32
+ def send(self, message) -> None:
33
+ """Send a message and print it to stdout."""
34
+ self.pprint(message)
35
+ super().send(message)
36
+
37
+ def pprint(self, msg):
38
+ """Pretty print the message."""
39
+ transport = getattr(self, "transport", None)
40
+ addr = transport.get_extra_info("peername") if transport else None
41
+ if addr:
42
+ host = f"[{addr[0]}]" if ":" in addr[0] else addr[0]
43
+ host = click.style(host, fg="green", bold=True)
44
+ port = click.style(str(addr[1]), fg="yellow", bold=True)
45
+ prefix = f"{host}:{port} - - [{time.asctime()}]"
46
+ else:
47
+ prefix = f"[unknown] - - [{time.asctime()}]"
48
+ pretty_msg = highlight(str(msg), SIPLexer(), formatters.TerminalFormatter())
49
+ click.echo(f"{prefix} {pretty_msg}")
50
+
51
+
52
+ @click.group()
53
+ @click.option("-v", "--verbose", count=True, help="Increase verbosity.")
54
+ @click.pass_context
55
+ def voip(ctx, verbose):
56
+ """VoIP command line interface."""
57
+ ctx.ensure_object(dict)
58
+ ctx.obj["verbose"] = verbose
59
+ logging.basicConfig(
60
+ level=max(10, 10 * (4 - verbose)),
61
+ format="%(levelname)s: [%(asctime)s] (%(name)s) %(message)s",
62
+ handlers=[logging.StreamHandler()],
63
+ )
64
+ logging.getLogger("voip").setLevel(max(10, 10 * (3 - verbose)))
65
+
66
+
67
+ @voip.group()
68
+ def sip():
69
+ """Session Initiation Protocol (SIP)."""
70
+
71
+
72
+ main = voip
73
+
74
+ #: Standard SIP/TCP port โ€” plain text, no TLS (RFC 3261 ยง18.2).
75
+ SIP_TCP_PORT = 5060
76
+ #: Standard SIP/TLS port (RFC 3261 ยง26.2.2).
77
+ SIP_TLS_PORT = 5061
78
+
79
+
80
+ def _parse_aor(value: str) -> tuple[str, str, str, int | None]:
81
+ """Parse a SIP URI into ``(scheme, user, host, port)``.
82
+
83
+ The port is ``None`` when not present in the URI.
84
+
85
+ Examples::
86
+
87
+ >>> _parse_aor("sip:alice@example.com")
88
+ ('sip', 'alice', 'example.com', None)
89
+ >>> _parse_aor("sips:+15551234567@carrier.com:5061")
90
+ ('sips', '+15551234567', 'carrier.com', 5061)
91
+ """
92
+ scheme, _, rest = value.partition(":")
93
+ if not scheme or not rest:
94
+ raise click.BadParameter(
95
+ f"Invalid SIP URI: {value!r}. Expected sip[s]:user@host[:port]."
96
+ )
97
+ user_part, _, hostport = rest.partition("@")
98
+ if not hostport:
99
+ raise click.BadParameter(f"Invalid SIP URI: {value!r}. Missing user@host part.")
100
+ host, _, port_str = hostport.partition(":")
101
+ if not host:
102
+ raise click.BadParameter(f"Invalid SIP URI: {value!r}. Missing host.")
103
+ port: int | None = int(port_str) if port_str else None
104
+ return scheme, user_part, host, port
105
+
106
+
107
+ def _parse_hostport(
108
+ ctx, param, value: str, default_port: int = 5061
109
+ ) -> tuple[str, int]:
110
+ """Parse ``HOST[:PORT]`` into a ``(host, port)`` tuple."""
111
+ host, _, port_str = value.rpartition(":")
112
+ if not host:
113
+ return value, default_port
114
+ try:
115
+ return host, int(port_str)
116
+ except ValueError:
117
+ raise click.BadParameter(f"Invalid port in {value!r}.", param=param) from None
118
+
119
+
120
+ def _parse_stun_server(ctx, param, value: str | None) -> tuple[str, int] | None:
121
+ """Parse the --stun-server option; return None when the value is 'none'."""
122
+ if value is None or value.lower() == "none":
123
+ return None
124
+ return _parse_hostport(ctx, param, value, default_port=3478)
125
+
126
+
127
+ # Keep the old name as an alias so existing internal callers still work.
128
+ _parse_server = _parse_hostport
129
+
130
+
131
+ @sip.command()
132
+ @click.argument("aor", metavar="AOR", envvar="SIP_AOR")
133
+ @click.option(
134
+ "--model",
135
+ default="base",
136
+ envvar="WHISPER_MODEL",
137
+ show_default=True,
138
+ help="Whisper model size.",
139
+ )
140
+ @click.option(
141
+ "--password",
142
+ envvar="SIP_PASSWORD",
143
+ required=True,
144
+ help="SIP password (not parsed from AOR for security).",
145
+ )
146
+ @click.option(
147
+ "--username",
148
+ envvar="SIP_USERNAME",
149
+ default=None,
150
+ help="Override SIP username (defaults to user part of AOR).",
151
+ )
152
+ @click.option(
153
+ "--proxy",
154
+ envvar="SIP_PROXY",
155
+ default=None,
156
+ metavar="HOST[:PORT]",
157
+ help=(
158
+ "Outbound proxy address (RFC 3261 ยง8.1.2). "
159
+ "Defaults to the host and port from AOR. "
160
+ "Use this when the proxy differs from the registrar domain."
161
+ ),
162
+ )
163
+ @click.option(
164
+ "--stun-server",
165
+ envvar="STUN_SERVER",
166
+ default="stun.cloudflare.com:3478",
167
+ show_default=True,
168
+ metavar="HOST[:PORT]",
169
+ callback=_parse_stun_server,
170
+ is_eager=False,
171
+ help="STUN server for RTP NAT traversal (use 'none' to disable).",
172
+ )
173
+ @click.option(
174
+ "--no-tls",
175
+ is_flag=True,
176
+ default=False,
177
+ help=(
178
+ "Force plain TCP โ€” skips TLS. "
179
+ "Auto-selected when port 5060 is used; explicit flag overrides any port."
180
+ ),
181
+ )
182
+ @click.option(
183
+ "--no-verify-tls",
184
+ is_flag=True,
185
+ default=False,
186
+ help="Disable TLS certificate verification (insecure; for testing only).",
187
+ )
188
+ @click.pass_context
189
+ def transcribe(
190
+ ctx, aor, model, password, username, proxy, stun_server, no_tls, no_verify_tls
191
+ ):
192
+ r"""Register with a SIP carrier and transcribe incoming calls via Whisper.
193
+
194
+ AOR is a SIP Address of Record URI identifying the account to register,
195
+ e.g. ``sips:alice@carrier.example.com`` or ``sip:alice@carrier.example.com:5060``.
196
+
197
+ \b
198
+ Transport selection (overridable with --no-tls):
199
+ sips: URI or port 5061 โ†’ TLS (default)
200
+ sip: URI or port 5060 โ†’ plain TCP
201
+
202
+ \b
203
+ Examples:
204
+ voip sip transcribe sips:alice@sip.example.com --password secret
205
+ voip sip transcribe sip:alice@sip.example.com:5060 --password secret
206
+ voip sip transcribe sips:alice@carrier.com --proxy proxy.carrier.com --password secret
207
+ """
208
+ from voip.sip.protocol import SIP
209
+
210
+ from .audio import WhisperCall # noqa: PLC0415
211
+
212
+ try:
213
+ scheme, aor_user, aor_host, aor_port = _parse_aor(aor)
214
+ except click.BadParameter as exc:
215
+ raise click.BadParameter(str(exc), param_hint="AOR") from exc
216
+
217
+ effective_username = username or aor_user
218
+
219
+ # Determine outbound proxy address: --proxy overrides, otherwise use AOR host.
220
+ if proxy is not None:
221
+ proxy_addr = _parse_hostport(ctx, None, proxy)
222
+ else:
223
+ # Default port: SIP_TCP_PORT for sip scheme, SIP_TLS_PORT for sips.
224
+ default_port = SIP_TCP_PORT if scheme == "sip" else SIP_TLS_PORT
225
+ port = aor_port if aor_port is not None else default_port
226
+ proxy_addr = (aor_host, port)
227
+
228
+ # Transport: port 5060 (SIP_TCP_PORT) โ†’ plain TCP; any other port โ†’ TLS.
229
+ # --no-tls always overrides this auto-detection.
230
+ use_tls = not no_tls and proxy_addr[1] != SIP_TCP_PORT
231
+
232
+ # The AOR stored in the protocol must NOT include the port
233
+ # (AOR is scheme:user@host per RFC 3261 ยง10).
234
+ normalized_aor = f"{scheme}:{effective_username}@{aor_host}"
235
+
236
+ verbose = ctx.obj.get("verbose", 0)
237
+
238
+ # Capture the CLI model arg as the dataclass field default so that the
239
+ # class can still be passed as a plain type to SIP.answer().
240
+ _model = model
241
+
242
+ @dataclasses.dataclass
243
+ class TranscribingCall(WhisperCall):
244
+ """WhisperCall with the CLI-selected model and console output."""
245
+
246
+ model: str = _model
247
+
248
+ def transcription_received(self, text: str) -> None:
249
+ click.echo(click.style(text, fg="green", bold=True))
250
+
251
+ # Mix in ConsoleMessageProcessor only at maximum verbosity (-vvv) so that
252
+ # normal operation is not flooded with protocol-level message dumps.
253
+ bases = (ConsoleMessageProcessor, SIP) if verbose >= 3 else (SIP,)
254
+
255
+ class TranscribeSession(*bases):
256
+ def call_received(self, request) -> None:
257
+ self.ringing(request=request)
258
+ asyncio.create_task(
259
+ self.answer(request=request, call_class=TranscribingCall)
260
+ )
261
+
262
+ async def run():
263
+ loop = asyncio.get_running_loop()
264
+ if use_tls:
265
+ ssl_context = ssl.create_default_context()
266
+ if no_verify_tls:
267
+ ssl_context.check_hostname = False
268
+ ssl_context.verify_mode = ssl.CERT_NONE
269
+ else:
270
+ ssl_context = None
271
+ await loop.create_connection(
272
+ lambda: TranscribeSession(
273
+ outbound_proxy=proxy_addr,
274
+ aor=normalized_aor,
275
+ username=effective_username,
276
+ password=password,
277
+ rtp_stun_server_address=stun_server,
278
+ ),
279
+ host=proxy_addr[0],
280
+ port=proxy_addr[1],
281
+ ssl=ssl_context,
282
+ )
283
+ await asyncio.Future()
284
+
285
+ try:
286
+ asyncio.run(run())
287
+ except KeyboardInterrupt:
288
+ pass
289
+
290
+
291
+ if __name__ == "__main__": # pragma: no cover
292
+ main()
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.0'
32
+ __version_tuple__ = version_tuple = (0, 1, 0)
33
+
34
+ __commit_id__ = commit_id = 'g1a0b5c6d1'