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.
- bithuman_cli/__init__.py +22 -0
- bithuman_cli/__main__.py +12 -0
- bithuman_cli/_avatar_bridge.py +250 -0
- bithuman_cli/_bin/bithuman +0 -0
- bithuman_cli/_bin/lib/libabsl_base.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_borrowed_fixup_buffer.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_city.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_civil_time.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_cord.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_cord_internal.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_cordz_functions.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_cordz_handle.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_cordz_info.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_crc32c.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_crc_cord_state.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_crc_cpu_detect.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_crc_internal.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_debugging_internal.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_decode_rust_punycode.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_demangle_internal.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_demangle_rust.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_die_if_null.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_examine_stack.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_exponential_biased.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_flags_commandlineflag.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_flags_commandlineflag_internal.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_flags_config.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_flags_internal.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_flags_marshalling.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_flags_private_handle_accessor.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_flags_program_name.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_flags_reflection.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_graphcycles_internal.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_hash.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_hashtablez_sampler.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_int128.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_kernel_timeout_internal.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_leak_check.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_log_entry.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_log_globals.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_log_initialize.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_log_internal_check_op.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_log_internal_conditions.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_log_internal_fnmatch.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_log_internal_format.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_log_internal_globals.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_log_internal_log_sink_set.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_log_internal_message.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_log_internal_nullguard.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_log_internal_proto.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_log_internal_structured_proto.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_log_severity.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_log_sink.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_malloc_internal.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_random_distributions.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_random_internal_entropy_pool.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_random_internal_platform.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_random_internal_randen.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_random_internal_randen_hwaes.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_random_internal_randen_hwaes_impl.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_random_internal_randen_slow.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_random_internal_seed_material.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_random_seed_gen_exception.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_random_seed_sequences.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_raw_hash_set.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_raw_logging_internal.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_spinlock_wait.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_stacktrace.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_status.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_statusor.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_str_format_internal.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_strerror.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_strings.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_strings_internal.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_symbolize.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_synchronization.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_throw_delegate.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_time.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_time_zone.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_tracing_internal.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_utf8_for_code_point.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libabsl_vlog_config_internal.2601.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libaec.0.dylib +0 -0
- bithuman_cli/_bin/lib/libhdf5.320.dylib +0 -0
- bithuman_cli/_bin/lib/libhdf5_hl.320.dylib +0 -0
- bithuman_cli/_bin/lib/libjpeg.8.dylib +0 -0
- bithuman_cli/_bin/lib/libonnx.dylib +0 -0
- bithuman_cli/_bin/lib/libonnx_proto.dylib +0 -0
- bithuman_cli/_bin/lib/libonnxruntime.1.dylib +0 -0
- bithuman_cli/_bin/lib/libprotobuf-lite.35.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libre2.11.dylib +0 -0
- bithuman_cli/_bin/lib/libsharpyuv.0.dylib +0 -0
- bithuman_cli/_bin/lib/libsz.2.dylib +0 -0
- bithuman_cli/_bin/lib/libutf8_range.35.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libutf8_validity.35.0.0.dylib +0 -0
- bithuman_cli/_bin/lib/libwebp.7.dylib +0 -0
- bithuman_cli/_bin/livekit-server +0 -0
- bithuman_cli/_entry.py +112 -0
- bithuman_cli/agent.py +316 -0
- bithuman_cli/local_plugins/__init__.py +35 -0
- bithuman_cli/local_plugins/_resample.py +17 -0
- bithuman_cli/local_plugins/_trace.py +86 -0
- bithuman_cli/local_plugins/llm.py +192 -0
- bithuman_cli/local_plugins/stt.py +140 -0
- bithuman_cli/local_plugins/tts.py +152 -0
- bithuman_cli-2.3.0.dist-info/METADATA +73 -0
- bithuman_cli-2.3.0.dist-info/RECORD +109 -0
- bithuman_cli-2.3.0.dist-info/WHEEL +5 -0
- bithuman_cli-2.3.0.dist-info/entry_points.txt +3 -0
bithuman_cli/__init__.py
ADDED
|
@@ -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"
|
bithuman_cli/__main__.py
ADDED
|
@@ -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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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())
|