agentvibes-avatars 0.1.0
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.
- package/LICENSE +34 -0
- package/README.md +77 -0
- package/avatars.json +35 -0
- package/bin/cli.js +109 -0
- package/package.json +40 -0
- package/public/cosmic.html +539 -0
- package/public/gallery.html +77 -0
- package/public/index.html +484 -0
- package/server.js +286 -0
|
@@ -0,0 +1,539 @@
|
|
|
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.0">
|
|
6
|
+
<title>AgentVibes HTML Receiver</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
html, body { width: 100%; height: 100%; overflow: hidden; background: #05030f; color: #e9e9ff;
|
|
10
|
+
font-family: -apple-system, 'Segoe UI', sans-serif; }
|
|
11
|
+
|
|
12
|
+
#galaxy { position: fixed; inset: 0; width: 100vw; height: 100vh; z-index: 0; display: block; }
|
|
13
|
+
#nebula { position: fixed; inset: 0; z-index: 1; pointer-events: none;
|
|
14
|
+
background:
|
|
15
|
+
radial-gradient(60% 50% at 30% 30%, rgba(80,40,160,0.22), transparent 70%),
|
|
16
|
+
radial-gradient(50% 50% at 75% 65%, rgba(20,120,160,0.20), transparent 70%),
|
|
17
|
+
radial-gradient(120% 100% at 50% 120%, rgba(0,0,0,0.6), transparent 60%); }
|
|
18
|
+
|
|
19
|
+
/* Brand — top right */
|
|
20
|
+
#brand { position: fixed; top: 12px; right: 16px; z-index: 40; display: flex; align-items: center; gap: 8px;
|
|
21
|
+
background: rgba(10,8,24,0.7); border: 1px solid rgba(255,255,255,0.14); padding: 7px 14px; border-radius: 999px;
|
|
22
|
+
font-size: 13px; font-weight: 700; letter-spacing: 0.3px; backdrop-filter: blur(8px); }
|
|
23
|
+
#brand .logo { background: linear-gradient(90deg,#a78bfa,#22d3ee); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
|
24
|
+
#brand .dot { width: 8px; height: 8px; border-radius: 50%; background: #f87171; box-shadow: 0 0 8px #f87171; }
|
|
25
|
+
#brand .dot.on { background: #4ade80; box-shadow: 0 0 8px #4ade80; }
|
|
26
|
+
|
|
27
|
+
/* App layout: sidebar + stage */
|
|
28
|
+
#app { position: fixed; inset: 0; z-index: 10; display: flex; }
|
|
29
|
+
#sidebar { width: 300px; flex: 0 0 300px; height: 100vh; display: flex; flex-direction: column;
|
|
30
|
+
background: linear-gradient(180deg, rgba(10,8,24,0.86), rgba(8,6,18,0.78)); backdrop-filter: blur(10px);
|
|
31
|
+
border-right: 1px solid rgba(255,255,255,0.08); }
|
|
32
|
+
#stage { position: relative; flex: 1; height: 100vh; }
|
|
33
|
+
|
|
34
|
+
/* Sidebar header */
|
|
35
|
+
#side-head { padding: 14px 14px 8px; font-size: 12px; letter-spacing: 1px; text-transform: uppercase; color: #9aa3d8; }
|
|
36
|
+
|
|
37
|
+
/* Session tabs */
|
|
38
|
+
#tabs { display: flex; flex-direction: column; gap: 4px; padding: 0 8px 8px; max-height: 28vh; overflow-y: auto; }
|
|
39
|
+
.tab { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 10px; cursor: pointer;
|
|
40
|
+
border: 1px solid transparent; font-size: 13px; color: #d7dbff; }
|
|
41
|
+
.tab:hover { background: rgba(255,255,255,0.05); }
|
|
42
|
+
.tab.active { background: rgba(124,92,255,0.16); border-color: rgba(124,92,255,0.5); }
|
|
43
|
+
.tab .tname { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
44
|
+
.tab .badge { min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px; background: #fb7185; color: #fff;
|
|
45
|
+
font-size: 11px; font-weight: 700; display: none; align-items: center; justify-content: center; }
|
|
46
|
+
.tab.has-unread .badge { display: inline-flex; }
|
|
47
|
+
.tab .kind { font-size: 10px; opacity: 0.55; }
|
|
48
|
+
|
|
49
|
+
/* Sidebar actions */
|
|
50
|
+
#actions { display: flex; gap: 8px; padding: 8px 12px; border-top: 1px solid rgba(255,255,255,0.07); border-bottom: 1px solid rgba(255,255,255,0.07); }
|
|
51
|
+
.btn { flex: 1; text-align: center; padding: 8px; border-radius: 9px; cursor: pointer; font-size: 12px; font-weight: 600;
|
|
52
|
+
border: 1px solid rgba(255,255,255,0.14); background: rgba(255,255,255,0.05); color: #e9e9ff; }
|
|
53
|
+
.btn:hover { background: rgba(255,255,255,0.12); }
|
|
54
|
+
.btn.danger { border-color: rgba(248,113,113,0.5); color: #fca5a5; }
|
|
55
|
+
|
|
56
|
+
/* Chat (clips for active session) */
|
|
57
|
+
#chat-head { padding: 10px 14px 6px; font-size: 11px; letter-spacing: 1px; text-transform: uppercase; color: #9aa3d8; }
|
|
58
|
+
#chat { flex: 1; overflow-y: auto; padding: 0 10px 14px; display: flex; flex-direction: column; gap: 7px; }
|
|
59
|
+
.msg { background: rgba(16,16,34,0.7); border: 1px solid rgba(255,255,255,0.08); border-left-width: 3px;
|
|
60
|
+
border-radius: 10px; padding: 8px 10px; font-size: 12px; cursor: pointer; }
|
|
61
|
+
.msg:hover { background: rgba(40,40,75,0.8); }
|
|
62
|
+
.msg .who { font-size: 11px; font-weight: 700; margin-bottom: 2px; }
|
|
63
|
+
.msg .ts { float: right; font-weight: 400; opacity: 0.45; font-size: 10px; }
|
|
64
|
+
.msg .proj { font-size: 10px; opacity: 0.55; }
|
|
65
|
+
.msg.playing { box-shadow: 0 0 0 1px #4ade80 inset; }
|
|
66
|
+
|
|
67
|
+
/* Metallic translucent back-wall behind the avatars (so they read as solid) */
|
|
68
|
+
#wall { position: absolute; left: 0; right: 0; top: 0; bottom: 0; z-index: 1; pointer-events: none; overflow: hidden;
|
|
69
|
+
background:
|
|
70
|
+
linear-gradient(180deg, rgba(48,54,68,0.18) 0%, rgba(60,68,84,0.34) 35%, rgba(38,43,54,0.5) 100%);
|
|
71
|
+
box-shadow: inset 0 3px 60px rgba(255,255,255,0.05); }
|
|
72
|
+
/* Brushed-metal sheen streaks */
|
|
73
|
+
#wall::after { content:''; position:absolute; inset:0;
|
|
74
|
+
background: repeating-linear-gradient(95deg, rgba(255,255,255,0.018) 0 2px, transparent 2px 7px); mix-blend-mode: screen; }
|
|
75
|
+
/* Side rails with bolts */
|
|
76
|
+
.rail { position: absolute; top: 0; bottom: 0; width: 40px; z-index: 2;
|
|
77
|
+
background: linear-gradient(90deg, #232833, #586377 45%, #2c323e); border-left: 1px solid rgba(0,0,0,0.5); border-right: 1px solid rgba(0,0,0,0.5);
|
|
78
|
+
box-shadow: 0 0 30px rgba(0,0,0,0.5); }
|
|
79
|
+
.rail.left { left: 0; } .rail.right { right: 0; }
|
|
80
|
+
.rail::before { content:''; position:absolute; inset:0;
|
|
81
|
+
background-image: radial-gradient(circle at 50% 9px, #e8eefc 0 2px, #aab4c8 2px 3px, #20242e 4px 5.5px, transparent 6px);
|
|
82
|
+
background-size: 100% 46px; background-repeat: repeat-y; }
|
|
83
|
+
|
|
84
|
+
/* Big project name + source label at the top of the avatar area */
|
|
85
|
+
#stage-header { position: absolute; top: 16px; left: 0; right: 0; z-index: 8; text-align: center; pointer-events: none; }
|
|
86
|
+
#stage-header .proj-big { font-size: 34px; font-weight: 800; letter-spacing: 1px;
|
|
87
|
+
background: linear-gradient(90deg,#a78bfa,#22d3ee,#fb7185); -webkit-background-clip: text; background-clip: text; color: transparent;
|
|
88
|
+
text-shadow: 0 2px 24px rgba(0,0,0,0.5); }
|
|
89
|
+
#stage-header .origin-badge { display: none; margin-top: 6px; font-size: 12px; font-weight: 700; text-transform: uppercase;
|
|
90
|
+
letter-spacing: 1.5px; padding: 4px 14px; border-radius: 999px; background: rgba(14,14,30,0.78);
|
|
91
|
+
border: 1px solid rgba(255,255,255,0.2); color: #cdd3ff; backdrop-filter: blur(6px); }
|
|
92
|
+
#stage-header .origin-badge.show { display: inline-block; }
|
|
93
|
+
#stage-header .origin-badge.local { border-color:#34d399; color:#9af7c8; }
|
|
94
|
+
#stage-header .origin-badge.remote { border-color:#fbbf24; color:#ffe3a0; }
|
|
95
|
+
|
|
96
|
+
/* Cast layer inside the stage */
|
|
97
|
+
.cast { position: absolute; inset: 0; z-index: 3; display: flex; align-items: flex-end; justify-content: center; gap: 0; pointer-events: none; }
|
|
98
|
+
.cast.hidden { display: none; }
|
|
99
|
+
.slot { position: relative; width: 420px; height: 94vh; display: flex; align-items: flex-end; justify-content: center;
|
|
100
|
+
transition: filter .5s, transform .5s cubic-bezier(0.34,1.4,0.64,1);
|
|
101
|
+
filter: brightness(0.86) saturate(0.95); opacity: 1; }
|
|
102
|
+
.slot.speaking { filter: brightness(1.12) saturate(1.12); transform: translateY(-10px) scale(1.03); z-index: 5; }
|
|
103
|
+
.slot .canvas-host { position: absolute; inset: 0; }
|
|
104
|
+
.slot .glow { position: absolute; left: 50%; bottom: 10%; transform: translateX(-50%); width: 300px; height: 300px;
|
|
105
|
+
border-radius: 50%; background: radial-gradient(circle, var(--accent,#8a5cff) 0%, transparent 65%); opacity: 0;
|
|
106
|
+
transition: opacity .5s; filter: blur(14px); z-index: -1; }
|
|
107
|
+
.slot.speaking .glow { opacity: 0.55; }
|
|
108
|
+
.slot .plate { position: absolute; left: 50%; bottom: 4%; transform: translateX(-50%); display: flex; flex-direction: column;
|
|
109
|
+
align-items: center; gap: 1px; background: rgba(8,8,22,0.84); backdrop-filter: blur(8px);
|
|
110
|
+
border: 1px solid var(--accent,#8a5cff); color: #eef; padding: 5px 14px; border-radius: 14px; white-space: nowrap;
|
|
111
|
+
box-shadow: 0 0 18px -6px var(--accent,#8a5cff); }
|
|
112
|
+
.slot .plate .nm { font-size: 13px; font-weight: 700; }
|
|
113
|
+
.slot .plate .pj { font-size: 10px; opacity: 0.6; }
|
|
114
|
+
|
|
115
|
+
/* Caption — bottom of the stage area (never over the sidebar) */
|
|
116
|
+
#caption { position: absolute; left: 50%; transform: translateX(-50%); bottom: 16px; max-width: 80%; text-align: center;
|
|
117
|
+
font-size: 17px; line-height: 1.4; color: #fff; background: rgba(8,8,22,0.62); backdrop-filter: blur(8px);
|
|
118
|
+
padding: 11px 18px; border-radius: 14px; display: none; border: 1px solid rgba(255,255,255,0.12); z-index: 6; }
|
|
119
|
+
#caption.visible { display: block; }
|
|
120
|
+
#caption .spk { font-size: 11px; opacity: 0.65; display: block; margin-bottom: 3px; }
|
|
121
|
+
|
|
122
|
+
#empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; flex-direction: column;
|
|
123
|
+
gap: 10px; color: rgba(220,225,255,0.5); font-size: 15px; text-align: center; z-index: 4; }
|
|
124
|
+
#empty .big { font-size: 40px; opacity: 0.5; }
|
|
125
|
+
|
|
126
|
+
#enable-audio { position: fixed; top: 50%; left: calc(50% + 150px); transform: translate(-50%,-50%); display: none;
|
|
127
|
+
background: rgba(20,120,60,0.92); color:#fff; padding: 14px 22px; border-radius: 999px; font-size: 15px;
|
|
128
|
+
font-weight: 600; cursor: pointer; z-index: 50; box-shadow: 0 4px 20px rgba(0,0,0,0.5); }
|
|
129
|
+
#debug { position: fixed; right: 8px; bottom: 8px; width: 320px; max-height: 32vh; overflow-y: auto;
|
|
130
|
+
background: rgba(0,0,0,0.7); color: #9effa0; font: 10px/1.45 ui-monospace, Consolas, monospace; padding: 8px 10px;
|
|
131
|
+
border-radius: 8px; white-space: pre-wrap; z-index: 45; display: none; border: 1px solid rgba(255,255,255,0.1); }
|
|
132
|
+
#debug.visible { display: block; }
|
|
133
|
+
#error-msg { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); background: rgba(180,40,40,0.9);
|
|
134
|
+
color:#fff; padding: 16px 24px; border-radius: 10px; font-size: 13px; max-width: 420px; text-align: center; display: none; z-index: 60; }
|
|
135
|
+
</style>
|
|
136
|
+
</head>
|
|
137
|
+
<body>
|
|
138
|
+
<canvas id="galaxy"></canvas>
|
|
139
|
+
<div id="nebula"></div>
|
|
140
|
+
|
|
141
|
+
<div id="brand"><span class="dot" id="connDot"></span><span class="logo">✦ AgentVibes HTML Receiver</span></div>
|
|
142
|
+
|
|
143
|
+
<div id="app">
|
|
144
|
+
<div id="sidebar">
|
|
145
|
+
<div id="side-head">Sessions</div>
|
|
146
|
+
<div id="tabs"></div>
|
|
147
|
+
<div id="actions">
|
|
148
|
+
<div class="btn danger" id="clearBtn" title="Clear avatars + chat for the active session">⨯ Clear</div>
|
|
149
|
+
<div class="btn" id="replayBtn" title="Replay the demo scene" style="display:none">▶ Replay scene</div>
|
|
150
|
+
</div>
|
|
151
|
+
<div id="chat-head">Chat</div>
|
|
152
|
+
<div id="chat"></div>
|
|
153
|
+
</div>
|
|
154
|
+
<div id="stage">
|
|
155
|
+
<div id="wall"><div class="rail left"></div><div class="rail right"></div></div>
|
|
156
|
+
<div id="stage-header"><div class="proj-big" id="projBig">—</div><div class="origin-badge" id="originBadge"></div></div>
|
|
157
|
+
<div id="empty"><div class="big">🌌</div><div>Waiting for AgentVibes messages…<br>avatars appear when someone speaks.</div></div>
|
|
158
|
+
<!-- per-session cast containers are injected here -->
|
|
159
|
+
<div id="caption"></div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div id="enable-audio">🔊 Click to enable audio</div>
|
|
164
|
+
<div id="debug"></div>
|
|
165
|
+
<div id="error-msg"></div>
|
|
166
|
+
|
|
167
|
+
<script type="importmap">
|
|
168
|
+
{ "imports": {
|
|
169
|
+
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js/+esm",
|
|
170
|
+
"three/examples/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/",
|
|
171
|
+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
|
|
172
|
+
} }
|
|
173
|
+
</script>
|
|
174
|
+
|
|
175
|
+
<script type="module">
|
|
176
|
+
import * as THREE from 'three';
|
|
177
|
+
import { TalkingHead } from 'https://cdn.jsdelivr.net/gh/met4citizen/TalkingHead@1.3/modules/talkinghead.mjs';
|
|
178
|
+
|
|
179
|
+
// ---------- config ----------
|
|
180
|
+
const AVATAR_BASE = 'https://cdn.jsdelivr.net/gh/met4citizen/TalkingHead@1.3/avatars/';
|
|
181
|
+
// Smaller, fast-loading avatars first; the big ones (avaturn/avatarsdk ~12-13MB) last.
|
|
182
|
+
const AVATARS = [
|
|
183
|
+
{ id:'brunette', url:AVATAR_BASE+'brunette.glb', body:'F' },
|
|
184
|
+
{ id:'brunette-t',url:AVATAR_BASE+'brunette-t.glb',body:'F' },
|
|
185
|
+
{ id:'vroid', url:AVATAR_BASE+'vroid.glb', body:'F' },
|
|
186
|
+
{ id:'avatarsdk', url:AVATAR_BASE+'avatarsdk.glb', body:'M' },
|
|
187
|
+
{ id:'avaturn', url:AVATAR_BASE+'avaturn.glb', body:'M' },
|
|
188
|
+
];
|
|
189
|
+
const DEFAULT_AVATAR = 'brunette';
|
|
190
|
+
const ACCENTS = ['#a78bfa','#22d3ee','#fb7185','#34d399','#fbbf24','#60a5fa'];
|
|
191
|
+
const params = new URLSearchParams(location.search);
|
|
192
|
+
const MODE = params.get('mode') || 'meeting';
|
|
193
|
+
// Single reliable avatar for the everyday receiver; up to 4 only for casts.
|
|
194
|
+
const MAX_SLOTS = (MODE==='party' || MODE==='opencast' || params.get('demo')==='party') ? 4 : 1;
|
|
195
|
+
if (params.get('debug')==='1') document.getElementById('debug').classList.add('visible');
|
|
196
|
+
|
|
197
|
+
// ---------- helpers ----------
|
|
198
|
+
const $ = id => document.getElementById(id);
|
|
199
|
+
const stageEl=$('stage'), caption=$('caption'), chatEl=$('chat'), tabsEl=$('tabs'),
|
|
200
|
+
emptyEl=$('empty'), connDot=$('connDot'), debugBox=$('debug');
|
|
201
|
+
const esc = s => String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
202
|
+
function dbg(m){ const t=new Date().toLocaleTimeString(); debugBox.textContent=`${t} ${m}\n`+debugBox.textContent; if(debugBox.textContent.length>4000)debugBox.textContent=debugBox.textContent.slice(0,4000);
|
|
203
|
+
try{ fetch('/clientlog',{method:'POST',body:m}); }catch{} }
|
|
204
|
+
function showError(m){ const e=$('error-msg'); e.textContent=m; e.style.display='block'; }
|
|
205
|
+
function hashStr(s){ let h=0; for(let i=0;i<s.length;i++) h=(h*31+s.charCodeAt(i))|0; return h; }
|
|
206
|
+
|
|
207
|
+
// Big project name + source label at the top of the avatar area.
|
|
208
|
+
const projBig = $('projBig'), originBadge = $('originBadge');
|
|
209
|
+
function updateHeader(project, origin){
|
|
210
|
+
projBig.textContent = project || '—';
|
|
211
|
+
const o = (origin||'').toLowerCase();
|
|
212
|
+
if (!o){ originBadge.className='origin-badge'; originBadge.textContent=''; return; }
|
|
213
|
+
let label, cls;
|
|
214
|
+
if (o==='local'){ label='💻 LOCAL'; cls='local'; }
|
|
215
|
+
else if (o==='remote'){ label='🖥️ REMOTE'; cls='remote'; }
|
|
216
|
+
else { label='🖥️ '+origin.toUpperCase(); cls='remote'; } // e.g. a remote server name
|
|
217
|
+
originBadge.textContent=label;
|
|
218
|
+
originBadge.className='origin-badge show '+cls;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Cap retained clips per session so a long-lived window doesn't grow without
|
|
222
|
+
// bound; free the cached audio bytes of evicted clips.
|
|
223
|
+
const MAX_CLIPS = 60;
|
|
224
|
+
function pushClip(s, clip){
|
|
225
|
+
s.clips.push(clip);
|
|
226
|
+
while (s.clips.length > MAX_CLIPS){ const old = s.clips.shift(); if (old) old._bytes = null; }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------- galaxy ----------
|
|
230
|
+
const galaxy = (() => {
|
|
231
|
+
const canvas=$('galaxy');
|
|
232
|
+
const renderer=new THREE.WebGLRenderer({canvas,antialias:true,alpha:true});
|
|
233
|
+
renderer.setPixelRatio(Math.min(devicePixelRatio,2)); renderer.setSize(innerWidth,innerHeight);
|
|
234
|
+
const scene=new THREE.Scene();
|
|
235
|
+
const camera=new THREE.PerspectiveCamera(60,innerWidth/innerHeight,0.1,1000); camera.position.set(0,1.4,6);
|
|
236
|
+
let palette={inside:new THREE.Color('#ffd9a0'),outside:new THREE.Color('#5b8cff')};
|
|
237
|
+
let points=null;
|
|
238
|
+
function build(seed=1,pal){ if(pal)palette=pal; if(points){scene.remove(points);points.geometry.dispose();points.material.dispose();}
|
|
239
|
+
const COUNT=9000,ARMS=3,RADIUS=7,SPIN=1.1,RAND=0.45,RP=2.6; const pos=new Float32Array(COUNT*3),col=new Float32Array(COUNT*3);
|
|
240
|
+
let s=seed*99991; const rnd=()=>{s=(s*1103515245+12345)&0x7fffffff;return s/0x7fffffff;};
|
|
241
|
+
for(let i=0;i<COUNT;i++){ const r=Math.pow(rnd(),1.6)*RADIUS; const arm=(i%ARMS)/ARMS*Math.PI*2; const spin=r*SPIN;
|
|
242
|
+
const rx=Math.pow(rnd(),RP)*(rnd()<0.5?1:-1)*RAND*r, ry=Math.pow(rnd(),RP)*(rnd()<0.5?1:-1)*RAND*r*0.35, rz=Math.pow(rnd(),RP)*(rnd()<0.5?1:-1)*RAND*r;
|
|
243
|
+
const a=arm+spin; pos[i*3]=Math.cos(a)*r+rx; pos[i*3+1]=ry; pos[i*3+2]=Math.sin(a)*r+rz;
|
|
244
|
+
const c=palette.inside.clone().lerp(palette.outside,Math.min(1,r/RADIUS)); col[i*3]=c.r;col[i*3+1]=c.g;col[i*3+2]=c.b; }
|
|
245
|
+
const geo=new THREE.BufferGeometry(); geo.setAttribute('position',new THREE.BufferAttribute(pos,3)); geo.setAttribute('color',new THREE.BufferAttribute(col,3));
|
|
246
|
+
points=new THREE.Points(geo,new THREE.PointsMaterial({size:0.045,sizeAttenuation:true,depthWrite:false,blending:THREE.AdditiveBlending,vertexColors:true,transparent:true,opacity:0.95}));
|
|
247
|
+
points.rotation.x=0.5; scene.add(points); }
|
|
248
|
+
const STARS=1800,sgeo=new THREE.BufferGeometry(),spos=new Float32Array(STARS*3);
|
|
249
|
+
for(let i=0;i<STARS;i++){spos[i*3]=(Math.random()-0.5)*60;spos[i*3+1]=(Math.random()-0.5)*60;spos[i*3+2]=(Math.random()-0.5)*60;}
|
|
250
|
+
sgeo.setAttribute('position',new THREE.BufferAttribute(spos,3));
|
|
251
|
+
const stars=new THREE.Points(sgeo,new THREE.PointsMaterial({size:0.06,color:0xffffff,transparent:true,opacity:0.7,depthWrite:false})); scene.add(stars);
|
|
252
|
+
build(1); let t=0;
|
|
253
|
+
(function animate(){ requestAnimationFrame(animate); t+=0.0016; if(points)points.rotation.y=t; stars.rotation.y=t*0.15;
|
|
254
|
+
camera.position.x=Math.sin(t*0.3)*1.2; camera.position.y=1.3+Math.sin(t*0.21)*0.4; camera.lookAt(0,0,0); renderer.render(scene,camera); })();
|
|
255
|
+
addEventListener('resize',()=>{renderer.setSize(innerWidth,innerHeight);camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();});
|
|
256
|
+
return { rebuild: build };
|
|
257
|
+
})();
|
|
258
|
+
const THEMES=[{n:'aurora',i:'#b9ffd0',o:'#3a7bff'},{n:'ember',i:'#ffd9a0',o:'#ff4d6d'},{n:'orchid',i:'#f0c2ff',o:'#7c3aed'},
|
|
259
|
+
{n:'teal',i:'#aef9ff',o:'#0ea5e9'},{n:'sunset',i:'#ffe29a',o:'#ff6a3d'},{n:'mint',i:'#d6ffe9',o:'#10b981'}];
|
|
260
|
+
function applyTheme(seed){ const th=THEMES[Math.abs(seed)%THEMES.length]; galaxy.rebuild((Math.abs(seed)%7)+1,{inside:new THREE.Color(th.i),outside:new THREE.Color(th.o)}); dbg('theme '+th.n); }
|
|
261
|
+
|
|
262
|
+
// ---------- sessions ----------
|
|
263
|
+
// A session = one project/cast. Only the ACTIVE session has live avatars.
|
|
264
|
+
const sessions = new Map(); // key -> { key,label,kind,castEl,slots[],cast{}, clips[], unread, tabEl, built }
|
|
265
|
+
let activeKey = null;
|
|
266
|
+
const persistMap = JSON.parse(localStorage.getItem('avAvatarMap')||'{}'); // voice -> avatarId (remembered)
|
|
267
|
+
const savePersist = () => localStorage.setItem('avAvatarMap', JSON.stringify(persistMap));
|
|
268
|
+
|
|
269
|
+
function sessionKey(msg){ return ((msg.project||'').trim()) || ((msg.origin||'').trim()) || 'default'; }
|
|
270
|
+
function sessionKind(s){ return Object.keys(s.cast).length>1 ? 'cast' : 'solo'; }
|
|
271
|
+
|
|
272
|
+
function getSession(msg){
|
|
273
|
+
const key = sessionKey(msg);
|
|
274
|
+
if (sessions.has(key)) return sessions.get(key);
|
|
275
|
+
const castEl=document.createElement('div'); castEl.className='cast hidden'; stageEl.insertBefore(castEl, caption);
|
|
276
|
+
const s={ key, label:key, kind:'solo', castEl, slots:[], cast:{}, clips:[], unread:0, tabEl:null, built:false, lastOrigin:'' };
|
|
277
|
+
sessions.set(key,s); addTab(s);
|
|
278
|
+
if(!activeKey) switchTo(key);
|
|
279
|
+
return s;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function addTab(s){
|
|
283
|
+
const el=document.createElement('div'); el.className='tab'; el.dataset.key=s.key;
|
|
284
|
+
el.innerHTML=`<span class="tname">${esc(s.label)}</span><span class="kind"></span><span class="badge">0</span>`;
|
|
285
|
+
el.addEventListener('click',()=>switchTo(s.key));
|
|
286
|
+
tabsEl.appendChild(el); s.tabEl=el; updateTab(s);
|
|
287
|
+
}
|
|
288
|
+
function updateTab(s){
|
|
289
|
+
if(!s.tabEl) return;
|
|
290
|
+
s.tabEl.querySelector('.kind').textContent = sessionKind(s)==='cast' ? `cast · ${Object.keys(s.cast).length}` : 'solo';
|
|
291
|
+
s.tabEl.querySelector('.badge').textContent = s.unread;
|
|
292
|
+
s.tabEl.classList.toggle('has-unread', s.unread>0 && s.key!==activeKey);
|
|
293
|
+
s.tabEl.classList.toggle('active', s.key===activeKey);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function switchTo(key){
|
|
297
|
+
if(activeKey && sessions.has(activeKey)) sessions.get(activeKey).castEl.classList.add('hidden');
|
|
298
|
+
activeKey=key; const s=sessions.get(key); s.unread=0;
|
|
299
|
+
s.castEl.classList.remove('hidden');
|
|
300
|
+
emptyEl.style.display = (Object.keys(s.cast).length||s.clips.length) ? 'none' : 'flex';
|
|
301
|
+
updateHeader(s.label, s.lastOrigin);
|
|
302
|
+
renderChat(s);
|
|
303
|
+
sessions.forEach(updateTab);
|
|
304
|
+
// Lazily build avatars for this session's known cast
|
|
305
|
+
if(!s.built){ for(const vk of Object.keys(s.cast)) await ensureSlot(s, vk); s.built=true; }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function renderChat(s){
|
|
309
|
+
dbg('renderChat key='+s.key+' clips='+s.clips.length);
|
|
310
|
+
chatEl.innerHTML='';
|
|
311
|
+
s.clips.slice(-40).forEach(clip=>{
|
|
312
|
+
const m=document.createElement('div'); m.className='msg'; m.style.borderLeftColor=clip.accent;
|
|
313
|
+
m.innerHTML=`<div class="who" style="color:${clip.accent}">${esc(clip.name)}<span class="ts">${esc(clip.time||'')}</span></div>`+
|
|
314
|
+
`<div>${esc((clip.text||'(no text)').slice(0,160))}</div>`+
|
|
315
|
+
`<div class="proj">${esc(clip.project||clip.origin||'')}</div>`;
|
|
316
|
+
m.addEventListener('click',()=>{ const slot=s.slots.find(x=>x.voiceKey===clip.voiceKey)||s.slots[0]; if(slot) playClip(clip,slot,m); else dbg('replay: no avatar slot'); });
|
|
317
|
+
chatEl.appendChild(m);
|
|
318
|
+
});
|
|
319
|
+
chatEl.scrollTop=chatEl.scrollHeight;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ---------- avatar slots (per session) ----------
|
|
323
|
+
function pickAvatar(s, voiceKey, idx){
|
|
324
|
+
if(persistMap[voiceKey]) return persistMap[voiceKey];
|
|
325
|
+
const used=new Set(Object.values(s.cast).map(c=>c.avatarId));
|
|
326
|
+
const pool=AVATARS.filter(a=>!used.has(a.id));
|
|
327
|
+
const seed=hashStr(voiceKey)+idx;
|
|
328
|
+
const av=(pool.length?pool:AVATARS)[Math.abs(seed)%(pool.length||AVATARS.length)].id;
|
|
329
|
+
persistMap[voiceKey]=av; savePersist(); return av;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function relayout(s){ const n=Math.max(1,s.slots.length); const w=Math.min(440,Math.max(220,(stageEl.clientWidth-40)/n)); s.slots.forEach(sl=>sl.el.style.width=w+'px'); }
|
|
333
|
+
|
|
334
|
+
async function ensureSlot(s, voiceKey){
|
|
335
|
+
let slot=s.slots.find(x=>x.voiceKey===voiceKey);
|
|
336
|
+
if(slot) return slot;
|
|
337
|
+
if(s.slots.length>=MAX_SLOTS){
|
|
338
|
+
slot=s.slots.reduce((a,b)=>(a._last||0)<=(b._last||0)?a:b); slot.voiceKey=voiceKey;
|
|
339
|
+
const m=s.cast[voiceKey];
|
|
340
|
+
if(m){ slot.nm.textContent=m.name; slot.pj.textContent=m.project||s.label||''; slot.accent=m.accent; slot.el.style.setProperty('--accent',m.accent); }
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
const meta=s.cast[voiceKey]; const accent=meta.accent;
|
|
344
|
+
const el=document.createElement('div'); el.className='slot'; el.style.setProperty('--accent',accent);
|
|
345
|
+
el.innerHTML=`<div class="glow"></div><div class="canvas-host"></div><div class="plate"><span class="nm"></span><span class="pj"></span></div>`;
|
|
346
|
+
s.castEl.appendChild(el);
|
|
347
|
+
slot={ el, host:el.querySelector('.canvas-host'), nm:el.querySelector('.nm'), pj:el.querySelector('.pj'),
|
|
348
|
+
head:null, voiceKey, accent, ready:false, key:meta.avatarId };
|
|
349
|
+
s.slots.push(slot); relayout(s);
|
|
350
|
+
slot.nm.textContent=meta.name; slot.pj.textContent=meta.project||s.label||'';
|
|
351
|
+
const av=AVATARS.find(a=>a.id===meta.avatarId)||AVATARS[0];
|
|
352
|
+
slot.head=new TalkingHead(slot.host,{ ttsEndpoint:'https://localhost/_unused_tts', cameraView:'upper',
|
|
353
|
+
cameraRotateEnable:false, avatarMood:'neutral', avatarIdleEyeContact:0.4, avatarIdleHeadMove:0.5, mixerGainSpeech:3.0 });
|
|
354
|
+
try{ if(slot.head.audioSpeechGainNode) slot.head.audioSpeechGainNode.gain.value=3.0; }catch{}
|
|
355
|
+
try{ await slot.head.showAvatar({url:av.url,body:av.body,avatarMood:'neutral'}); slot.ready=true; dbg('avatar '+av.id); }
|
|
356
|
+
catch(e){ dbg('avatar FAIL '+av.id+': '+e.message); }
|
|
357
|
+
}
|
|
358
|
+
emptyEl.style.display='none';
|
|
359
|
+
return slot;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function setSpeaking(s, slot, on){ s.slots.forEach(x=>x.el.classList.toggle('speaking', x===slot&&on)); if(on)slot._last=Date.now(); }
|
|
363
|
+
|
|
364
|
+
// ---------- audio ----------
|
|
365
|
+
async function playClip(clip, slot, msgEl){
|
|
366
|
+
const s = activeKey ? sessions.get(activeKey) : null;
|
|
367
|
+
if(!s){ dbg('playClip: no active session'); return 1500; }
|
|
368
|
+
caption.innerHTML=`<span class="spk">${esc(clip.name)} · ${esc(clip.project||clip.origin||'')}</span>${esc(clip.text||'(no text)')}`;
|
|
369
|
+
caption.classList.add('visible');
|
|
370
|
+
chatEl.querySelectorAll('.msg.playing').forEach(e=>e.classList.remove('playing')); if(msgEl)msgEl.classList.add('playing');
|
|
371
|
+
const head=slot.head; if(!head){ dbg('playClip: no head'); return 1500; }
|
|
372
|
+
setSpeaking(s,slot,true);
|
|
373
|
+
try{
|
|
374
|
+
dbg('playClip ctx='+(head.audioCtx?head.audioCtx.state:'none'));
|
|
375
|
+
if(head.audioCtx && head.audioCtx.state!=='running'){
|
|
376
|
+
await Promise.race([ head.audioCtx.resume().catch(()=>{}), new Promise(r=>setTimeout(r,1200)) ]);
|
|
377
|
+
dbg('ctx after resume='+head.audioCtx.state);
|
|
378
|
+
}
|
|
379
|
+
// Cache the raw audio bytes on the clip so replay works even after the
|
|
380
|
+
// server deletes the temp file (120s) and across slot reuse.
|
|
381
|
+
let raw;
|
|
382
|
+
if (clip._bytes) { raw = clip._bytes; }
|
|
383
|
+
else { raw = await (await fetch(clip.audioUrl)).arrayBuffer(); clip._bytes = raw; }
|
|
384
|
+
const buf = await head.audioCtx.decodeAudioData(raw.slice(0)); // decode a copy; keep _bytes intact
|
|
385
|
+
dbg('decoded '+buf.duration.toFixed(1)+'s '+buf.sampleRate+'Hz'+(clip._bytes?' (cached)':''));
|
|
386
|
+
const r={audio:buf}; const text=(clip.text||'').trim();
|
|
387
|
+
if(text){ const w=text.split(/\s+/).filter(Boolean); const per=(buf.duration*1000)/Math.max(1,w.length);
|
|
388
|
+
r.words=w; r.wtimes=w.map((_,i)=>Math.round(i*per)); r.wdurations=w.map(()=>Math.round(per*0.9)); }
|
|
389
|
+
head.speakAudio(r,{lipsyncLang:'en'});
|
|
390
|
+
dbg('speakAudio queued '+(r.words?r.words.length:0)+'w');
|
|
391
|
+
clearTimeout(slot._endT); slot._endT=setTimeout(()=>setSpeaking(s,slot,false), buf.duration*1000+400);
|
|
392
|
+
return buf.duration*1000;
|
|
393
|
+
}catch(e){ dbg('audio ERR '+e.message); try{const a=new Audio(clip.audioUrl);a.volume=1;await a.play();}catch{} setSpeaking(s,slot,false); return 1500; }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ---------- handle speak ----------
|
|
397
|
+
let seq=0;
|
|
398
|
+
async function handleSpeak(msg){
|
|
399
|
+
if(!msg.audioUrl){ dbg('handleSpeak: no audioUrl, skip'); return; }
|
|
400
|
+
const s=getSession(msg);
|
|
401
|
+
const voiceKey=(msg.voice||msg.llm||'default');
|
|
402
|
+
dbg('handleSpeak v='+voiceKey+' active='+(s.key===activeKey?'Y':'N')+' key='+s.key);
|
|
403
|
+
if(!s.cast[voiceKey]){ const idx=Object.keys(s.cast).length;
|
|
404
|
+
s.cast[voiceKey]={ avatarId:pickAvatar(s,voiceKey,idx), accent:ACCENTS[idx%ACCENTS.length], name:(msg.llm||msg.voice||voiceKey), project:(msg.project||'') }; }
|
|
405
|
+
const meta=s.cast[voiceKey];
|
|
406
|
+
const clip={ id:++seq, audioUrl:msg.audioUrl, text:msg.text||'', voiceKey, name:meta.name, accent:meta.accent, project:msg.project||'', origin:msg.origin||'remote', time:new Date().toLocaleTimeString() };
|
|
407
|
+
pushClip(s, clip); s.lastOrigin = msg.origin || s.lastOrigin; updateTab(s);
|
|
408
|
+
if(s.key===activeKey){
|
|
409
|
+
updateHeader(s.label, s.lastOrigin);
|
|
410
|
+
dbg('-> active render, clips='+s.clips.length);
|
|
411
|
+
const slot=await ensureSlot(s,voiceKey);
|
|
412
|
+
renderChat(s);
|
|
413
|
+
await playClip(clip, slot, null);
|
|
414
|
+
} else { dbg('-> UNREAD tab '+s.key+' (active='+activeKey+')'); s.unread++; updateTab(s); }
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ---------- clear ----------
|
|
418
|
+
$('clearBtn').addEventListener('click',()=>{
|
|
419
|
+
if(!activeKey) return; const s=sessions.get(activeKey);
|
|
420
|
+
s.slots.forEach(sl=>{ try{ sl.head&&sl.head.stop&&sl.head.stop(); }catch{}; sl.el.remove(); });
|
|
421
|
+
s.slots=[]; s.cast={}; s.clips=[]; s.built=false; renderChat(s); caption.classList.remove('visible');
|
|
422
|
+
emptyEl.style.display='flex'; updateTab(s); dbg('cleared '+s.key);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// ---------- self-playing demo (?demo=party), plays ONCE ----------
|
|
426
|
+
$('replayBtn').addEventListener('click',()=>runPartyDemo());
|
|
427
|
+
let demoBench=null;
|
|
428
|
+
async function runPartyDemo(){
|
|
429
|
+
applyTheme(hashStr('partydemo'));
|
|
430
|
+
let cast; try{ cast=await(await fetch('/demo-audio/manifest.json')).json(); }catch(e){ dbg('demo manifest fail'); return; }
|
|
431
|
+
// build a dedicated demo session
|
|
432
|
+
const fake={ project:'BMAD Party (demo)', origin:'demo' };
|
|
433
|
+
const s=getSession(fake); switchTo(s.key);
|
|
434
|
+
// reset it
|
|
435
|
+
s.slots.forEach(sl=>sl.el.remove()); s.slots=[]; s.cast={}; s.clips=[]; s.built=true;
|
|
436
|
+
demoBench=[];
|
|
437
|
+
for(const c of cast){ const vk='agent-'+c.name.toLowerCase();
|
|
438
|
+
persistMap[vk]=c.avatar; savePersist();
|
|
439
|
+
s.cast[vk]={ avatarId:c.avatar, accent:c.accent, name:`${c.name} · ${c.role}`, project:'BMAD Party' };
|
|
440
|
+
const slot=await ensureSlot(s,vk); demoBench.push({slot,c,vk}); }
|
|
441
|
+
updateTab(s);
|
|
442
|
+
let i=0;
|
|
443
|
+
async function tick(){
|
|
444
|
+
if(i>=demoBench.length){ $('replayBtn').style.display='block'; dbg('demo done'); return; } // play ONCE
|
|
445
|
+
const {slot,c,vk}=demoBench[i];
|
|
446
|
+
const clip={ id:++seq, audioUrl:c.audioUrl, text:c.text, voiceKey:vk, name:`${c.name} · ${c.role}`, accent:c.accent, project:'BMAD Party', origin:'demo', time:new Date().toLocaleTimeString() };
|
|
447
|
+
pushClip(s, clip); renderChat(s);
|
|
448
|
+
const ms=await playClip(clip,slot,null); i++; setTimeout(tick,(ms||3000)+800);
|
|
449
|
+
}
|
|
450
|
+
tick();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ---------- SSE ----------
|
|
454
|
+
function connectSSE(){
|
|
455
|
+
const es=new EventSource('/events');
|
|
456
|
+
es.onmessage=ev=>{ let m; try{m=JSON.parse(ev.data);}catch{return;}
|
|
457
|
+
if(m.type==='connected'){ connDot.classList.add('on'); dbg('SSE connected'); }
|
|
458
|
+
else if(m.type==='reload'){ dbg('reload signal — refreshing'); setTimeout(()=>location.reload(), 150); }
|
|
459
|
+
else if(m.type==='speak'){ dbg('SSE speak v='+(m.voice||'?')+' p='+(m.project||'?')+' url='+(m.audioUrl?'y':'n')); handleSpeak(m).catch(e=>dbg('speak ERR '+e.message)); } };
|
|
460
|
+
es.onerror=()=>{ connDot.classList.remove('on'); es.close(); setTimeout(connectSSE,3000); };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---------- audio unlock ----------
|
|
464
|
+
function setupAudioUnlock(){
|
|
465
|
+
const hint=$('enable-audio');
|
|
466
|
+
const running=()=>{ for(const s of sessions.values()) for(const sl of s.slots) if(sl.head&&sl.head.audioCtx&&sl.head.audioCtx.state==='running') return true; return false; };
|
|
467
|
+
const resume=async()=>{ for(const s of sessions.values()) for(const sl of s.slots){ try{ if(sl.head&&sl.head.audioCtx&&sl.head.audioCtx.state!=='running') await sl.head.audioCtx.resume(); }catch{} } if(running())hint.style.display='none'; };
|
|
468
|
+
addEventListener('click',resume); addEventListener('keydown',resume); hint.addEventListener('click',resume); hint.style.display='block';
|
|
469
|
+
const p=setInterval(()=>{ if(running()){hint.style.display='none';clearInterval(p);} },800); setTimeout(()=>clearInterval(p),20000);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ---------- diagnostics (exposed for CDP testing) ----------
|
|
473
|
+
window.__errors = [];
|
|
474
|
+
window.addEventListener('error', e => {
|
|
475
|
+
const stk = (e.error && e.error.stack) ? e.error.stack.split('\n').slice(0,4).join(' << ') : '';
|
|
476
|
+
const det = (e.message||'') + ' @ ' + String(e.filename||'?').split('/').pop() + ':' + (e.lineno||'?') + ':' + (e.colno||'?') + (stk ? ' | ' + stk : '');
|
|
477
|
+
window.__errors.push(det); dbg('JS ERROR '+det);
|
|
478
|
+
});
|
|
479
|
+
window.addEventListener('unhandledrejection', e => { window.__errors.push('reject: '+String(e.reason)); dbg('REJECT '+String(e.reason)); });
|
|
480
|
+
window.__diag = () => {
|
|
481
|
+
const out = { activeKey, sessions:[...sessions.keys()], errors: window.__errors.slice(-8), slots:[] };
|
|
482
|
+
for (const s of sessions.values()) for (const sl of s.slots) {
|
|
483
|
+
out.slots.push({ session:s.key, voice:sl.voiceKey, avatar:sl.key, ready:sl.ready,
|
|
484
|
+
ctx: sl.head && sl.head.audioCtx ? sl.head.audioCtx.state : 'none',
|
|
485
|
+
gain: sl.head && sl.head.audioSpeechGainNode ? sl.head.audioSpeechGainNode.gain.value : null,
|
|
486
|
+
armature: sl.head ? !!sl.head.armature : false,
|
|
487
|
+
speechSrc: sl.head ? !!sl.head.audioSpeechSource : false,
|
|
488
|
+
reverbBuf: sl.head && sl.head.audioReverbNode ? !!sl.head.audioReverbNode.buffer : 'noReverbNode',
|
|
489
|
+
hasAnalyzer: sl.head ? !!sl.head.audioAnalyzerNode : false });
|
|
490
|
+
}
|
|
491
|
+
return out;
|
|
492
|
+
};
|
|
493
|
+
// Play a 0.4s beep straight to destination on the first slot's context — proves
|
|
494
|
+
// whether the page can output ANY audio (isolates TalkingHead's graph from the device).
|
|
495
|
+
window.__beep = async () => {
|
|
496
|
+
const sl = [...sessions.values()].flatMap(s=>s.slots)[0];
|
|
497
|
+
if (!sl || !sl.head || !sl.head.audioCtx) return 'no audio ctx';
|
|
498
|
+
const ctx = sl.head.audioCtx;
|
|
499
|
+
if (ctx.state!=='running') await ctx.resume().catch(()=>{});
|
|
500
|
+
const osc = ctx.createOscillator(), g = ctx.createGain();
|
|
501
|
+
osc.frequency.value = 440; g.gain.value = 0.3;
|
|
502
|
+
osc.connect(g); g.connect(ctx.destination);
|
|
503
|
+
osc.start(); osc.stop(ctx.currentTime + 0.4);
|
|
504
|
+
return 'beep on ctx='+ctx.state;
|
|
505
|
+
};
|
|
506
|
+
// Run a real clip through TalkingHead.speakAudio and measure the analyser
|
|
507
|
+
// amplitude — >0 means audio is flowing through the graph to the destination.
|
|
508
|
+
window.__testSpeak = async () => {
|
|
509
|
+
const sl = [...sessions.values()].flatMap(s=>s.slots)[0];
|
|
510
|
+
if (!sl || !sl.head) return 'no slot';
|
|
511
|
+
const ctx = sl.head.audioCtx;
|
|
512
|
+
if (ctx.state!=='running') await ctx.resume().catch(()=>{});
|
|
513
|
+
const buf = await ctx.decodeAudioData(await (await fetch('/demo-audio/cast1.wav')).arrayBuffer());
|
|
514
|
+
sl.head.speakAudio({ audio: buf }, { lipsyncLang:'en' });
|
|
515
|
+
const an = sl.head.audioAnalyzerNode;
|
|
516
|
+
const data = new Uint8Array(an.fftSize || 2048);
|
|
517
|
+
let maxAmp = 0;
|
|
518
|
+
for (let i=0;i<40;i++){ await new Promise(r=>setTimeout(r,50)); an.getByteTimeDomainData(data); for (const v of data) maxAmp=Math.max(maxAmp, Math.abs(v-128)); }
|
|
519
|
+
return { maxAnalyzerAmp: maxAmp, ctx: ctx.state, dur: +buf.duration.toFixed(1), gainNow: sl.head.audioSpeechGainNode.gain.value };
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// ---------- boot ----------
|
|
523
|
+
(async()=>{
|
|
524
|
+
try{
|
|
525
|
+
applyTheme(0); setupAudioUnlock(); connectSSE();
|
|
526
|
+
if(params.get('demo')==='party'){ $('replayBtn').style.display='none'; runPartyDemo(); }
|
|
527
|
+
else {
|
|
528
|
+
// Preload one reliable avatar so the stage is never blank and the first
|
|
529
|
+
// message has somewhere to speak immediately.
|
|
530
|
+
const s=getSession({project:'AgentVibes',origin:'local'});
|
|
531
|
+
s.cast['__default__']={ avatarId:DEFAULT_AVATAR, accent:ACCENTS[0], name:'AgentVibes', project:'AgentVibes' };
|
|
532
|
+
await ensureSlot(s,'__default__');
|
|
533
|
+
dbg('default avatar preloaded');
|
|
534
|
+
}
|
|
535
|
+
}catch(e){ showError('Boot failed: '+e.message); dbg('BOOT '+e.message); }
|
|
536
|
+
})();
|
|
537
|
+
</script>
|
|
538
|
+
</body>
|
|
539
|
+
</html>
|
|
@@ -0,0 +1,77 @@
|
|
|
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.0">
|
|
6
|
+
<title>AgentVibes · Avatar Gallery</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body { min-height: 100vh; background: radial-gradient(120% 100% at 50% -10%, #1b1240 0%, #07040f 55%, #04030a 100%);
|
|
10
|
+
color: #e9e9ff; font-family: -apple-system, 'Segoe UI', sans-serif; padding: 48px 24px; }
|
|
11
|
+
.stars { position: fixed; inset: 0; z-index: 0; pointer-events: none;
|
|
12
|
+
background-image: radial-gradient(1px 1px at 20% 30%, #fff, transparent), radial-gradient(1px 1px at 70% 60%, #cdd, transparent),
|
|
13
|
+
radial-gradient(1px 1px at 40% 80%, #fff, transparent), radial-gradient(1px 1px at 85% 25%, #aef, transparent),
|
|
14
|
+
radial-gradient(1px 1px at 55% 15%, #fff, transparent), radial-gradient(1px 1px at 10% 70%, #fce, transparent);
|
|
15
|
+
background-size: 100% 100%; opacity: 0.5; }
|
|
16
|
+
.wrap { position: relative; z-index: 1; max-width: 960px; margin: 0 auto; }
|
|
17
|
+
h1 { font-size: 34px; font-weight: 800; letter-spacing: -0.5px; margin-bottom: 6px;
|
|
18
|
+
background: linear-gradient(90deg,#a78bfa,#22d3ee,#fb7185); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
|
19
|
+
.sub { opacity: 0.7; margin-bottom: 32px; font-size: 15px; }
|
|
20
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px,1fr)); gap: 18px; }
|
|
21
|
+
.card { display: block; text-decoration: none; color: inherit; background: rgba(20,18,42,0.7);
|
|
22
|
+
border: 1px solid rgba(255,255,255,0.1); border-radius: 18px; padding: 22px; transition: transform .15s, border-color .15s, box-shadow .15s; }
|
|
23
|
+
.card:hover { transform: translateY(-4px); border-color: var(--a,#a78bfa); box-shadow: 0 12px 40px -12px var(--a,#a78bfa); }
|
|
24
|
+
.card .ico { font-size: 30px; margin-bottom: 12px; }
|
|
25
|
+
.card h3 { font-size: 17px; margin-bottom: 6px; }
|
|
26
|
+
.card p { font-size: 13px; opacity: 0.7; line-height: 1.5; }
|
|
27
|
+
.tag { display: inline-block; margin-top: 12px; font-size: 11px; padding: 3px 9px; border-radius: 999px;
|
|
28
|
+
background: rgba(255,255,255,0.08); border: 1px solid var(--a,#a78bfa); color: var(--a,#a78bfa); }
|
|
29
|
+
.note { margin-top: 30px; font-size: 12px; opacity: 0.55; line-height: 1.6; }
|
|
30
|
+
code { background: rgba(255,255,255,0.08); padding: 1px 6px; border-radius: 5px; font-size: 12px; }
|
|
31
|
+
</style>
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<div class="stars"></div>
|
|
35
|
+
<div class="wrap">
|
|
36
|
+
<h1>✦ AgentVibes Avatar Stage</h1>
|
|
37
|
+
<div class="sub">Talking-head avatars on a living galaxy — wired to your live AgentVibes audio.</div>
|
|
38
|
+
<div class="grid">
|
|
39
|
+
<a class="card" style="--a:#a78bfa" href="/cosmic.html?mode=party&demo=party">
|
|
40
|
+
<div class="ico">🎭</div><h3>Party Cast — Demo</h3>
|
|
41
|
+
<p>Four agents (Winston, Sally, John, Amelia) perform a scripted scene on the cosmic stage. Self-playing — open and watch.</p>
|
|
42
|
+
<span class="tag">recommended first</span>
|
|
43
|
+
</a>
|
|
44
|
+
<a class="card" style="--a:#22d3ee" href="/cosmic.html?mode=party">
|
|
45
|
+
<div class="ico">🛰️</div><h3>Party Cast — Live</h3>
|
|
46
|
+
<p>Up to four heads, one per agent voice, remembered across sessions. Lights up the speaker. Driven by real party-mode audio.</p>
|
|
47
|
+
<span class="tag">live</span>
|
|
48
|
+
</a>
|
|
49
|
+
<a class="card" style="--a:#fb7185" href="/cosmic.html?mode=regular">
|
|
50
|
+
<div class="ico">🌙</div><h3>Regular Avatar</h3>
|
|
51
|
+
<p>One consistent avatar for everyday Claude TTS, floating in the galaxy. Speaks whatever the receiver receives.</p>
|
|
52
|
+
<span class="tag">live</span>
|
|
53
|
+
</a>
|
|
54
|
+
<a class="card" style="--a:#34d399" href="/cosmic.html?mode=opencast">
|
|
55
|
+
<div class="ico">🌌</div><h3>OpenCast</h3>
|
|
56
|
+
<p>Each new cast gets a fresh galaxy theme + a suited set of characters. Background regenerates per session.</p>
|
|
57
|
+
<span class="tag">experimental</span>
|
|
58
|
+
</a>
|
|
59
|
+
<a class="card" style="--a:#fbbf24" href="/index.html">
|
|
60
|
+
<div class="ico">🧪</div><h3>Classic POC</h3>
|
|
61
|
+
<p>The original single-head proof of concept (no galaxy). Kept as the known-good baseline.</p>
|
|
62
|
+
<span class="tag">baseline</span>
|
|
63
|
+
</a>
|
|
64
|
+
<a class="card" style="--a:#60a5fa" href="/cosmic.html?mode=party&demo=party&debug=1">
|
|
65
|
+
<div class="ico">🔧</div><h3>Party Demo + Debug</h3>
|
|
66
|
+
<p>Same self-playing scene with the green debug panel visible (decode, words, lip-sync ground truth).</p>
|
|
67
|
+
<span class="tag">diagnostics</span>
|
|
68
|
+
</a>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="note">
|
|
71
|
+
Tip: launch any of these in a borderless window via <code>open-cosmic.ps1</code> (adds the Chrome autoplay flag so audio plays hands-free).
|
|
72
|
+
Click the <code>regular / party / opencast</code> chip (top of the stage) to switch modes live.
|
|
73
|
+
All stages also listen to the live receiver, so real AgentVibes messages animate them in real time.
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</body>
|
|
77
|
+
</html>
|