bithuman 1.0.2__py3-none-any.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 (44) hide show
  1. bithuman/__init__.py +13 -0
  2. bithuman/_version.py +1 -0
  3. bithuman/api.py +164 -0
  4. bithuman/audio/__init__.py +19 -0
  5. bithuman/audio/audio.py +396 -0
  6. bithuman/audio/hparams.py +108 -0
  7. bithuman/audio/utils.py +255 -0
  8. bithuman/config.py +88 -0
  9. bithuman/engine/__init__.py +15 -0
  10. bithuman/engine/auth.py +335 -0
  11. bithuman/engine/compression.py +257 -0
  12. bithuman/engine/enums.py +16 -0
  13. bithuman/engine/image_ops.py +192 -0
  14. bithuman/engine/inference.py +108 -0
  15. bithuman/engine/knn.py +58 -0
  16. bithuman/engine/video_data.py +391 -0
  17. bithuman/engine/video_reader.py +168 -0
  18. bithuman/lib/__init__.py +1 -0
  19. bithuman/lib/audio_encoder.onnx +45631 -28
  20. bithuman/lib/generator.py +763 -0
  21. bithuman/lib/pth2h5.py +106 -0
  22. bithuman/plugins/__init__.py +0 -0
  23. bithuman/plugins/stt.py +185 -0
  24. bithuman/runtime.py +1004 -0
  25. bithuman/runtime_async.py +469 -0
  26. bithuman/service/__init__.py +9 -0
  27. bithuman/service/client.py +788 -0
  28. bithuman/service/messages.py +210 -0
  29. bithuman/service/server.py +759 -0
  30. bithuman/utils/__init__.py +43 -0
  31. bithuman/utils/agent.py +359 -0
  32. bithuman/utils/fps_controller.py +90 -0
  33. bithuman/utils/image.py +41 -0
  34. bithuman/utils/unzip.py +38 -0
  35. bithuman/video_graph/__init__.py +16 -0
  36. bithuman/video_graph/action_trigger.py +83 -0
  37. bithuman/video_graph/driver_video.py +482 -0
  38. bithuman/video_graph/navigator.py +736 -0
  39. bithuman/video_graph/trigger.py +90 -0
  40. bithuman/video_graph/video_script.py +344 -0
  41. bithuman-1.0.2.dist-info/METADATA +37 -0
  42. bithuman-1.0.2.dist-info/RECORD +44 -0
  43. bithuman-1.0.2.dist-info/WHEEL +5 -0
  44. bithuman-1.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,469 @@
