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.
- bithuman/__init__.py +13 -0
- bithuman/_version.py +1 -0
- bithuman/api.py +164 -0
- bithuman/audio/__init__.py +19 -0
- bithuman/audio/audio.py +396 -0
- bithuman/audio/hparams.py +108 -0
- bithuman/audio/utils.py +255 -0
- bithuman/config.py +88 -0
- bithuman/engine/__init__.py +15 -0
- bithuman/engine/auth.py +335 -0
- bithuman/engine/compression.py +257 -0
- bithuman/engine/enums.py +16 -0
- bithuman/engine/image_ops.py +192 -0
- bithuman/engine/inference.py +108 -0
- bithuman/engine/knn.py +58 -0
- bithuman/engine/video_data.py +391 -0
- bithuman/engine/video_reader.py +168 -0
- bithuman/lib/__init__.py +1 -0
- bithuman/lib/audio_encoder.onnx +45631 -28
- bithuman/lib/generator.py +763 -0
- bithuman/lib/pth2h5.py +106 -0
- bithuman/plugins/__init__.py +0 -0
- bithuman/plugins/stt.py +185 -0
- bithuman/runtime.py +1004 -0
- bithuman/runtime_async.py +469 -0
- bithuman/service/__init__.py +9 -0
- bithuman/service/client.py +788 -0
- bithuman/service/messages.py +210 -0
- bithuman/service/server.py +759 -0
- bithuman/utils/__init__.py +43 -0
- bithuman/utils/agent.py +359 -0
- bithuman/utils/fps_controller.py +90 -0
- bithuman/utils/image.py +41 -0
- bithuman/utils/unzip.py +38 -0
- bithuman/video_graph/__init__.py +16 -0
- bithuman/video_graph/action_trigger.py +83 -0
- bithuman/video_graph/driver_video.py +482 -0
- bithuman/video_graph/navigator.py +736 -0
- bithuman/video_graph/trigger.py +90 -0
- bithuman/video_graph/video_script.py +344 -0
- bithuman-1.0.2.dist-info/METADATA +37 -0
- bithuman-1.0.2.dist-info/RECORD +44 -0
- bithuman-1.0.2.dist-info/WHEEL +5 -0
- 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
|
+
|