bithuman-cli 2.3.0__py3-none-macosx_11_0_arm64.whl

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.
Files changed (109) hide show
  1. bithuman_cli/__init__.py +22 -0
  2. bithuman_cli/__main__.py +12 -0
  3. bithuman_cli/_avatar_bridge.py +250 -0
  4. bithuman_cli/_bin/bithuman +0 -0
  5. bithuman_cli/_bin/lib/libabsl_base.2601.0.0.dylib +0 -0
  6. bithuman_cli/_bin/lib/libabsl_borrowed_fixup_buffer.2601.0.0.dylib +0 -0
  7. bithuman_cli/_bin/lib/libabsl_city.2601.0.0.dylib +0 -0
  8. bithuman_cli/_bin/lib/libabsl_civil_time.2601.0.0.dylib +0 -0
  9. bithuman_cli/_bin/lib/libabsl_cord.2601.0.0.dylib +0 -0
  10. bithuman_cli/_bin/lib/libabsl_cord_internal.2601.0.0.dylib +0 -0
  11. bithuman_cli/_bin/lib/libabsl_cordz_functions.2601.0.0.dylib +0 -0
  12. bithuman_cli/_bin/lib/libabsl_cordz_handle.2601.0.0.dylib +0 -0
  13. bithuman_cli/_bin/lib/libabsl_cordz_info.2601.0.0.dylib +0 -0
  14. bithuman_cli/_bin/lib/libabsl_crc32c.2601.0.0.dylib +0 -0
  15. bithuman_cli/_bin/lib/libabsl_crc_cord_state.2601.0.0.dylib +0 -0
  16. bithuman_cli/_bin/lib/libabsl_crc_cpu_detect.2601.0.0.dylib +0 -0
  17. bithuman_cli/_bin/lib/libabsl_crc_internal.2601.0.0.dylib +0 -0
  18. bithuman_cli/_bin/lib/libabsl_debugging_internal.2601.0.0.dylib +0 -0
  19. bithuman_cli/_bin/lib/libabsl_decode_rust_punycode.2601.0.0.dylib +0 -0
  20. bithuman_cli/_bin/lib/libabsl_demangle_internal.2601.0.0.dylib +0 -0
  21. bithuman_cli/_bin/lib/libabsl_demangle_rust.2601.0.0.dylib +0 -0
  22. bithuman_cli/_bin/lib/libabsl_die_if_null.2601.0.0.dylib +0 -0
  23. bithuman_cli/_bin/lib/libabsl_examine_stack.2601.0.0.dylib +0 -0
  24. bithuman_cli/_bin/lib/libabsl_exponential_biased.2601.0.0.dylib +0 -0
  25. bithuman_cli/_bin/lib/libabsl_flags_commandlineflag.2601.0.0.dylib +0 -0
  26. bithuman_cli/_bin/lib/libabsl_flags_commandlineflag_internal.2601.0.0.dylib +0 -0
  27. bithuman_cli/_bin/lib/libabsl_flags_config.2601.0.0.dylib +0 -0
  28. bithuman_cli/_bin/lib/libabsl_flags_internal.2601.0.0.dylib +0 -0
  29. bithuman_cli/_bin/lib/libabsl_flags_marshalling.2601.0.0.dylib +0 -0
  30. bithuman_cli/_bin/lib/libabsl_flags_private_handle_accessor.2601.0.0.dylib +0 -0
  31. bithuman_cli/_bin/lib/libabsl_flags_program_name.2601.0.0.dylib +0 -0
  32. bithuman_cli/_bin/lib/libabsl_flags_reflection.2601.0.0.dylib +0 -0
  33. bithuman_cli/_bin/lib/libabsl_graphcycles_internal.2601.0.0.dylib +0 -0
  34. bithuman_cli/_bin/lib/libabsl_hash.2601.0.0.dylib +0 -0
  35. bithuman_cli/_bin/lib/libabsl_hashtablez_sampler.2601.0.0.dylib +0 -0
  36. bithuman_cli/_bin/lib/libabsl_int128.2601.0.0.dylib +0 -0
  37. bithuman_cli/_bin/lib/libabsl_kernel_timeout_internal.2601.0.0.dylib +0 -0
  38. bithuman_cli/_bin/lib/libabsl_leak_check.2601.0.0.dylib +0 -0
  39. bithuman_cli/_bin/lib/libabsl_log_entry.2601.0.0.dylib +0 -0
  40. bithuman_cli/_bin/lib/libabsl_log_globals.2601.0.0.dylib +0 -0
  41. bithuman_cli/_bin/lib/libabsl_log_initialize.2601.0.0.dylib +0 -0
  42. bithuman_cli/_bin/lib/libabsl_log_internal_check_op.2601.0.0.dylib +0 -0
  43. bithuman_cli/_bin/lib/libabsl_log_internal_conditions.2601.0.0.dylib +0 -0
  44. bithuman_cli/_bin/lib/libabsl_log_internal_fnmatch.2601.0.0.dylib +0 -0
  45. bithuman_cli/_bin/lib/libabsl_log_internal_format.2601.0.0.dylib +0 -0
  46. bithuman_cli/_bin/lib/libabsl_log_internal_globals.2601.0.0.dylib +0 -0
  47. bithuman_cli/_bin/lib/libabsl_log_internal_log_sink_set.2601.0.0.dylib +0 -0
  48. bithuman_cli/_bin/lib/libabsl_log_internal_message.2601.0.0.dylib +0 -0
  49. bithuman_cli/_bin/lib/libabsl_log_internal_nullguard.2601.0.0.dylib +0 -0
  50. bithuman_cli/_bin/lib/libabsl_log_internal_proto.2601.0.0.dylib +0 -0
  51. bithuman_cli/_bin/lib/libabsl_log_internal_structured_proto.2601.0.0.dylib +0 -0
  52. bithuman_cli/_bin/lib/libabsl_log_severity.2601.0.0.dylib +0 -0
  53. bithuman_cli/_bin/lib/libabsl_log_sink.2601.0.0.dylib +0 -0
  54. bithuman_cli/_bin/lib/libabsl_malloc_internal.2601.0.0.dylib +0 -0
  55. bithuman_cli/_bin/lib/libabsl_random_distributions.2601.0.0.dylib +0 -0
  56. bithuman_cli/_bin/lib/libabsl_random_internal_entropy_pool.2601.0.0.dylib +0 -0
  57. bithuman_cli/_bin/lib/libabsl_random_internal_platform.2601.0.0.dylib +0 -0
  58. bithuman_cli/_bin/lib/libabsl_random_internal_randen.2601.0.0.dylib +0 -0
  59. bithuman_cli/_bin/lib/libabsl_random_internal_randen_hwaes.2601.0.0.dylib +0 -0
  60. bithuman_cli/_bin/lib/libabsl_random_internal_randen_hwaes_impl.2601.0.0.dylib +0 -0
  61. bithuman_cli/_bin/lib/libabsl_random_internal_randen_slow.2601.0.0.dylib +0 -0
  62. bithuman_cli/_bin/lib/libabsl_random_internal_seed_material.2601.0.0.dylib +0 -0
  63. bithuman_cli/_bin/lib/libabsl_random_seed_gen_exception.2601.0.0.dylib +0 -0
  64. bithuman_cli/_bin/lib/libabsl_random_seed_sequences.2601.0.0.dylib +0 -0
  65. bithuman_cli/_bin/lib/libabsl_raw_hash_set.2601.0.0.dylib +0 -0
  66. bithuman_cli/_bin/lib/libabsl_raw_logging_internal.2601.0.0.dylib +0 -0
  67. bithuman_cli/_bin/lib/libabsl_spinlock_wait.2601.0.0.dylib +0 -0
  68. bithuman_cli/_bin/lib/libabsl_stacktrace.2601.0.0.dylib +0 -0
  69. bithuman_cli/_bin/lib/libabsl_status.2601.0.0.dylib +0 -0
  70. bithuman_cli/_bin/lib/libabsl_statusor.2601.0.0.dylib +0 -0
  71. bithuman_cli/_bin/lib/libabsl_str_format_internal.2601.0.0.dylib +0 -0
  72. bithuman_cli/_bin/lib/libabsl_strerror.2601.0.0.dylib +0 -0
  73. bithuman_cli/_bin/lib/libabsl_strings.2601.0.0.dylib +0 -0
  74. bithuman_cli/_bin/lib/libabsl_strings_internal.2601.0.0.dylib +0 -0
  75. bithuman_cli/_bin/lib/libabsl_symbolize.2601.0.0.dylib +0 -0
  76. bithuman_cli/_bin/lib/libabsl_synchronization.2601.0.0.dylib +0 -0
  77. bithuman_cli/_bin/lib/libabsl_throw_delegate.2601.0.0.dylib +0 -0
  78. bithuman_cli/_bin/lib/libabsl_time.2601.0.0.dylib +0 -0
  79. bithuman_cli/_bin/lib/libabsl_time_zone.2601.0.0.dylib +0 -0
  80. bithuman_cli/_bin/lib/libabsl_tracing_internal.2601.0.0.dylib +0 -0
  81. bithuman_cli/_bin/lib/libabsl_utf8_for_code_point.2601.0.0.dylib +0 -0
  82. bithuman_cli/_bin/lib/libabsl_vlog_config_internal.2601.0.0.dylib +0 -0
  83. bithuman_cli/_bin/lib/libaec.0.dylib +0 -0
  84. bithuman_cli/_bin/lib/libhdf5.320.dylib +0 -0
  85. bithuman_cli/_bin/lib/libhdf5_hl.320.dylib +0 -0
  86. bithuman_cli/_bin/lib/libjpeg.8.dylib +0 -0
  87. bithuman_cli/_bin/lib/libonnx.dylib +0 -0
  88. bithuman_cli/_bin/lib/libonnx_proto.dylib +0 -0
  89. bithuman_cli/_bin/lib/libonnxruntime.1.dylib +0 -0
  90. bithuman_cli/_bin/lib/libprotobuf-lite.35.0.0.dylib +0 -0
  91. bithuman_cli/_bin/lib/libre2.11.dylib +0 -0
  92. bithuman_cli/_bin/lib/libsharpyuv.0.dylib +0 -0
  93. bithuman_cli/_bin/lib/libsz.2.dylib +0 -0
  94. bithuman_cli/_bin/lib/libutf8_range.35.0.0.dylib +0 -0
  95. bithuman_cli/_bin/lib/libutf8_validity.35.0.0.dylib +0 -0
  96. bithuman_cli/_bin/lib/libwebp.7.dylib +0 -0
  97. bithuman_cli/_bin/livekit-server +0 -0
  98. bithuman_cli/_entry.py +112 -0
  99. bithuman_cli/agent.py +316 -0
  100. bithuman_cli/local_plugins/__init__.py +35 -0
  101. bithuman_cli/local_plugins/_resample.py +17 -0
  102. bithuman_cli/local_plugins/_trace.py +86 -0
  103. bithuman_cli/local_plugins/llm.py +192 -0
  104. bithuman_cli/local_plugins/stt.py +140 -0
  105. bithuman_cli/local_plugins/tts.py +152 -0
  106. bithuman_cli-2.3.0.dist-info/METADATA +73 -0
  107. bithuman_cli-2.3.0.dist-info/RECORD +109 -0
  108. bithuman_cli-2.3.0.dist-info/WHEEL +5 -0
  109. bithuman_cli-2.3.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,22 @@