1
+ """Asynchronous wrapper for Bithuman Runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import queue
7
+ import threading
8
+ import time
9
+ from pathlib import Path
10
+ from typing import AsyncIterator, Optional, Union
11
+
12
+ from loguru import logger
13
+
14
+ from .api import VideoControl, VideoFrame
15
+ from .runtime import Bithuman, BufferEmptyCallback
16
+
17
+ # Sentinel to signal end of frame stream
18
+ _STREAM_END = object()
19
+
20
+
21
+ class AsyncBithuman(Bithuman):
22
+ """Asynchronous wrapper for Bithuman Runtime.
23
+
24
+ This class wraps the synchronous BithumanRuntime to provide an asynchronous interface.
25
+ It runs the runtime in a separate thread to avoid blocking the asyncio event loop.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ *,
31
+ model_path: Optional[str] = None,
32
+ token: Optional[str] = None,
33
+ api_secret: Optional[str] = None,
34
+ api_url: str = "https://auth.api.bithuman.ai/v1/runtime-tokens/request",
35
+ tags: Optional[str] = "bithuman",
36
+ insecure: bool = True,
37
+ input_buffer_size: int = 0,
38
+ output_buffer_size: int = 5,
39
+ load_model: bool = False,
40
+ num_threads: int = 0,
41
+ verbose: Optional[bool] = None,
42
+ ) -> None:
43
+ """Initialize the async runtime with a BithumanRuntime instance.
44
+
45
+ Args:
46
+ model_path: The path to the avatar model.
47
+ token: The token for the Bithuman Runtime. Either token or api_secret must be provided.
48
+ api_secret: API Secret for API authentication. Either token or api_secret must be provided.
49
+ api_url: API endpoint URL for token requests.
50
+ tags: Optional tags for token request.
51
+ insecure: Disable SSL certificate verification (not recommended for production use).
52
+ input_buffer_size: Size of the input buffer.
53
+ output_buffer_size: Size of the output buffer.
54
+ load_model: If True, load the model synchronously.
55
+ num_threads: Number of threads for processing, 0 = single-threaded, >0 = use specified number of threads, <0 = auto-detect optimal thread count
56
+ verbose: Enable verbose logging for token validation. If None, reads from BITHUMAN_VERBOSE environment variable.
57
+ """
58
+ # Call parent init WITHOUT the model_path parameter
59
+ # This prevents parent's __init__ from calling set_model()
60
+ logger.debug(
61
+ f"Initializing AsyncBithuman with token={token is not None}, api_secret={api_secret is not None}, verbose={verbose}"
62
+ )
63
+ super().__init__(
64
+ input_buffer_size=input_buffer_size,
65
+ token=token,
66
+ model_path=None, # Important: Pass None here
67
+ api_secret=api_secret,
68
+ api_url=api_url,
69
+ tags=tags,
70
+ insecure=insecure,
71
+ verbose=verbose,
72
+ num_threads=num_threads,
73
+ )
74
+
75
+ # Store the model path for later use
76
+ self._model_path = model_path
77
+
78
+ self._model_hash = None
79
+
80
+ # Thread management
81
+ self._stop_event = threading.Event()
82
+ self._thread = None
83
+
84
+ # Thread-safe queue for cross-thread frame passing (producer thread → async consumer)
85
+ # Using queue.Queue avoids per-frame Future creation overhead of asyncio.run_coroutine_threadsafe
86
+ self._frame_queue: queue.Queue[Union[VideoFrame, Exception, object]] = queue.Queue(
87
+ maxsize=output_buffer_size
88
+ )
89
+
90
+ # State
91
+ self._running = False
92
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
93
+
94
+ if load_model:
95
+ self._initialize_token_sync()
96
+ super().set_model(model_path)
97
+
98
+ @classmethod
99
+ async def create(
100
+ cls,
101
+ *,
102
+ model_path: Optional[str] = None,
103
+ token: Optional[str] = None,
104
+ api_secret: Optional[str] = None,
105
+ api_url: str = "https://auth.api.bithuman.ai/v1/runtime-tokens/request",
106
+ tags: Optional[str] = "bithuman",
107
+ insecure: bool = True,
108
+ input_buffer_size: int = 0,
109
+ output_buffer_size: int = 5,
110
+ num_threads: int = 0,
111
+ verbose: Optional[bool] = None,
112
+ ) -> "AsyncBithuman":
113
+ """Create a fully initialized AsyncBithuman instance asynchronously.
114
+
115
+ Token refresh will start lazily when start() is called if api_secret is provided.
116
+ This prevents unnecessary token requests during prewarm/initialization.
117
+ """
118
+ # Create instance with initial parameters but defer model setting
119
+ instance = cls(
120
+ model_path=None, # Will set model later
121
+ token=token,
122
+ api_secret=api_secret,
123
+ api_url=api_url,
124
+ tags=tags,
125
+ insecure=insecure,
126
+ input_buffer_size=input_buffer_size,
127
+ output_buffer_size=output_buffer_size,
128
+ verbose=verbose,
129
+ )
130
+
131
+ if model_path:
132
+ instance._model_path = model_path
133
+ await instance._initialize_token()
134
+ await instance.set_model(model_path)
135
+
136
+ return instance
137
+
138
+ async def set_model(self, model_path: str | None = None) -> "AsyncBithuman":
139
+ """Set the avatar model for the runtime.
140
+
141
+ Args:
142
+ model_path: The path to the avatar model. If None, uses the model_path provided during initialization.
143
+ """
144
+ # Use the model path provided during initialization if none is provided
145
+ model_path = model_path or self._model_path
146
+
147
+ if not model_path:
148
+ logger.error("No model path provided for set_model")
149
+ raise ValueError(
150
+ "Model path must be provided either during initialization or when calling set_model"
151
+ )
152
+
153
+ # Store the model path for token requests
154
+ self._model_path = model_path
155
+
156
+ # Now run the set_model in the executor and wait for it to finish
157
+ loop = self._loop or asyncio.get_running_loop()
158
+ try:
159
+ await loop.run_in_executor(None, super().set_model, model_path)
160
+ except Exception as e:
161
+ logger.error(f"Error in parent set_model: {e}")
162
+ raise
163
+
164
+ return self
165
+
166
+ async def push_audio(
167
+ self, data: bytes, sample_rate: int, last_chunk: bool = True
168
+ ) -> None:
169
+ """Push audio data to the runtime asynchronously.
170
+
171
+ Args:
172
+ data: Audio data in bytes.
173
+ sample_rate: Sample rate of the audio.
174
+ last_chunk: Whether this is the last chunk of the speech.
175
+ """
176
+ control = VideoControl.from_audio(data, sample_rate, last_chunk)
177
+ await self._input_buffer.aput(control)
178
+
179
+ async def push(self, control: VideoControl) -> None:
180
+ """Push a VideoControl to the runtime asynchronously.
181
+
182
+ Args:
183
+ control: The VideoControl to push.
184
+ """
185
+ await self._input_buffer.aput(control)
186
+
187
+ async def flush(self) -> None:
188
+ """Flush the audio buffer, indicating end of speech."""
189
+ await self._input_buffer.aput(VideoControl(end_of_speech=True))
190
+
191
+ async def run(
192
+ self,
193
+ out_buffer_empty: Optional[BufferEmptyCallback] = None,
194
+ *,
195
+ idle_timeout: float | None = None,
196
+ loop: Optional[asyncio.AbstractEventLoop] = None,
197
+ ) -> AsyncIterator[VideoFrame]:
198
+ """Stream video frames asynchronously.
199
+
200
+ Yields:
201
+ VideoFrame objects from the runtime.
202
+ """
203
+ # Start the runtime if not already running
204
+ await self.start(
205
+ out_buffer_empty=out_buffer_empty,
206
+ idle_timeout=idle_timeout,
207
+ loop=loop,
208
+ )
209
+
210
+ try:
211
+ loop = asyncio.get_running_loop()
212
+ while True:
213
+ # Get the next frame from the thread-safe queue via executor
214
+ # This avoids per-frame Future creation overhead
215
+ item = await loop.run_in_executor(None, self._frame_queue.get)
216
+
217
+ # Check for stream end sentinel
218
+ if item is _STREAM_END:
219
+ break
220
+
221
+ # If we got an exception, raise it
222
+ if isinstance(item, Exception):
223
+ # Check if it's a token validation error
224
+ # Use parent class's unified error handler
225
+ if isinstance(item, RuntimeError):
226
+ try:
227
+ # Try to use unified error handler
228
+ # If it's a token error, returns standardized error; otherwise re-raises
229
+ standardized_error = self._handle_token_validation_error(item, "async run loop")
230
+ # If we get here, it's a token validation error
231
+ logger.error(f"Token validation failed: {str(item)}, stopping runtime")
232
+ await self.stop()
233
+ raise standardized_error from item
234
+ except RuntimeError as e:
235
+ # If handler re-raised (not a token error), check if it's the same exception
236
+ if e is item:
237
+ # Not a token error, re-raise original
238
+ raise
239
+ # Otherwise it's a different RuntimeError from the handler, raise it
240
+ raise
241
+
242
+ # For non-RuntimeError exceptions, just re-raise
243
+ raise item
244
+
245
+ # Yield the frame
246
+ yield item
247
+
248
+ except asyncio.CancelledError:
249
+ # Stream was cancelled, stop the runtime
250
+ await self.stop()
251
+ raise
252
+
253
+ async def _initialize_token(self) -> None:
254
+ """Initialize token if provided by user.
255
+
256
+ If user provided a token, validate and set it.
257
+ If user provided api_secret, token refresh is handled automatically.
258
+ """
259
+ if self._token:
260
+ logger.debug("Token provided, validating...")
261
+ try:
262
+ loop = self._loop or asyncio.get_running_loop()
263
+ is_valid = await loop.run_in_executor(
264
+ None,
265
+ lambda: self.generator._generator.validate_token(self._token, self._verbose)
266
+ )
267
+ if not is_valid:
268
+ raise ValueError("Token validation failed")
269
+ logger.debug("Token validated and set successfully")
270
+ except Exception as e:
271
+ logger.warning(f"Token validation failed: {e}")
272
+ raise
273
+ # If api_secret is provided, token refresh is handled automatically
274
+
275
+ def _initialize_token_sync(self) -> None:
276
+ """Initialize token if provided by user (synchronous version).
277
+
278
+ If user provided a token, validate and set it.
279
+ If user provided api_secret, token refresh is handled automatically.
280
+ """
281
+ if self._token:
282
+ is_valid = self.generator._generator.validate_token(self._token, self._verbose)
283
+ if not is_valid:
284
+ logger.warning("Token validation failed")
285
+ raise ValueError("Token validation failed")
286
+ # If api_secret is provided, token refresh is handled automatically
287
+
288
+ async def start(
289
+ self,
290
+ out_buffer_empty: Optional[BufferEmptyCallback] = None,
291
+ *,
292
+ idle_timeout: float | None = None,
293
+ loop: Optional[asyncio.AbstractEventLoop] = None,
294
+ ) -> None:
295
+ """Start the runtime thread."""
296
+ if self._running:
297
+ logger.debug("Runtime already running, skipping start")
298
+ return
299
+
300
+ # Start token refresh if api_secret is provided and refresh is not already running
301
+ # This ensures token is available before runtime starts processing
302
+ if self._api_secret and self._api_url and self._model_path:
303
+ if not self.generator.is_token_refresh_running():
304
+ try:
305
+ # Generate transaction ID before starting token refresh
306
+ if not self.transaction_id:
307
+ self._regenerate_transaction_id()
308
+
309
+ # Run token refresh start in executor since it's synchronous
310
+ loop_exec = loop or asyncio.get_running_loop()
311
+ success = await loop_exec.run_in_executor(
312
+ None,
313
+ self.generator.start_token_refresh,
314
+ self._api_url,
315
+ self._api_secret,
316
+ self._model_path,
317
+ self._tags,
318
+ 60, # refresh_interval
319
+ self._insecure,
320
+ 30.0 # timeout
321
+ )
322
+ if success:
323
+ logger.debug("Token refresh started in start()")
324
+ self._token_refresh_started = True
325
+ # startTokenRefresh does synchronous initial token request,
326
+ # so token is already validated when run_in_executor returns
327
+ else:
328
+ logger.error("Failed to start token refresh in start()")
329
+ raise RuntimeError("Failed to start token refresh")
330
+ except Exception as e:
331
+ logger.error(f"Failed to start token refresh in start(): {e}")
332
+ raise
333
+ else:
334
+ # Token refresh already running - just ensure transaction ID is set
335
+ if not self.transaction_id:
336
+ self._regenerate_transaction_id()
337
+ else:
338
+ # Generate transaction ID only if not already set (prevents bypassing billing)
339
+ if not self.transaction_id:
340
+ self._regenerate_transaction_id()
341
+
342
+ # Store the current event loop
343
+ self._loop = loop or asyncio.get_running_loop()
344
+ self._input_buffer.set_loop(self._loop)
345
+
346
+ # Clear the stop event
347
+ self._stop_event.clear()
348
+
349
+ # Start the runtime thread
350
+ self._running = True
351
+ self._thread = threading.Thread(
352
+ target=self._frame_producer,
353
+ kwargs={"out_buffer_empty": out_buffer_empty, "idle_timeout": idle_timeout},
354
+ )
355
+ self._thread.daemon = True
356
+ self._thread.start()
357
+
358
+ async def stop(self) -> None:
359
+ """Stop the runtime thread and token refresh task."""
360
+ if not self._running:
361
+ return
362
+
363
+ # Set the stop event
364
+ self._stop_event.set()
365
+
366
+ # Token refresh is automatically stopped (BithumanRuntime destructor)
367
+ # Wait for the thread to finish
368
+ if self._thread and self._thread.is_alive():
369
+ self._thread.join(timeout=1.0)
370
+
371
+ # Reset state
372
+ self._running = False
373
+
374
+ def _frame_producer(
375
+ self,
376
+ out_buffer_empty: Optional[BufferEmptyCallback] = None,
377
+ *,
378
+ idle_timeout: float | None = None,
379
+ ) -> None:
380
+ """Run the runtime in a separate thread and produce frames."""
381
+ try:
382
+ # Run the runtime and process frames
383
+ out_buffer_empty = out_buffer_empty or self._frame_queue.empty
384
+ frame_iterator = None
385
+ try:
386
+ frame_iterator = super().run(
387
+ out_buffer_empty, idle_timeout=idle_timeout
388
+ )
389
+ except RuntimeError as e:
390
+ # Catch token validation errors during initialization
391
+ error_msg = str(e)
392
+ if ("Token has expired" in error_msg or
393
+ "Token validation failed" in error_msg or
394
+ "token has expired" in error_msg.lower() or
395
+ "validation failed" in error_msg.lower()):
396
+ logger.error(f"Token validation failed during frame iterator initialization: {error_msg}")
397
+ # Put exception in queue
398
+ try:
399
+ self._frame_queue.put(
400
+ RuntimeError("Token validation failed: token has expired")
401
+ )
402
+ except Exception as e2:
403
+ logger.error(f"Error putting token validation error in frame queue: {e2}")
404
+ return
405
+ raise
406
+ except Exception as e:
407
+ logger.error(f"Error initializing frame iterator in run(): {e}")
408
+ raise
409
+
410
+ if frame_iterator:
411
+ for frame in frame_iterator:
412
+ if self._stop_event.is_set():
413
+ logger.debug("Stop event set, stopping frame producer")
414
+ break
415
+
416
+ # Put the frame in the thread-safe queue (blocks if full for backpressure)
417
+ try:
418
+ self._frame_queue.put(frame)
419
+ except RuntimeError as e:
420
+ # Catch token validation errors
421
+ error_msg = str(e).lower()
422
+ if ("token has expired" in error_msg or
423
+ "token validation failed" in error_msg or
424
+ "validation failed" in error_msg):
425
+ logger.error(f"Token validation failed in frame producer: {str(e)}")
426
+ self._frame_queue.put(
427
+ RuntimeError("Token validation failed: token has expired")
428
+ )
429
+ logger.debug("Frame producer stopped due to token validation error")
430
+ break
431
+ raise
432
+
433
+ # Log when frame iterator completes
434
+ logger.debug("Frame iterator completed")
435
+ else:
436
+ logger.error("Frame iterator is None")
437
+
438
+ except Exception as e:
439
+ logger.error(f"Exception in frame producer: {e}")
440
+ # If an exception occurs, put it in the frame queue
441
+ try:
442
+ self._frame_queue.put(e)
443
+ except Exception as e2:
444
+ logger.error(f"Error putting exception in frame queue: {e2}")
445
+ finally:
446
+ # Signal end of stream
447
+ try:
448
+ self._frame_queue.put(_STREAM_END)
449
+ except Exception:
450
+ pass
451
+
452
+ async def load_data_async(self) -> None:
453
+ """Load the workspace and set up related components asynchronously."""
454
+ if self._video_loaded:
455
+ return
456
+ if self.video_graph is None:
457
+ logger.error("Video graph is None. Model may not be set properly.")
458
+ raise ValueError("Video graph is not set. Call set_avatar_model() first.")
459
+
460
+ # Run the synchronous load_data in a thread pool
461
+ loop = self._loop or asyncio.get_running_loop()
462
+ try:
463
+ await loop.run_in_executor(None, super().load_data)
464
+ self._video_loaded = True
465
+ except Exception as e:
466
+ logger.error(f"Error in load_data: {e}")
467
+ raise
468
+
469
+
@@ -0,0 +1,9 @@
1
+ from . import messages
2
+ from .client import ZMQBithumanRuntimeClient
3
+ from .server import ZMQBithumanRuntimeServer
4
+
5
+ __all__ = [
6
+ "ZMQBithumanRuntimeClient",
7
+ "ZMQBithumanRuntimeServer",
8
+ "messages",
9
+ ]