测试通过
This commit is contained in:
+278
-42
@@ -5,42 +5,284 @@ mode: auto
|
||||
bind: 0.0.0.0:8018
|
||||
listen: https://0.0.0.0:8018
|
||||
browser test page: https://<server-ip>:8018/
|
||||
INFO: Started server process [17367]
|
||||
INFO: Started server process [20276]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: Application startup complete.
|
||||
INFO: Uvicorn running on https://0.0.0.0:8018 (Press CTRL+C to quit)
|
||||
INFO: 127.0.0.1:38170 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: 127.0.0.1:35988 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: 127.0.0.1:38188 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: 127.0.0.1:38202 - "GET /health HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:38220 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: connection closed
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:38220 - "GET /web/app.js?v=20260329k HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:38220 - "GET /web/models/VRM1_Constraint_Twist_Sample.vrm HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:38256 - "GET /health HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:38266 - "GET /events HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:38286 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: 127.0.0.1:38302 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: 127.0.0.1:40412 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: connection closed
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:40412 - "GET /web/app.js?v=20260329k HTTP/1.1" 304 Not Modified
|
||||
INFO: 127.0.0.1:40412 - "GET /web/models/VRM1_Constraint_Twist_Sample.vrm HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:40440 - "GET /events HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:40436 - "GET /health HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:40462 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: 127.0.0.1:40472 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: 127.0.0.1:52378 - "WebSocket /ws/audio" [accepted]
|
||||
INFO: 127.0.0.1:36002 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:52392 - "WebSocket /ws/audio" [accepted]
|
||||
INFO: 127.0.0.1:36022 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: 127.0.0.1:36036 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:36050 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:36072 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:36078 - "GET /health HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:36104 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:36118 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:36148 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:36168 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:36194 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:36206 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:36224 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54498 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54524 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54542 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54562 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54584 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54596 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54608 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54628 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54642 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54650 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54664 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54684 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54706 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54718 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54748 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54756 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:54788 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59782 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59802 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59812 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59832 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59860 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59874 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59888 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59902 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59906 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59910 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59922 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59928 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59944 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59962 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59976 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:60002 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:58794 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:58800 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:58830 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:58854 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:58884 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:58906 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:58930 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:58950 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:58974 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:58986 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59002 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59026 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59046 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: connection closed
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59046 - "GET /web/app.js?v=20260329m HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:59046 - "GET /web/app.js?v=20260329q HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:59046 - "GET /web/models/VRM1_Constraint_Twist_Sample.vrm HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:59114 - "GET /health HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:59124 - "GET /events HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:59114 - "GET /events HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:59150 - "GET /health HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:59174 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: 127.0.0.1:59184 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: 127.0.0.1:59186 - "WebSocket /ws/subtitles" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:59190 - "WebSocket /ws/animation" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:44402 - "WebSocket /ws/audio" [accepted]
|
||||
INFO: connection open
|
||||
INFO: connection closed
|
||||
INFO: 127.0.0.1:44446 - "WebSocket /ws/audio" [accepted]
|
||||
INFO: connection open
|
||||
INFO: 127.0.0.1:44450 - "WebSocket /ws/audio" [accepted]
|
||||
INFO: connection open
|
||||
INFO: 127.0.0.1:44472 - "WebSocket /ws/audio" [accepted]
|
||||
INFO: connection closed
|
||||
INFO: connection closed
|
||||
INFO: connection open
|
||||
ERROR: Exception in ASGI application
|
||||
Traceback (most recent call last):
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/uvicorn/protocols/websockets/websockets_impl.py", line 329, in asgi_send
|
||||
await self.send(data) # type: ignore[arg-type]
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/websockets/legacy/protocol.py", line 618, in send
|
||||
await self.ensure_open()
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/websockets/legacy/protocol.py", line 916, in ensure_open
|
||||
raise self.connection_closed_exc()
|
||||
websockets.exceptions.ConnectionClosedOK: received 1000 (OK) stale-open; then sent 1000 (OK) stale-open
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/starlette/websockets.py", line 86, in send
|
||||
await self._send(message)
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 39, in sender
|
||||
await send(message)
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/uvicorn/protocols/websockets/websockets_impl.py", line 342, in asgi_send
|
||||
raise ClientDisconnected from exc
|
||||
uvicorn.protocols.utils.ClientDisconnected
|
||||
|
||||
During handling of the above exception, another exception occurred:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/uvicorn/protocols/websockets/websockets_impl.py", line 244, in run_asgi
|
||||
result = await self.app(self.scope, self.asgi_receive, self.asgi_send) # type: ignore[func-returns-value]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py", line 60, in __call__
|
||||
return await self.app(scope, receive, send)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/fastapi/applications.py", line 1159, in __call__
|
||||
await super().__call__(scope, receive, send)
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/starlette/applications.py", line 90, in __call__
|
||||
await self.middleware_stack(scope, receive, send)
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 151, in __call__
|
||||
await self.app(scope, receive, send)
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/starlette/middleware/cors.py", line 80, in __call__
|
||||
await self.app(scope, receive, send)
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 63, in __call__
|
||||
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
|
||||
raise exc
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
|
||||
await app(scope, receive, sender)
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
|
||||
await self.app(scope, receive, send)
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/starlette/routing.py", line 660, in __call__
|
||||
await self.middleware_stack(scope, receive, send)
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/starlette/routing.py", line 680, in app
|
||||
await route.handle(scope, receive, send)
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/starlette/routing.py", line 350, in handle
|
||||
await self.app(scope, receive, send)
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/fastapi/routing.py", line 160, in app
|
||||
await wrap_app_handling_exceptions(app, session)(scope, receive, send)
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
|
||||
raise exc
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
|
||||
await app(scope, receive, sender)
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/fastapi/routing.py", line 157, in app
|
||||
await func(session)
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/fastapi/routing.py", line 764, in app
|
||||
await dependant.call(**solved_result.values)
|
||||
File "/home/xsl/code/product/main.py", line 446, in ws_audio
|
||||
await websocket.send_text(
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/starlette/websockets.py", line 166, in send_text
|
||||
await self.send({"type": "websocket.send", "text": data})
|
||||
File "/home/xsl/code/product/.venv/lib/python3.12/site-packages/starlette/websockets.py", line 89, in send
|
||||
raise WebSocketDisconnect(code=1006)
|
||||
starlette.websockets.WebSocketDisconnect
|
||||
INFO:httpx:HTTP Request: POST https://api.deepseek.com/v1/chat/completions "HTTP/1.1 200 OK"
|
||||
/home/xsl/code/product/.venv/lib/python3.12/site-packages/torch/cuda/__init__.py:180: UserWarning: CUDA initialization: The NVIDIA driver on your system is too old (found version 12090). Please update your GPU driver by downloading and installing a new version from the URL: http://www.nvidia.com/Download/index.aspx Alternatively, go to: https://pytorch.org to install a PyTorch version that has been compiled with your version of the CUDA driver. (Triggered internally at /pytorch/c10/cuda/CUDAFunctions.cpp:119.)
|
||||
return torch._C._cuda_getDeviceCount() > 0
|
||||
@@ -60,20 +302,19 @@ Building prefix dict from the default dictionary ...
|
||||
DEBUG:jieba:Building prefix dict from the default dictionary ...
|
||||
Loading model from cache /tmp/jieba.cache
|
||||
DEBUG:jieba:Loading model from cache /tmp/jieba.cache
|
||||
Loading model cost 0.341 seconds.
|
||||
DEBUG:jieba:Loading model cost 0.341 seconds.
|
||||
Loading model cost 0.356 seconds.
|
||||
DEBUG:jieba:Loading model cost 0.356 seconds.
|
||||
Prefix dict has been built successfully.
|
||||
DEBUG:jieba:Prefix dict has been built successfully.
|
||||
WARNING: Defaulting repo_id to hexgrad/Kokoro-82M. Pass repo_id='hexgrad/Kokoro-82M' to suppress this warning.
|
||||
INFO: 127.0.0.1:42956 - "POST /chat/text HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:42956 - "GET /favicon.ico HTTP/1.1" 404 Not Found
|
||||
INFO: 127.0.0.1:42956 - "GET /favicon.ico HTTP/1.1" 404 Not Found
|
||||
INFO: 127.0.0.1:44470 - "POST /chat/text HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:44470 - "GET /favicon.ico HTTP/1.1" 404 Not Found
|
||||
INFO:root:download models from model hub: hf
|
||||
INFO:httpx:HTTP Request: GET https://huggingface.co/api/models/FunAudioLLM/SenseVoiceSmall/revision/main "HTTP/1.1 200 OK"
|
||||
funasr version: 1.3.1.
|
||||
Check update of funasr, and it would cost few times. You may disable it by set `disable_update=True` in AutoModel
|
||||
You are using the latest version of funasr-1.3.1
|
||||
|
||||
|
||||
Fetching 29 files: 0%| | 0/29 [00:00<?, ?it/s]
|
||||
Fetching 29 files: 100%|██████████| 29/29 [00:00<00:00, 50935.85it/s]
|
||||
WARNING:root:trust_remote_code: False
|
||||
@@ -85,7 +326,7 @@ INFO:httpx:HTTP Request: GET https://huggingface.co/api/models/FunAudioLLM/Sense
|
||||
INFO:root:download models from model hub: hf
|
||||
INFO:httpx:HTTP Request: GET https://huggingface.co/api/models/FunAudioLLM/SenseVoiceSmall/revision/main "HTTP/1.1 200 OK"
|
||||
funasr version: 1.3.1.
|
||||
Check update of funasr, and it would cost few times. You may disable it by set `disable_update=True` in AutoModel
|
||||
Check update of funasr, and it would cost few times. You may disable it by set `disable_update=True` in AutoModel
|
||||
You are using the latest version of funasr-1.3.1
|
||||
|
||||
Fetching 29 files: 0%| | 0/29 [00:00<?, ?it/s]
|
||||
@@ -93,10 +334,5 @@ INFO:root:scope_map: ['module.', 'None']
|
||||
WARNING:root:trust_remote_code: False
|
||||
INFO:root:Loading pretrained params from /home/xsl/.cache/huggingface/hub/models--FunAudioLLM--SenseVoiceSmall/snapshots/3eb3b4eeffc2f2dde6051b853983753db33e35c3/model.pt
|
||||
INFO:root:ckpt: /home/xsl/.cache/huggingface/hub/models--FunAudioLLM--SenseVoiceSmall/snapshots/3eb3b4eeffc2f2dde6051b853983753db33e35c3/model.pt
|
||||
INFO:root:scope_map: ['module.', 'None']
|
||||
INFO:root:scope_map: ['module.', 'None']
|
||||
INFO:root:excludes: None
|
||||
INFO:root:Loading ckpt: /home/xsl/.cache/huggingface/hub/models--FunAudioLLM--SenseVoiceSmall/snapshots/3eb3b4eeffc2f2dde6051b853983753db33e35c3/model.pt, status: <All keys matched successfully>
|
||||
WARNING:services.asr:ASR running in degraded mode on cpu
|
||||
|
||||
0%|[34m [0m| 0/1 [00:00<?, ?it/s]
|
||||
100%|[34m██████████[0m| 1/1 [00:00<00:00, 1.04it/s]
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
17367
|
||||
20276
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
当前形态更接近单机演示和研发验证环境,不是已经拆分完毕的生产化架构。
|
||||
|
||||
当前运行约束:
|
||||
|
||||
- 服务端按“单客户端接管”模式工作。新的浏览器页面连上后,会主动断开旧页面的音频、字幕、动画 WebSocket。
|
||||
- 浏览器刷新后,语音通道可能仍受浏览器媒体安全策略限制,需要一次页面点击或点“连接语音通道”来恢复麦克风与播放上下文。
|
||||
- 前端目前默认保留随机眨眼;更激进的 idle body motion 已回退,避免部分 VRM 模型进入 T pose。
|
||||
|
||||
## 当前架构
|
||||
|
||||
语音链路:
|
||||
@@ -90,6 +96,12 @@ python main.py
|
||||
|
||||
或使用脚本:
|
||||
|
||||
```bash
|
||||
bash scripts/start-test.sh auto
|
||||
```
|
||||
|
||||
如果你明确要固定走当前 shell/虚拟环境里的 Python,也可以用:
|
||||
|
||||
```bash
|
||||
bash scripts/start-public.sh
|
||||
```
|
||||
@@ -107,6 +119,11 @@ python scripts/smoke_test.py --text "你好,请做一个简短自我介绍。"
|
||||
- `http://127.0.0.1:8080/`
|
||||
- `http://127.0.0.1:8080/health`
|
||||
|
||||
如果 `.env` 里已经配置了 `WEBRTC_PORT=8018` 与 `SSL_CERTFILE` / `SSL_KEYFILE`,实际访问地址通常会变成:
|
||||
|
||||
- `https://<server-ip>:8018/`
|
||||
- `https://<server-ip>:8018/health`
|
||||
|
||||
如果浏览器与服务端跨机器访问,麦克风采集通常需要 HTTPS。可以先生成自签证书:
|
||||
|
||||
```bash
|
||||
@@ -133,6 +150,8 @@ bash scripts/gen-self-signed-cert.sh
|
||||
- `WS /ws/animation`: 数字人控制流。
|
||||
- `GET /avatar/schema`: 当前控制协议 schema。
|
||||
|
||||
说明:三个 WebSocket 通道当前都只保留一个活动客户端。新页面接入后,旧页面会收到 `1012 replaced` 并停止接收数据。
|
||||
|
||||
## 后续开发建议
|
||||
|
||||
优先阅读 [docs/development-guide.md](docs/development-guide.md)。这个文档说明了:
|
||||
|
||||
Binary file not shown.
@@ -49,6 +49,14 @@
|
||||
|
||||
当前实现假设一次只保留一个主音频 WebSocket 连接。新连接到来时会主动关闭旧连接。这是刻意简化,不是 bug。
|
||||
|
||||
现在这个约束已经扩展到三个浏览器通道:
|
||||
|
||||
- `WS /ws/audio`
|
||||
- `WS /ws/subtitles`
|
||||
- `WS /ws/animation`
|
||||
|
||||
也就是说,服务端当前只允许一个浏览器页面完整接管数字人会话。新页面连上后,旧页面会被主动断开,关闭码为 `1012 replaced`。
|
||||
|
||||
### 3.2 会话状态机
|
||||
|
||||
[core/state_machine.py](../core/state_machine.py) 的状态很轻:
|
||||
@@ -99,6 +107,7 @@
|
||||
- 维护聊天消息面板和诊断面板。
|
||||
- 用 Three.js + VRM 加载和渲染模型。
|
||||
- 把动画控制帧缓存约 `120ms` 后再播放,尽量和音频保持一致。
|
||||
- 处理刷新后的语音恢复:如果浏览器拦截自动恢复,会等待下一次用户手势再恢复麦克风和播放上下文。
|
||||
|
||||
前端当前兼容三种形态:
|
||||
|
||||
@@ -106,6 +115,11 @@
|
||||
- 通用 GLTF morph target。
|
||||
- 没有表情 rig 时回退到调试头像。
|
||||
|
||||
当前前端待机动画状态:
|
||||
|
||||
- 已启用随机眨眼。
|
||||
- 更激进的身体 idle motion 已回退,因为部分 VRM 模型会出现 T pose 或姿态异常。
|
||||
|
||||
如果以后想接入真实业务数字人模型,先确认模型是否提供:
|
||||
|
||||
- 标准 VRM 表情。
|
||||
@@ -177,10 +191,11 @@
|
||||
这些约束需要后续开发明确知晓:
|
||||
|
||||
- 当前是进程内单例服务,不是多租户、多 session 设计。
|
||||
- 浏览器侧只允许一个主音频 WebSocket 会话接管服务端语音输入输出。
|
||||
- 浏览器侧只允许一个页面完整接管服务端语音输入输出与字幕/动画订阅。
|
||||
- 模型推理基本都直接跑在应用事件循环附近,适合研发验证,不适合高并发。
|
||||
- 当前动画驱动依赖音频能量和谱质心,不具备严格唇音级别精度。
|
||||
- 语音链路高度依赖 VAD;如果要做电话式长连接交互,建议补更稳的 turn management。
|
||||
- 浏览器刷新后不保证零手势恢复语音;这是浏览器媒体策略约束,不是后端故障。
|
||||
|
||||
## 9. 推荐扩展方向
|
||||
|
||||
|
||||
@@ -103,6 +103,17 @@ async def _broadcast_json(clients: set[WebSocket], payload: dict) -> None:
|
||||
clients.discard(ws)
|
||||
|
||||
|
||||
async def _replace_existing_clients(clients: set[WebSocket]) -> bool:
|
||||
replaced_previous = len(clients) > 0
|
||||
if not replaced_previous:
|
||||
return False
|
||||
old_clients = list(clients)
|
||||
await asyncio.gather(*(client.close(code=1012, reason="replaced") for client in old_clients), return_exceptions=True)
|
||||
for client in old_clients:
|
||||
clients.discard(client)
|
||||
return True
|
||||
|
||||
|
||||
async def _broadcast_subtitle(
|
||||
role: str,
|
||||
text: str,
|
||||
@@ -392,6 +403,7 @@ async def events():
|
||||
@app.websocket("/ws/subtitles")
|
||||
async def ws_subtitles(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
await _replace_existing_clients(subtitle_clients)
|
||||
subtitle_clients.add(websocket)
|
||||
try:
|
||||
while True:
|
||||
@@ -406,6 +418,7 @@ async def ws_subtitles(websocket: WebSocket):
|
||||
@app.websocket("/ws/animation")
|
||||
async def ws_animation(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
await _replace_existing_clients(animation_clients)
|
||||
animation_clients.add(websocket)
|
||||
try:
|
||||
await websocket.send_text(json.dumps({"type": "animation_ready", **avatar_service.schema}, ensure_ascii=False))
|
||||
@@ -423,12 +436,7 @@ async def ws_animation(websocket: WebSocket):
|
||||
async def ws_audio(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
|
||||
replaced_previous = len(audio_clients) > 0
|
||||
if replaced_previous:
|
||||
old_clients = list(audio_clients)
|
||||
await asyncio.gather(*(client.close(code=1012, reason="replaced") for client in old_clients), return_exceptions=True)
|
||||
for client in old_clients:
|
||||
audio_clients.discard(client)
|
||||
replaced_previous = await _replace_existing_clients(audio_clients)
|
||||
|
||||
audio_clients.add(websocket)
|
||||
sample_rate = 48000
|
||||
|
||||
+33
-3
@@ -5,8 +5,6 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
cd "$ROOT"
|
||||
|
||||
PY="/home/xsl/miniconda3/envs/MuseTalk/bin/python"
|
||||
|
||||
if [[ -f ".env" ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
@@ -14,6 +12,26 @@ if [[ -f ".env" ]]; then
|
||||
set +a
|
||||
fi
|
||||
|
||||
pick_python() {
|
||||
if [[ -n "${VISUAL_CHAT_PYTHON:-}" && -x "${VISUAL_CHAT_PYTHON}" ]]; then
|
||||
echo "$VISUAL_CHAT_PYTHON"
|
||||
return 0
|
||||
fi
|
||||
if [[ -n "${VIRTUAL_ENV:-}" && -x "${VIRTUAL_ENV}/bin/python" ]]; then
|
||||
echo "${VIRTUAL_ENV}/bin/python"
|
||||
return 0
|
||||
fi
|
||||
if [[ -x "/home/xsl/miniconda3/envs/MuseTalk/bin/python" ]]; then
|
||||
echo "/home/xsl/miniconda3/envs/MuseTalk/bin/python"
|
||||
return 0
|
||||
fi
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
command -v python3
|
||||
return 0
|
||||
fi
|
||||
command -v python
|
||||
}
|
||||
|
||||
normalize_host() {
|
||||
local host="${1:-0.0.0.0}"
|
||||
case "$host" in
|
||||
@@ -27,10 +45,22 @@ normalize_host() {
|
||||
}
|
||||
|
||||
HOST="$(normalize_host "${WEBRTC_HOST:-0.0.0.0}")"
|
||||
ARGS=(main:app --host "$HOST" --port "${WEBRTC_PORT:-8080}")
|
||||
PORT="${WEBRTC_PORT:-8080}"
|
||||
PY="$(pick_python)"
|
||||
SCHEME="http"
|
||||
ARGS=(main:app --host "$HOST" --port "$PORT")
|
||||
|
||||
if [[ -n "${SSL_CERTFILE:-}" && -n "${SSL_KEYFILE:-}" ]]; then
|
||||
SCHEME="https"
|
||||
ARGS+=(--ssl-certfile "$SSL_CERTFILE" --ssl-keyfile "$SSL_KEYFILE")
|
||||
fi
|
||||
|
||||
echo "repo: $ROOT"
|
||||
echo "python: $PY"
|
||||
echo "bind: ${HOST}:${PORT}"
|
||||
echo "listen: ${SCHEME}://${HOST}:${PORT}"
|
||||
echo "browser test page: ${SCHEME}://<server-ip>:${PORT}/"
|
||||
echo "note: only one browser page is allowed to own the session; a newer client replaces the older one."
|
||||
echo "note: after browser refresh, one click may still be required to resume mic/audio due to browser media policy."
|
||||
|
||||
exec "$PY" -m uvicorn "${ARGS[@]}"
|
||||
|
||||
@@ -80,5 +80,7 @@ echo "mode: $MODE"
|
||||
echo "bind: ${HOST}:${PORT}"
|
||||
echo "listen: ${SCHEME}://${HOST}:${PORT}"
|
||||
echo "browser test page: ${SCHEME}://<server-ip>:${PORT}/"
|
||||
echo "note: only one browser page is allowed to own the session; a newer client replaces the older one."
|
||||
echo "note: after browser refresh, one click may still be required to resume mic/audio due to browser media policy."
|
||||
|
||||
exec "$PY" -m uvicorn "${ARGS[@]}"
|
||||
+139
-2
@@ -64,6 +64,7 @@ let micProcessorNode = null;
|
||||
let micSinkGain = null;
|
||||
let audioPlaybackCursor = 0;
|
||||
const audioPlaybackSources = new Set();
|
||||
const recentSubtitleEvents = new Map();
|
||||
let reconnectOnGestureArmed = false;
|
||||
let pageGestureBootstrapArmed = false;
|
||||
|
||||
@@ -127,6 +128,18 @@ const avatarState = {
|
||||
debugRig: null,
|
||||
debugJaw: null,
|
||||
debugMouth: null,
|
||||
debugLeftEye: null,
|
||||
debugRightEye: null,
|
||||
blink: {
|
||||
value: 0,
|
||||
phase: "idle",
|
||||
phaseStartedAtMs: performance.now(),
|
||||
nextAtMs: performance.now() + 1800,
|
||||
closeMs: 90,
|
||||
holdMs: 50,
|
||||
openMs: 120,
|
||||
pendingDoubleBlink: false,
|
||||
},
|
||||
};
|
||||
|
||||
const gltfLoader = new GLTFLoader();
|
||||
@@ -160,6 +173,27 @@ function addMessage(role, text) {
|
||||
return div;
|
||||
}
|
||||
|
||||
function shouldRenderSubtitleEvent(data) {
|
||||
const key = JSON.stringify({
|
||||
role: data?.role || "",
|
||||
text: data?.text || "",
|
||||
partial: Boolean(data?.partial),
|
||||
final: Boolean(data?.final),
|
||||
source: data?.source || "",
|
||||
});
|
||||
const now = Date.now();
|
||||
const lastSeenAt = recentSubtitleEvents.get(key) || 0;
|
||||
recentSubtitleEvents.set(key, now);
|
||||
|
||||
for (const [entryKey, seenAt] of recentSubtitleEvents.entries()) {
|
||||
if (now - seenAt > 1500) {
|
||||
recentSubtitleEvents.delete(entryKey);
|
||||
}
|
||||
}
|
||||
|
||||
return now - lastSeenAt > 350;
|
||||
}
|
||||
|
||||
function setConnBadge(label, kind = "neutral") {
|
||||
connBadge.textContent = label;
|
||||
connBadge.className = `badge ${kind}`;
|
||||
@@ -487,6 +521,8 @@ function createDebugRig() {
|
||||
avatarState.debugRig = group;
|
||||
avatarState.debugJaw = jawPivot;
|
||||
avatarState.debugMouth = mouth;
|
||||
avatarState.debugLeftEye = leftEye;
|
||||
avatarState.debugRightEye = rightEye;
|
||||
avatarState.headBone = neck;
|
||||
avatarState.neckBone = neck;
|
||||
}
|
||||
@@ -505,6 +541,7 @@ function renderLoop() {
|
||||
const elapsed = CLOCK.getElapsedTime();
|
||||
controls.update();
|
||||
flushAnimationQueue();
|
||||
updateBlinkState(performance.now());
|
||||
applyProceduralIdlePose(elapsed);
|
||||
applyAnimationControls(avatarState.activeControls);
|
||||
avatarState.currentVrm?.update?.(delta);
|
||||
@@ -618,6 +655,8 @@ function applyAnimationControls(controls) {
|
||||
setMorphValue("mouthPucker", mouthPucker);
|
||||
}
|
||||
|
||||
applyBlinkControls(avatarState.blink?.value || 0);
|
||||
|
||||
// For plain head meshes (no VRM expressions and no morph targets),
|
||||
// amplify head motion to keep speaking visually obvious.
|
||||
const yawApplied = hasFacialRig ? headYaw * 1.8 : headYaw * 10.0 + Math.sin(t * 5.0) * jawOpen * 0.09;
|
||||
@@ -702,6 +741,8 @@ function detachCurrentAvatar() {
|
||||
avatarState.headBone = avatarState.debugRig.children[1];
|
||||
avatarState.neckBone = avatarState.debugRig.children[1];
|
||||
avatarState.debugRig.visible = true;
|
||||
avatarState.debugLeftEye = avatarState.debugLeftEye || avatarState.debugRig.children[1]?.children?.[2] || null;
|
||||
avatarState.debugRightEye = avatarState.debugRightEye || avatarState.debugRig.children[1]?.children?.[3] || null;
|
||||
modelName.textContent = "debug-avatar";
|
||||
}
|
||||
|
||||
@@ -831,6 +872,9 @@ function collectMorphBindings(root) {
|
||||
viseme_ee: ["viseme_ee", "ee", "ih", "i", "mouthee"],
|
||||
viseme_oh: ["viseme_oh", "oh", "ou", "o", "mouthoh"],
|
||||
mouthPucker: ["mouthpucker", "pucker", "ou", "kiss"],
|
||||
blink: ["blink", "eyeblink", "eye_blink", "blinkboth"],
|
||||
blinkLeft: ["blinkleft", "blink_left", "eyeblinkleft", "eyelid_l", "eyeblink_l"],
|
||||
blinkRight: ["blinkright", "blink_right", "eyeblinkright", "eyelid_r", "eyeblink_r"],
|
||||
};
|
||||
|
||||
root.traverse((node) => {
|
||||
@@ -910,6 +954,82 @@ function applyProceduralIdlePose(time) {
|
||||
applyBoneRotation(rig.bones.rightHand, handDrift * 0.2, 0, 0.05);
|
||||
}
|
||||
|
||||
function scheduleNextBlink(nowMs, preferSoon = false) {
|
||||
const quickBlink = Math.random() < 0.18;
|
||||
const minDelay = preferSoon ? 240 : 1800;
|
||||
const maxDelay = preferSoon ? 520 : 5200;
|
||||
avatarState.blink.nextAtMs = nowMs + minDelay + Math.random() * (maxDelay - minDelay);
|
||||
avatarState.blink.pendingDoubleBlink = !preferSoon && quickBlink;
|
||||
}
|
||||
|
||||
function updateBlinkState(nowMs) {
|
||||
const blink = avatarState.blink;
|
||||
if (!blink) return;
|
||||
|
||||
if (blink.phase === "idle") {
|
||||
blink.value = 0;
|
||||
if (nowMs >= blink.nextAtMs) {
|
||||
blink.phase = "closing";
|
||||
blink.phaseStartedAtMs = nowMs;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (blink.phase === "closing") {
|
||||
const progress = Math.min(1, (nowMs - blink.phaseStartedAtMs) / blink.closeMs);
|
||||
blink.value = progress;
|
||||
if (progress >= 1) {
|
||||
blink.phase = "hold";
|
||||
blink.phaseStartedAtMs = nowMs;
|
||||
blink.value = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (blink.phase === "hold") {
|
||||
blink.value = 1;
|
||||
if (nowMs - blink.phaseStartedAtMs >= blink.holdMs) {
|
||||
blink.phase = "opening";
|
||||
blink.phaseStartedAtMs = nowMs;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (blink.phase === "opening") {
|
||||
const progress = Math.min(1, (nowMs - blink.phaseStartedAtMs) / blink.openMs);
|
||||
blink.value = 1 - progress;
|
||||
if (progress >= 1) {
|
||||
blink.phase = "idle";
|
||||
blink.phaseStartedAtMs = nowMs;
|
||||
blink.value = 0;
|
||||
scheduleNextBlink(nowMs, blink.pendingDoubleBlink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyBlinkControls(blinkValue) {
|
||||
const clamped = clampControl(blinkValue);
|
||||
|
||||
if (avatarState.currentVrm?.expressionManager) {
|
||||
setExpressionValue("blink", clamped);
|
||||
setExpressionValue("blinkLeft", clamped);
|
||||
setExpressionValue("blinkRight", clamped);
|
||||
}
|
||||
|
||||
setMorphValue("blink", clamped);
|
||||
setMorphValue("blinkLeft", clamped);
|
||||
setMorphValue("blinkRight", clamped);
|
||||
|
||||
const eyeScaleY = Math.max(0.08, 1 - clamped * 0.92);
|
||||
const eyeScaleZ = 1 + clamped * 0.35;
|
||||
if (avatarState.debugLeftEye) {
|
||||
avatarState.debugLeftEye.scale.set(1, eyeScaleY, eyeScaleZ);
|
||||
}
|
||||
if (avatarState.debugRightEye) {
|
||||
avatarState.debugRightEye.scale.set(1, eyeScaleY, eyeScaleZ);
|
||||
}
|
||||
}
|
||||
|
||||
function applyBoneRotation(bone, x = 0, y = 0, z = 0) {
|
||||
if (!bone) return;
|
||||
const baseQuaternion = avatarState.idleRig?.base?.get(bone);
|
||||
@@ -956,6 +1076,9 @@ function subscribeSubtitles() {
|
||||
subtitleWs.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (!shouldRenderSubtitleEvent(data)) {
|
||||
return;
|
||||
}
|
||||
if (data.role === "user") {
|
||||
aiStreamingEl = null;
|
||||
addMessage(data.role, data.text || "");
|
||||
@@ -973,7 +1096,14 @@ function subscribeSubtitles() {
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
subtitleWs.onclose = () => {
|
||||
subtitleWs.onclose = (event) => {
|
||||
if (subtitleWs && subtitleWs.readyState === WebSocket.CLOSED) {
|
||||
subtitleWs = null;
|
||||
}
|
||||
if (event.code === 1012) {
|
||||
addMessage("system", "字幕通道已被新客户端接管。当前页面停止接收字幕。");
|
||||
return;
|
||||
}
|
||||
setTimeout(subscribeSubtitles, 1000);
|
||||
};
|
||||
}
|
||||
@@ -1001,8 +1131,15 @@ function subscribeAnimation() {
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
animationWs.onclose = () => {
|
||||
animationWs.onclose = (event) => {
|
||||
if (animationWs && animationWs.readyState === WebSocket.CLOSED) {
|
||||
animationWs = null;
|
||||
}
|
||||
animationConn.textContent = "已断开";
|
||||
if (event.code === 1012) {
|
||||
animationConn.textContent = "已被新客户端接管";
|
||||
return;
|
||||
}
|
||||
setTimeout(subscribeAnimation, 1000);
|
||||
};
|
||||
}
|
||||
|
||||
+2
-1
@@ -96,6 +96,7 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="/web/app.js?v=20260329k"></script>
|
||||
<script type="module" src="/web/app.js?v=20260329m"></script>
|
||||
<script type="module" src="/web/app.js?v=20260329q"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user