1
+ """bithuman-cli — the bitHuman command-line tool.
2
+
3
+ Internal package. Users interact via the `bithuman` console script
4
+ installed by this wheel:
5
+
6
+ bithuman run model.imx # live avatar
7
+ bithuman render -a in.wav -o out.mp4
8
+ bithuman doctor
9
+
10
+ Programmatic invocation is also supported:
11
+
12
+ python -m bithuman_cli # equivalent to running `bithuman`
13
+
14
+ For embedding the bitHuman runtime in a Python application, use the
15
+ bitHuman SDK directly (this wheel depends on it):
16
+
17
+ from bithuman import AsyncBithuman
18
+ """
19
+ from __future__ import annotations
20
+
21
+ # Keep aligned with cpp/bindings/python-cli/pyproject.toml.
22
+ __version__ = "2.3.0"
@@ -0,0 +1,12 @@
1
+ """bithuman-cli module entry: `python -m bithuman_cli <command>`.
2
+
3
+ Equivalent to running the `bithuman` console script installed by this
4
+ wheel — both call `bithuman_cli._entry:main`, which locates the
5
+ bundled Rust binary at `bithuman_cli/_bin/bithuman` and `execvpe`s
6
+ into it.
7
+ """
8
+ import sys
9
+
10
+ from ._entry import main
11
+
12
+ sys.exit(main())
@@ -0,0 +1,250 @@
1
+ """Inline LiveKit avatar bridge — replaces `livekit-plugins-bithuman`.
2
+
3
+ WHY THIS EXISTS
4
+ ---------------
5
+ Upstream `livekit-plugins-bithuman` pins `bithuman<1.12`, which blocks
6
+ installs of our 2.x SDK wheel. Until the upstream pin relaxes (or our
7
+ PR lands on PyPI), `pip install bithuman-cli` would fail to resolve.
8
+
9
+ Rather than wait, we re-implement the small slice of `AvatarSession`
10
+ that the CLI actually uses — the *cloud* / remote-serve path — directly
11
+ on top of livekit-agents primitives. This drops a transitive dependency
12
+ chain (`bithuman-cli → livekit-plugins-bithuman → bithuman<1.12`) and
13
+ makes `pip install bithuman-cli` resolve cleanly on its own.
14
+
15
+ SCOPE — only what `bithuman_cli.agent` calls:
16
+ avatar = AvatarSession(
17
+ api_url=f"{serve_url}/launch",
18
+ api_secret=...,
19
+ avatar_id=...,
20
+ model="essence",
21
+ )
22
+ await avatar.start(session, room=ctx.room)
23
+
24
+ We do NOT reimplement:
25
+ * Local in-process runtime (`model_path`/`runtime` kwargs) — `agent.py`
26
+ has its own `BITHUMAN_LOCAL_AVATAR` branch.
27
+ * Custom avatar images (`avatar_image=`) — CLI never sets these.
28
+ * The multipart/form-data path for `auth.api.bithuman.ai` / expression
29
+ workers — CLI only talks to `bithuman serve`'s `/launch` JSON endpoint.
30
+ * `BithumanGenerator` / `VideoGenerator` — used only by the local path.
31
+
32
+ SUNSET PATH
33
+ -----------
34
+ When upstream relaxes its `bithuman<1.12` pin (tracked in pyproject.toml),
35
+ delete this file and switch `agent.py` back to:
36
+
37
+ from livekit.plugins import bithuman as lk_bh
38
+ avatar = lk_bh.AvatarSession(...)
39
+
40
+ The protocol is identical (this file is a literal extract), so the swap
41
+ is mechanical and behaviour-preserving.
42
+
43
+ Apache-2.0; (c) bitHuman.
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import asyncio
49
+ import logging
50
+ import os
51
+ from typing import Optional
52
+
53
+ import aiohttp
54
+ from livekit import api, rtc
55
+ from livekit.agents import (
56
+ DEFAULT_API_CONNECT_OPTIONS,
57
+ APIConnectionError,
58
+ APIConnectOptions,
59
+ APIStatusError,
60
+ get_job_context,
61
+ utils,
62
+ )
63
+ from livekit.agents.types import ATTRIBUTE_PUBLISH_ON_BEHALF
64
+ from livekit.agents.voice import AgentSession
65
+ from livekit.agents.voice.avatar import (
66
+ AvatarSession as BaseAvatarSession,
67
+ DataStreamAudioOutput,
68
+ )
69
+
70
+ logger = logging.getLogger("bithuman_cli.avatar")
71
+
72
+
73
+ _AVATAR_AGENT_IDENTITY = "bithuman-avatar-agent"
74
+ _AVATAR_AGENT_NAME = "bithuman-avatar-agent"
75
+
76
+
77
+ class BitHumanException(Exception):
78
+ """Raised by the inline avatar bridge for misconfiguration or launch errors."""
79
+
80
+
81
+ class AvatarSession(BaseAvatarSession):
82
+ """Drop-in replacement for `livekit.plugins.bithuman.AvatarSession`,
83
+ cloud-mode only.
84
+
85
+ POSTs an avatar-launch request to ``api_url`` (i.e.
86
+ ``<bithuman-serve>/launch``), then routes the agent's TTS audio to the
87
+ spawned avatar participant via livekit-agents' ``DataStreamAudioOutput``.
88
+ The serve process publishes the rendered video track into the room
89
+ on the avatar participant's behalf.
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ *,
95
+ api_url: str,
96
+ api_secret: str,
97
+ avatar_id: str,
98
+ model: str = "essence",
99
+ conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,
100
+ avatar_participant_identity: Optional[str] = None,
101
+ avatar_participant_name: Optional[str] = None,
102
+ ) -> None:
103
+ super().__init__()
104
+ if not api_url:
105
+ raise BitHumanException("api_url is required")
106
+ if not api_secret:
107
+ raise BitHumanException("api_secret is required")
108
+ if not avatar_id:
109
+ raise BitHumanException("avatar_id is required")
110
+
111
+ self._api_url = api_url
112
+ self._api_secret = api_secret
113
+ self._avatar_id = avatar_id
114
+ self._model = model
115
+ self._conn_options = conn_options
116
+ self._avatar_participant_identity = (
117
+ avatar_participant_identity or _AVATAR_AGENT_IDENTITY
118
+ )
119
+ self._avatar_participant_name = (
120
+ avatar_participant_name or _AVATAR_AGENT_NAME
121
+ )
122
+ self._http_session: Optional[aiohttp.ClientSession] = None
123
+
124
+ @property
125
+ def avatar_identity(self) -> str:
126
+ return self._avatar_participant_identity
127
+
128
+ @property
129
+ def provider(self) -> str:
130
+ return "bithuman"
131
+
132
+ async def start(
133
+ self,
134
+ agent_session: AgentSession,
135
+ room: rtc.Room,
136
+ *,
137
+ livekit_url: Optional[str] = None,
138
+ livekit_api_key: Optional[str] = None,
139
+ livekit_api_secret: Optional[str] = None,
140
+ ) -> None:
141
+ # Base class: registers shutdown callback, starts the
142
+ # wait-for-avatar-join task, wires conversation_item_added.
143
+ await super().start(agent_session, room)
144
+
145
+ livekit_url = livekit_url or os.getenv("LIVEKIT_URL")
146
+ livekit_api_key = livekit_api_key or os.getenv("LIVEKIT_API_KEY")
147
+ livekit_api_secret = livekit_api_secret or os.getenv("LIVEKIT_API_SECRET")
148
+ if not livekit_url or not livekit_api_key or not livekit_api_secret:
149
+ raise BitHumanException(
150
+ "livekit_url, livekit_api_key, and livekit_api_secret must be "
151
+ "set by arguments or environment variables"
152
+ )
153
+
154
+ # Mint a JWT for the avatar participant. The
155
+ # ATTRIBUTE_PUBLISH_ON_BEHALF attribute lets serve publish video
156
+ # tracks attributed to our local agent participant.
157
+ job_ctx = get_job_context()
158
+ local_participant_identity = job_ctx.local_participant_identity
159
+
160
+ attributes: dict[str, str] = {
161
+ ATTRIBUTE_PUBLISH_ON_BEHALF: local_participant_identity,
162
+ "api_secret": self._api_secret,
163
+ "agent_id": self._avatar_id,
164
+ }
165
+
166
+ livekit_token = (
167
+ api.AccessToken(api_key=livekit_api_key, api_secret=livekit_api_secret)
168
+ .with_kind("agent")
169
+ .with_identity(self._avatar_participant_identity)
170
+ .with_name(self._avatar_participant_name)
171
+ .with_grants(api.VideoGrants(room_join=True, room=room.name))
172
+ .with_attributes(attributes)
173
+ .to_jwt()
174
+ )
175
+
176
+ logger.debug("starting avatar session")
177
+ await self._launch(livekit_url, livekit_token, room.name)
178
+
179
+ # Route agent TTS into the avatar over the LiveKit data stream.
180
+ # The avatar reads this audio and renders matching lipsync video.
181
+ agent_session.output.audio = DataStreamAudioOutput(
182
+ room=room,
183
+ destination_identity=self._avatar_participant_identity,
184
+ wait_playback_start=False,
185
+ )
186
+
187
+ async def _launch(
188
+ self, livekit_url: str, livekit_token: str, room_name: str
189
+ ) -> None:
190
+ """POST the avatar-launch request to ``api_url`` (bithuman-serve's
191
+ ``/launch``) using the JSON format expected by serve.
192
+
193
+ The CLI always talks to a custom self-hosted serve endpoint —
194
+ never the default ``auth.api.bithuman.ai`` BitHuman cloud — but
195
+ serve consumes the same JSON shape as the upstream plugin's
196
+ default-API path (``mode: cpu`` + ``agent_id`` + ``api-secret``
197
+ header). No multipart/form-data path needed.
198
+ """
199
+ json_data = {
200
+ "livekit_url": livekit_url,
201
+ "livekit_token": livekit_token,
202
+ "room_name": room_name,
203
+ "mode": "cpu" if self._model == "essence" else "gpu",
204
+ "agent_id": self._avatar_id,
205
+ }
206
+ headers = {
207
+ "Content-Type": "application/json",
208
+ "api-secret": self._api_secret,
209
+ }
210
+
211
+ last_err: Optional[Exception] = None
212
+ for i in range(self._conn_options.max_retry):
213
+ try:
214
+ async with self._ensure_http_session().post(
215
+ self._api_url,
216
+ headers=headers,
217
+ json=json_data,
218
+ timeout=aiohttp.ClientTimeout(
219
+ sock_connect=self._conn_options.timeout
220
+ ),
221
+ ) as response:
222
+ if not response.ok:
223
+ text = await response.text()
224
+ raise APIStatusError(
225
+ "Server returned an error",
226
+ status_code=response.status,
227
+ body=text,
228
+ )
229
+ return
230
+ except Exception as e:
231
+ last_err = e
232
+ if isinstance(e, APIConnectionError):
233
+ logger.warning(
234
+ "failed to call bithuman avatar api",
235
+ extra={"error": str(e)},
236
+ )
237
+ else:
238
+ logger.exception("failed to call bithuman avatar api")
239
+ if i < self._conn_options.max_retry - 1:
240
+ await asyncio.sleep(self._conn_options.retry_interval)
241
+
242
+ raise APIConnectionError(
243
+ f"Failed to start Bithuman Avatar Session after "
244
+ f"{self._conn_options.max_retry} retries: {last_err}"
245
+ )
246
+
247
+ def _ensure_http_session(self) -> aiohttp.ClientSession:
248
+ if self._http_session is None:
249
+ self._http_session = utils.http_context.http_session()
250
+ return self._http_session
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
bithuman_cli/_entry.py ADDED
@@ -0,0 +1,112 @@
1
+ """Python entry shim for the bundled `bithuman` Rust CLI.
2
+
3
+ The wheel ships the Rust binary at `bithuman_cli/_bin/bithuman`
4
+ (per-platform; vendored dylibs sit beside it). This shim is the
5
+ `bithuman` console-script entry point — it locates the binary, sets
6
+ up the env so the embedded conversation agent finds the wheel's Python
7
+ interpreter + bundled `agent.py`, and `execvpe`s into the binary so signal
8
+ handling + tty interaction behave exactly as if the user had invoked
9
+ the Rust binary directly.
10
+
11
+ Why a shim at all (vs. exposing the binary directly under
12
+ `venv/bin/bithuman`):
13
+ - The Rust binary discovers Python via `BITHUMAN_AGENT_PYTHON` env
14
+ (`BITHUMAN_BRAIN_PYTHON` is honored as a deprecated alias for one
15
+ release). A shim is the cleanest place to point that at
16
+ `sys.executable` (the venv's own python) without depending on PATH
17
+ ordering.
18
+ - It also lets the wheel attach a clear error when the binary
19
+ slot is empty (e.g. a sdist install or an unsupported platform).
20
+
21
+ Phase B split (2026-05-28): this file used to live at `bithuman/_cli.py`
22
+ inside the unified `bithuman` wheel. The CLI was extracted into its
23
+ own `bithuman-cli` PyPI wheel; the file is now `bithuman_cli/_entry.py`.
24
+ The `BITHUMAN_WHEEL_VERSION` env that the Rust binary surfaces in
25
+ `bithuman --version` / `bithuman doctor` now reports the `bithuman-cli`
26
+ wheel version (was `bithuman` pre-split).
27
+
28
+ Apache-2.0; (c) bitHuman.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import os
33
+ import platform
34
+ import sys
35
+ from pathlib import Path
36
+
37
+
38
+ _PKG_DIR = Path(__file__).resolve().parent
39
+ _BIN_DIR = _PKG_DIR / "_bin"
40
+
41
+
42
+ def _binary_name() -> str:
43
+ return "bithuman.exe" if platform.system() == "Windows" else "bithuman"
44
+
45
+
46
+ def _binary_path() -> Path:
47
+ return _BIN_DIR / _binary_name()
48
+
49
+
50
+ def _missing_binary_message() -> str:
51
+ triple = f"{platform.system()}-{platform.machine()}".lower()
52
+ return (
53
+ f"bithuman: the bundled CLI binary is missing for this platform "
54
+ f"({triple}).\n"
55
+ f" Expected at: {_binary_path()}\n"
56
+ "\n"
57
+ " This usually means you installed an sdist or a wheel built\n"
58
+ " for a different OS/arch. Reinstall with:\n"
59
+ " pip install --upgrade --force-reinstall bithuman-cli\n"
60
+ "\n"
61
+ " If your platform isn't covered by an upstream wheel yet, see:\n"
62
+ " https://github.com/bithuman-product/bithuman-sdk/issues\n"
63
+ )
64
+
65
+
66
+ def main() -> int:
67
+ binary = _binary_path()
68
+ if not binary.is_file():
69
+ sys.stderr.write(_missing_binary_message())
70
+ return 64 # EX_USAGE
71
+
72
+ # Point the Rust binary's agent discovery at this wheel's Python
73
+ # (the venv that pip-installed us) so the embedded `agent.py`
74
+ # picks up all the dependencies (livekit-agents[openai],
75
+ # livekit-plugins-bithuman, aiohttp) declared as required wheel deps.
76
+ # Set BOTH the new (`BITHUMAN_AGENT_*`) and the deprecated
77
+ # (`BITHUMAN_BRAIN_*`) names so an old Rust binary in a mixed install
78
+ # still picks them up; the Rust side prefers the new names.
79
+ env = os.environ.copy()
80
+ env.setdefault("BITHUMAN_AGENT_PYTHON", sys.executable)
81
+ env.setdefault("BITHUMAN_BRAIN_PYTHON", sys.executable)
82
+ agent_script = _PKG_DIR / "agent.py"
83
+ if agent_script.is_file():
84
+ env.setdefault("BITHUMAN_AGENT_SCRIPT", str(agent_script))
85
+ env.setdefault("BITHUMAN_BRAIN_SCRIPT", str(agent_script))
86
+
87
+ # Expose the pip wheel version to the Rust binary so `bithuman doctor`
88
+ # and `bithuman --version` can show the PyPI package version
89
+ # alongside the libessence engine. Use importlib.metadata as the
90
+ # source of truth (always matches the actual installed wheel).
91
+ # Phase B split: this is the `bithuman-cli` wheel; the user-visible
92
+ # version shown by `bithuman --version` is the CLI wheel's version
93
+ # (was `bithuman` pre-split — same value when they release in
94
+ # lockstep, but conceptually it's the CLI version now).
95
+ try:
96
+ from importlib.metadata import version as _pkg_version
97
+ env.setdefault("BITHUMAN_WHEEL_VERSION", _pkg_version("bithuman-cli"))
98
+ except Exception:
99
+ pass
100
+
101
+ argv = [str(binary), *sys.argv[1:]]
102
+ # execvpe replaces the current process — the Rust binary inherits
103
+ # the tty + signal handlers cleanly. On Windows there's no execve
104
+ # equivalent; we'd subprocess.run() + sys.exit() instead, but Phase
105
+ # 4 ships macOS arm64 + Linux first, so we punt on that branch.
106
+ os.execvpe(str(binary), argv, env)
107
+ # Unreachable; execvpe never returns on success.
108
+ return 1
109
+
110
+
111
+ if __name__ == "__main__":
112
+ raise SystemExit(main())