bevyra-avatar 0.1.0__tar.gz

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 (26) hide show
  1. bevyra_avatar-0.1.0/.gitignore +3 -0
  2. bevyra_avatar-0.1.0/PKG-INFO +7 -0
  3. bevyra_avatar-0.1.0/pyproject.toml +19 -0
  4. bevyra_avatar-0.1.0/src/bevyra_avatar/__init__.py +3 -0
  5. bevyra_avatar-0.1.0/src/bevyra_avatar/player.py +139 -0
  6. bevyra_avatar-0.1.0/src/bevyra_avatar/static/player.html +20 -0
  7. bevyra_avatar-0.1.0/src/bevyra_avatar/static/player.js +61 -0
  8. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/assets/bevyra-core-Bskcl7l_.js +195 -0
  9. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/assets/clientOnlyAvatarWorker-C6wTIDwp.js +2119 -0
  10. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/assets/clientOnlySourceWorker-CNVdGzNy.js +7347 -0
  11. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/assets/ort.wasm.min-BcBtKW3v.js +1400 -0
  12. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/assets/ort.webgpu.min-BqBLsdhU.js +1856 -0
  13. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/assets/rustAvatarWorker-vggdUXxI.js +1702 -0
  14. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/assets/teacherCorrectorWorker-Deu0CCPs.js +1406 -0
  15. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/assets/workerHelpers-BkWezgrb.js +31 -0
  16. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/bevyra-avatar-sdk.js +3367 -0
  17. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/ort/ort-wasm-simd-threaded.jsep.mjs +136 -0
  18. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  19. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/ort/ort-wasm-simd-threaded.mjs +68 -0
  20. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/ort/ort-wasm-simd-threaded.wasm +0 -0
  21. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/runtime/bevyra-core.js +1515 -0
  22. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/runtime/bevyra-core.wasm +0 -0
  23. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/runtime/closure_router.onnx +0 -0
  24. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/runtime/runtime-manifest.json +27 -0
  25. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/dist/sdk-manifest.json +70 -0
  26. bevyra_avatar-0.1.0/src/bevyra_avatar/static/sdk/index.js +108 -0
@@ -0,0 +1,3 @@
1
+ dist/
2
+ src/bevyra_avatar/static/sdk/
3
+ **/__pycache__/
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.5
2
+ Name: bevyra-avatar
3
+ Version: 0.1.0
4
+ Summary: Local browser avatar player controlled from Python
5
+ License: Proprietary
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: aiohttp<4,>=3.10
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.26"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bevyra-avatar"
7
+ version = "0.1.0"
8
+ description = "Local browser avatar player controlled from Python"
9
+ requires-python = ">=3.10"
10
+ dependencies = ["aiohttp>=3.10,<4"]
11
+ license = { text = "Proprietary" }
12
+
13
+ [tool.hatch.build.targets.wheel]
14
+ packages = ["src/bevyra_avatar"]
15
+ ignore-vcs = true
16
+
17
+ [tool.hatch.build.targets.sdist]
18
+ include = ["src/bevyra_avatar/**", "pyproject.toml"]
19
+ ignore-vcs = true
@@ -0,0 +1,3 @@
1
+ from .player import AvatarWindow
2
+
3
+ __all__ = ["AvatarWindow"]
@@ -0,0 +1,139 @@
1
+ """A loopback control bridge for the bundled browser avatar renderer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import secrets
7
+ import struct
8
+ import webbrowser
9
+ from pathlib import Path
10
+ from typing import Callable
11
+
12
+ from aiohttp import WSMsgType, web
13
+
14
+ _STATIC = Path(__file__).resolve().parent / "static"
15
+
16
+
17
+ class AvatarWindow:
18
+ """Render locally in a browser while Python supplies PCM16 speech audio."""
19
+
20
+ def __init__(
21
+ self,
22
+ avatar_id: str,
23
+ bundle_dir: str | Path,
24
+ *,
25
+ on_event: Callable[[dict], None] | None = None,
26
+ ) -> None:
27
+ self.avatar_id = avatar_id
28
+ self.bundle_dir = Path(bundle_dir).resolve()
29
+ if not (self.bundle_dir / "web-bundle.json").is_file():
30
+ raise ValueError(f"portable_v2 bundle missing: {self.bundle_dir}")
31
+ if not (_STATIC / "sdk" / "index.js").is_file():
32
+ raise RuntimeError("Browser renderer assets are missing from this Python installation")
33
+ self.on_event = on_event
34
+ self._runner: web.AppRunner | None = None
35
+ self._socket: web.WebSocketResponse | None = None
36
+ self._ready = asyncio.Event()
37
+ self._token = secrets.token_urlsafe(32)
38
+ self.url: str | None = None
39
+
40
+ async def start(self, *, open_browser: bool = True, timeout: float = 90) -> str:
41
+ if self._runner is not None:
42
+ raise RuntimeError("AvatarWindow has already started")
43
+ app = web.Application()
44
+ app.router.add_get("/", self._index)
45
+ app.router.add_get("/player.js", self._player_script)
46
+ app.router.add_get("/ws", self._websocket)
47
+ app.router.add_static("/sdk/", _STATIC / "sdk")
48
+ app.router.add_static("/avatar/", self.bundle_dir)
49
+ runner = web.AppRunner(app)
50
+ await runner.setup()
51
+ try:
52
+ site = web.TCPSite(runner, "127.0.0.1", 0)
53
+ await site.start()
54
+ port = site._server.sockets[0].getsockname()[1]
55
+ self._runner = runner
56
+ self.url = f"http://127.0.0.1:{port}/?token={self._token}"
57
+ if open_browser:
58
+ webbrowser.open(self.url)
59
+ await asyncio.wait_for(self._ready.wait(), timeout)
60
+ return self.url
61
+ except BaseException:
62
+ await runner.cleanup()
63
+ self._runner = None
64
+ raise
65
+
66
+ def _headers(self, response: web.StreamResponse) -> web.StreamResponse:
67
+ response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
68
+ response.headers["Cross-Origin-Embedder-Policy"] = "require-corp"
69
+ response.headers["Cache-Control"] = "no-store"
70
+ return response
71
+
72
+ async def _index(self, request: web.Request) -> web.StreamResponse:
73
+ response = web.FileResponse(_STATIC / "player.html")
74
+ return self._headers(response)
75
+
76
+ async def _player_script(self, request: web.Request) -> web.StreamResponse:
77
+ response = web.FileResponse(_STATIC / "player.js")
78
+ return self._headers(response)
79
+
80
+ async def _websocket(self, request: web.Request) -> web.StreamResponse:
81
+ if request.query.get("token") != self._token:
82
+ raise web.HTTPForbidden()
83
+ expected_origin = f"http://{request.host}"
84
+ if request.headers.get("Origin") != expected_origin:
85
+ raise web.HTTPForbidden()
86
+ if self._socket is not None and not self._socket.closed:
87
+ raise web.HTTPConflict(text="Avatar already connected")
88
+ ws = web.WebSocketResponse(heartbeat=20, max_msg_size=4096)
89
+ await ws.prepare(request)
90
+ self._socket = ws
91
+ await ws.send_json({"type": "configure", "avatar_id": self.avatar_id})
92
+ try:
93
+ async for message in ws:
94
+ if message.type is WSMsgType.TEXT:
95
+ data = message.json()
96
+ if data.get("type") == "ready":
97
+ self._ready.set()
98
+ if self.on_event:
99
+ self.on_event(data)
100
+ finally:
101
+ self._ready.clear()
102
+ if self._socket is ws:
103
+ self._socket = None
104
+ return ws
105
+
106
+ async def _send(self, payload: dict | bytes) -> None:
107
+ ws = self._socket
108
+ if ws is None or ws.closed or not self._ready.is_set():
109
+ raise RuntimeError("Avatar player is not ready")
110
+ if isinstance(payload, bytes):
111
+ await ws.send_bytes(payload)
112
+ else:
113
+ await ws.send_json(payload)
114
+
115
+ async def write_audio(self, pcm16: bytes, sample_rate: int = 16000) -> None:
116
+ if not 8000 <= sample_rate <= 96000 or len(pcm16) % 2:
117
+ raise ValueError("Expected even-length signed PCM16 bytes and a valid sample rate")
118
+ await self._send(struct.pack("<I", sample_rate) + pcm16)
119
+
120
+ async def end_utterance(self) -> None:
121
+ await self._send({"type": "end"})
122
+
123
+ async def interrupt(self) -> None:
124
+ await self._send({"type": "interrupt"})
125
+
126
+ async def close(self) -> None:
127
+ if self._socket is not None:
128
+ await self._socket.close()
129
+ if self._runner is not None:
130
+ await self._runner.cleanup()
131
+ self._runner = None
132
+ self._ready.clear()
133
+
134
+ async def __aenter__(self) -> "AvatarWindow":
135
+ await self.start()
136
+ return self
137
+
138
+ async def __aexit__(self, *_: object) -> None:
139
+ await self.close()
@@ -0,0 +1,20 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>BeVyra Avatar</title>
7
+ <style>
8
+ html,body{margin:0;width:100%;height:100%;background:transparent}
9
+ canvas{display:block;width:100%;height:100%;object-fit:contain}
10
+ #start{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);padding:1em 2em}
11
+ #status{position:absolute;left:1em;bottom:1em;color:white;background:#222a;padding:.4em}
12
+ </style>
13
+ </head>
14
+ <body>
15
+ <canvas id="avatar"></canvas>
16
+ <button id="start" disabled>Start avatar</button>
17
+ <div id="status" role="status">Connecting to Python…</div>
18
+ <script type="module" src="/player.js"></script>
19
+ </body>
20
+ </html>
@@ -0,0 +1,61 @@
1
+ import { BevyraAvatar } from "/sdk/index.js";
2
+
3
+ const status = document.getElementById("status");
4
+ const start = document.getElementById("start");
5
+ const canvas = document.getElementById("avatar");
6
+ const token = new URL(location.href).searchParams.get("token");
7
+ if (!token) throw new Error("Missing session token");
8
+ const socket = new WebSocket(`ws://${location.host}/ws?token=${encodeURIComponent(token)}`);
9
+ socket.binaryType = "arraybuffer";
10
+ let avatar = null;
11
+ let avatarId = null;
12
+ let pending = Promise.resolve();
13
+ let generation = 0;
14
+ const send = (value) => socket.readyState === WebSocket.OPEN && socket.send(JSON.stringify(value));
15
+ const enqueue = (task) => {
16
+ const current = generation;
17
+ pending = pending.catch(() => undefined).then(() => {
18
+ if (current === generation) return task();
19
+ }).catch((error) => send({ type: "error", message: String(error) }));
20
+ };
21
+ socket.onmessage = async (event) => {
22
+ if (typeof event.data === "string") {
23
+ const command = JSON.parse(event.data);
24
+ if (command.type === "configure") {
25
+ avatarId = command.avatar_id;
26
+ start.disabled = false;
27
+ status.textContent = "Click to start avatar";
28
+ } else if (command.type === "end") {
29
+ enqueue(() => avatar.endUtterance());
30
+ } else if (command.type === "interrupt") {
31
+ generation++;
32
+ avatar?.interrupt();
33
+ pending = Promise.resolve();
34
+ }
35
+ return;
36
+ }
37
+ const data = new DataView(event.data);
38
+ const sampleRate = data.getUint32(0, true);
39
+ const pcm = new Int16Array(event.data.slice(4));
40
+ enqueue(() => avatar.writeAudio(pcm, sampleRate));
41
+ };
42
+ start.onclick = async () => {
43
+ start.disabled = true;
44
+ status.textContent = "Loading avatar…";
45
+ try {
46
+ avatar = await BevyraAvatar.mount({
47
+ avatarId, canvas, bundleBaseUrl: "/avatar/", assetsBaseUrl: "/sdk/dist/",
48
+ onStage: (stage) => { status.textContent = stage.replaceAll("_", " "); },
49
+ });
50
+ avatar.onSpeechStart(() => send({ type: "speech_start" }));
51
+ avatar.onSpeechEnd(() => send({ type: "speech_end" }));
52
+ status.remove();
53
+ start.remove();
54
+ send({ type: "ready" });
55
+ } catch (error) {
56
+ status.textContent = String(error);
57
+ start.disabled = false;
58
+ send({ type: "error", message: String(error) });
59
+ }
60
+ };
61
+ socket.onclose = () => { avatar?.close(); status.textContent = "Python connection closed"; };