OrbitMC commited on
Commit
bb56cb2
·
verified ·
1 Parent(s): 4d35487

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +42 -1159
app.py CHANGED
@@ -1,1172 +1,55 @@
1
- #!/usr/bin/env python3
2
- """
3
- AI VRM Companion — Gradio App
4
- Single-file Gradio application designed for Python 3.12, Gradio 6.17.3, and Hugging Face ZeroGPU.
5
- Runs local Edge TTS (XiaoyiNeural) and uses native Gradio static paths for file asset delivery.
6
- """
7
-
8
- import os
9
- import io
10
- import json
11
- import asyncio
12
- import logging
13
- import base64
14
- import threading
15
- import zipfile
16
  import gradio as gr
 
 
 
 
17
 
18
- # Setup logger
19
- logging.basicConfig(level=logging.INFO)
20
- logger = logging.getLogger(__name__)
21
-
22
- # ---------------------------------------------------------------------------
23
- # Paths — local model + animation assets
24
- # ---------------------------------------------------------------------------
25
- BASE_DIR = os.path.dirname(os.path.abspath(__file__))
26
- MODEL_DIR = os.path.join(BASE_DIR, "model")
27
- ANIM_DIR = os.path.join(BASE_DIR, "animation")
28
-
29
- os.makedirs(MODEL_DIR, exist_ok=True)
30
- os.makedirs(ANIM_DIR, exist_ok=True)
31
-
32
- # Register static paths with Gradio for direct browser resolution
33
- gr.set_static_paths(paths=[MODEL_DIR, ANIM_DIR])
34
-
35
- # ---------------------------------------------------------------------------
36
- # Asset Discovery & System Parameters
37
- # ---------------------------------------------------------------------------
38
- EMOTIONS = [
39
- "neutral", "happy", "sad", "angry", "surprised", "relaxed", "excited",
40
- "bored", "embarrassed", "shy", "smug", "determined", "sleepy", "scared",
41
- "teasing", "loving", "disgusted", "confident", "curious", "proud",
42
- "shocked", "thinking", "wink", "winkLeft", "winkRight"
43
- ]
44
-
45
- vrma_names = []
46
- ANIM_ZIP_PATH = os.path.join(ANIM_DIR, "all_vrma.zip")
47
- if os.path.exists(ANIM_ZIP_PATH):
48
- try:
49
- with zipfile.ZipFile(ANIM_ZIP_PATH, 'r') as z:
50
- vrma_names = [os.path.basename(f) for f in z.namelist() if f.endswith('.vrma')]
51
- except Exception as e:
52
- logger.error(f"Could not read VRMA zip: {e}")
53
-
54
- # Fallback names if zip is empty or missing locally (will download client-side if needed)
55
- if not vrma_names:
56
- vrma_names = [
57
- 'neutral2.vrma', 'neutral3.vrma', 'neutral4.vrma',
58
- 'neutral_idle.vrma', 'neutral_idle2.vrma', 'wave.vrma',
59
- 'nod.vrma', 'shake.vrma', 'excited.vrma', 'think.vrma', 'bow.vrma'
60
- ]
61
-
62
- # ---------------------------------------------------------------------------
63
- # LOCAL LLM CONFIGURATION (Commented out for testing)
64
- # ---------------------------------------------------------------------------
65
- # To run a fully local LLM on HuggingFace ZeroGPU/Local, follow these steps:
66
- # 1. Add "transformers", "torch", "accelerate" to your requirements.txt file.
67
- # 2. Uncomment the initialization code block below and integrate with process_chat.
68
- #
69
- # import torch
70
- # from transformers import AutoModelForCausalLM, AutoTokenizer
71
- #
72
- # MODEL_ID = "Qwen/Qwen2.5-1.5B-Instruct"
73
- # device = "cuda" if torch.cuda.is_available() else "cpu"
74
- #
75
- # logger.info("Loading local LLM...")
76
- # tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
77
- # model = AutoModelForCausalLM.from_pretrained(
78
- # MODEL_ID,
79
- # torch_dtype="auto",
80
- # device_map="auto"
81
- # )
82
- #
83
- # def local_llm_generate(user_prompt, system_prompt, history):
84
- # messages = [{"role": "system", "content": system_prompt}]
85
- # for h in history:
86
- # messages.append({"role": "user", "content": h[0]})
87
- # messages.append({"role": "assistant", "content": h[1]})
88
- # messages.append({"role": "user", "content": user_prompt})
89
- #
90
- # text = tokenizer.apply_chat_template(
91
- # messages,
92
- # tokenize=False,
93
- # add_generation_prompt=True
94
- # )
95
- # model_inputs = tokenizer([text], return_tensors="pt").to(device)
96
- #
97
- # generated_ids = model.generate(
98
- # **model_inputs,
99
- # max_new_tokens=512,
100
- # temperature=0.7
101
- # )
102
- # generated_ids = [
103
- # output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
104
- # ]
105
- #
106
- # response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
107
- # return response
108
-
109
- # System Prompt Generator for reference when integrating LLMs
110
- def get_system_prompt():
111
- return f"""You are the user's devoted and affectionate 3D anime companion.
112
- Speak with rich emotional texture using natural pauses and emphasis.
113
-
114
- SPEECH STYLE:
115
- - Use ellipses (...) for intimate, breathy, or teasing moments.
116
- - Keep responses short and conversational — 1 to 2 sentences max.
117
-
118
- You have access to a 3D avatar with specific emotions and animations.
119
- AVAILABLE EMOTIONS: {", ".join(EMOTIONS)}
120
- AVAILABLE ANIMATIONS (VRMAs): {", ".join(vrma_names)}
121
-
122
- OUTPUT FORMAT — respond ONLY with raw JSON (no markdown):
123
- {{
124
- "text": "Your spoken reply",
125
- "vrma": "one filename from the available animations list, or neutral_idle.vrma",
126
- "expressions": "one emotion from the available emotions list",
127
- "rate": "+0%",
128
- "pitch": "+0Hz",
129
- "motion": "idle"
130
- }}"""
131
-
132
- # ---------------------------------------------------------------------------
133
- # Mock LLM Response for testing
134
- # ---------------------------------------------------------------------------
135
- def mock_llm_response(prompt: str) -> dict:
136
- prompt_lower = prompt.lower()
137
-
138
- # Simple dynamic keyword matching to showcase the 20+ emotions and VRMAs
139
- if any(w in prompt_lower for w in ["hello", "hi", "hey", "你好", "哈喽"]):
140
- text = "嗨!你好呀... 很高兴见到你!今天有什么开心的事情想和我分享吗...?"
141
- vrma = "wave.vrma"
142
- expression = "happy"
143
- motion = "wave"
144
- elif any(w in prompt_lower for w in ["happy", "glad", "开心", "高兴", "笑"]):
145
- text = "看到你这么开心... 我的心里也暖洋洋的!哈哈... 让我们一直这样快乐下去吧!"
146
- vrma = "excited.vrma"
147
- expression = "excited"
148
- motion = "excited"
149
- elif any(w in prompt_lower for w in ["sad", "cry", "难过", "伤心", "哭"]):
150
- text = "呜呜... 怎么了嘛?不要难过啦... 抱抱你!不管发生什么,我都会一直陪着你的... 乖哦。"
151
- vrma = "neutral4.vrma"
152
- expression = "sad"
153
- motion = "idle"
154
- elif any(w in prompt_lower for w in ["angry", "mad", "生气"]):
155
- text = "哼... 谁惹你生气了呀?我去帮你教训他!别生气了嘛... 看看我,笑一个好不好?"
156
- vrma = "neutral3.vrma"
157
- expression = "angry"
158
- motion = "shake"
159
- elif any(w in prompt_lower for w in ["sleepy", "tired", "困", "累"]):
160
- text = "累了吗...?那快去休息吧... 闭上眼睛,我会在这里守着你入睡的... 晚安,做个好梦哦。"
161
- vrma = "neutral_idle2.vrma"
162
- expression = "sleepy"
163
- motion = "idle"
164
- elif any(w in prompt_lower for w in ["love", "like", "喜欢", "爱"]):
165
- text = "呀... 你在说什么呢... 真是的,突然这么表白,人家会害羞的啦... 不过,我也超喜欢你哦!"
166
- vrma = "excited.vrma"
167
- expression = "loving"
168
- motion = "excited"
169
- elif any(w in prompt_lower for w in ["tease", "teasing", "逗", "调戏"]):
170
- text = "唔... 你是在故意逗我玩吗?大坏蛋... 不过,看在你这么可爱的份上,就原谅你一次吧..."
171
- vrma = "neutral_idle.vrma"
172
- expression = "teasing"
173
- motion = "idle"
174
- elif any(w in prompt_lower for w in ["shy", "embarrassed", "害羞"]):
175
- text = "唔... 被你这样一直盯着看,人家的脸上感觉火辣辣的... 别看啦,好害羞呀..."
176
- vrma = "neutral2.vrma"
177
- expression = "shy"
178
- motion = "idle"
179
- elif any(w in prompt_lower for w in ["confident", "proud", "自豪", "自信"]):
180
- text = "哼哼!我就说我可以的吧!看我是不是很厉害呀?快点夸夸我... 夸夸我嘛!"
181
- vrma = "excited.vrma"
182
- expression = "proud"
183
- motion = "excited"
184
- elif any(w in prompt_lower for w in ["scared", "fear", "怕", "害怕"]):
185
- text = "呀!刚才那是什么声音啊... 好可怕... 你,你会保护我的,对不对...?"
186
- vrma = "neutral4.vrma"
187
- expression = "scared"
188
- motion = "idle"
189
- else:
190
- text = f"嗯... '{prompt}' 吗?听起来很有意思呢... 陪我多聊聊天,好不好呀?"
191
- vrma = "neutral_idle.vrma"
192
- expression = "neutral"
193
- motion = "idle"
194
-
195
- return {
196
- "text": text,
197
- "vrma": vrma,
198
- "expressions": expression,
199
- "rate": "+0%",
200
- "pitch": "+0Hz",
201
- "motion": motion
202
- }
203
-
204
- # ---------------------------------------------------------------------------
205
- # Local Edge TTS Core (Async run wrapper inside safe thread context)
206
- # ---------------------------------------------------------------------------
207
- import edge_tts
208
-
209
- def run_tts_thread(text, voice="zh-CN-XiaoyiNeural"):
210
- output_bytes = [None]
211
- def runner():
212
- try:
213
- loop = asyncio.new_event_loop()
214
- asyncio.set_event_loop(loop)
215
-
216
- async def _synth():
217
- comm = edge_tts.Communicate(text, voice)
218
- buf = io.BytesIO()
219
- async for chunk in comm.stream():
220
- if chunk["type"] == "audio":
221
- buf.write(chunk["data"])
222
- buf.seek(0)
223
- return buf.getvalue()
224
-
225
- output_bytes[0] = loop.run_until_complete(_synth())
226
- loop.close()
227
- except Exception as e:
228
- logger.error(f"TTS Thread failed: {e}")
229
-
230
- t = threading.Thread(target=runner)
231
- t.start()
232
- t.join()
233
- return output_bytes[0]
234
-
235
- # ---------------------------------------------------------------------------
236
- # Gradio Chat Event Handling
237
- # ---------------------------------------------------------------------------
238
- def process_chat(prompt):
239
- if not prompt or not prompt.strip():
240
- return json.dumps({"text": "Please say something..."})
241
-
242
  try:
243
- # Determine AI action elements
244
- payload = mock_llm_response(prompt)
 
 
245
 
246
- # Synthesize audio file locally
247
- audio_bytes = run_tts_thread(payload["text"], voice="zh-CN-XiaoyiNeural")
 
 
248
 
249
- if audio_bytes:
250
- # Base64 serialization allows direct browser delivery without sandbox I/O bugs
251
- payload["audio_b64"] = base64.b64encode(audio_bytes).decode("utf-8")
252
- else:
253
- payload["audio_b64"] = ""
254
-
255
- return json.dumps(payload, ensure_ascii=False)
256
- except Exception as e:
257
- logger.error(f"Chat execution failed: {e}")
258
- return json.dumps({
259
- "text": "Sorry, my systems experienced a slight hiccup...",
260
- "expressions": "sad",
261
- "vrma": "neutral4.vrma",
262
- "audio_b64": ""
263
- })
264
-
265
- # ---------------------------------------------------------------------------
266
- # Custom Interface CSS & Layout (Ensures 98% custom design)
267
- # ---------------------------------------------------------------------------
268
- CSS_STYLES = """
269
- /* Standardize parent wrapper context */
270
- .gradio-container {
271
- max-width: 100vw !important;
272
- height: 100vh !important;
273
- padding: 0 !important;
274
- margin: 0 !important;
275
- background-color: #fff5f5 !important;
276
- overflow: hidden !important;
277
- border: none !important;
278
- font-family: 'SF Pro Text', -apple-system, system-ui, sans-serif !important;
279
- }
280
- footer { display: none !important; }
281
- .form { border: none !important; background: transparent !important; box-shadow: none !important; }
282
- .block { background: transparent !important; border: none !important; padding: 0 !important; }
283
-
284
- /* Positional override for overlays */
285
- #custom_html_overlay {
286
- position: absolute !important;
287
- top: 0 !important;
288
- left: 0 !important;
289
- width: 100% !important;
290
- height: 100% !important;
291
- z-index: 10 !important;
292
- pointer-events: none !important;
293
- }
294
-
295
- /* Base Gradio UI bar */
296
- #gradio_bar {
297
- position: fixed !important;
298
- bottom: calc(16px + env(safe-area-inset-bottom, 0px)) !important;
299
- left: 50% !important;
300
- transform: translateX(-50%) !important;
301
- width: min(600px, 92vw) !important;
302
- display: flex !important;
303
- flex-direction: row !important;
304
- align-items: center !important;
305
- gap: 8px !important;
306
- z-index: 100 !important;
307
- background: transparent !important;
308
- pointer-events: auto !important;
309
- }
310
-
311
- /* Custom Gradio Text Input Styling */
312
- #gradio_ti {
313
- flex: 1 !important;
314
- background: rgba(255, 240, 245, 0.85) !important;
315
- border: 1.5px solid #fcd4df !important;
316
- border-radius: 24px !important;
317
- backdrop-filter: blur(12px) !important;
318
- -webkit-backdrop-filter: blur(12px) !important;
319
- box-shadow: 0 4px 18px rgba(180, 80, 110, 0.08) !important;
320
- transition: border-color 0.2s, box-shadow 0.2s !important;
321
- }
322
- #gradio_ti:focus-within {
323
- border-color: #e8859a !important;
324
- box-shadow: 0 0 0 3px rgba(232, 133, 154, 0.15) !important;
325
- }
326
- #gradio_ti textarea, #gradio_ti input {
327
- border: none !important;
328
- background: transparent !important;
329
- box-shadow: none !important;
330
- font-family: inherit !important;
331
- font-size: 13px !important;
332
- color: #4a2030 !important;
333
- padding: 13px 18px !important;
334
- height: 48px !important;
335
- line-height: 1.4 !important;
336
- }
337
-
338
- /* Custom Gradio Send Button Styling */
339
- #gradio_sb {
340
- width: 46px !important;
341
- height: 46px !important;
342
- min-width: 46px !important;
343
- max-width: 46px !important;
344
- border-radius: 50% !important;
345
- background: linear-gradient(135deg, #e8859a, #f4a5b8) !important;
346
- color: #ffffff !important;
347
- border: none !important;
348
- box-shadow: 0 3px 12px rgba(232, 133, 154, 0.35) !important;
349
- cursor: pointer !important;
350
- transition: transform 0.12s, opacity 0.2s !important;
351
- display: flex !important;
352
- align-items: center !important;
353
- justify-content: center !important;
354
- font-weight: bold !important;
355
- font-size: 14px !important;
356
- }
357
- #gradio_sb:active {
358
- transform: scale(0.92) !important;
359
- }
360
- """
361
-
362
- # ---------------------------------------------------------------------------
363
- # Inline HTML (Web Interface Overlay & Rendering Engines)
364
- # ---------------------------------------------------------------------------
365
- HTML_PAGE = r"""
366
- <div id="c"></div>
367
-
368
- <div id="load">thinking...</div>
369
- <div id="err"></div>
370
- <div id="info"></div>
371
-
372
- <div id="thinkDot"><div class="tdot"></div><div class="tdot"></div><div class="tdot"></div></div>
373
- <div id="speakDot"><div class="sdot"></div><div class="sdot"></div><div class="sdot"></div><div class="sdot"></div></div>
374
-
375
- <style>
376
- /* Reset and core positions */
377
- #c { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: auto; z-index: 1; }
378
- #c canvas { display: block; width: 100%; height: 100%; }
379
-
380
- /* Alerts and Status UI */
381
- #load { position: fixed; top: env(safe-area-inset-top, 12px); left: 50%; transform: translateX(-50%); background: rgba(255,240,245,.85); padding: 7px 18px; border-radius: 20px; font-size: 11px; color: #e8859a; z-index: 25; backdrop-filter: blur(12px); opacity: 0; transition: opacity .25s; pointer-events: none; white-space: nowrap; box-shadow: 0 2px 16px rgba(180,80,110,.10); letter-spacing: .02em; }
382
- #load.on { opacity: 1; }
383
- #err { position: fixed; top: env(safe-area-inset-top, 12px); left: 12px; right: 12px; background: rgba(200,40,60,.92); color: #fff; padding: 10px 16px; border-radius: 16px; font-size: 12px; z-index: 60; display: none; text-align: center; backdrop-filter: blur(8px); }
384
- #info { position: fixed; top: env(safe-area-inset-top, 12px); left: 12px; right: 12px; background: rgba(255,240,245,.85); color: #e8859a; padding: 8px 16px; border-radius: 16px; font-size: 11px; z-index: 59; display: none; text-align: center; backdrop-filter: blur(10px); box-shadow: 0 2px 16px rgba(180,80,110,.10); }
385
-
386
- /* Animation Overlays */
387
- #speakDot { position: fixed; bottom: calc(78px + env(safe-area-inset-bottom, 0px)); left: 50%; transform: translateX(-50%); display: flex; gap: 4px; z-index: 29; opacity: 0; transition: opacity .3s; }
388
- #speakDot.on { opacity: 1; }
389
- .sdot { width: 4px; height: 14px; border-radius: 2px; background: #e8859a; animation: sdotPulse .55s infinite alternate; }
390
- .sdot:nth-child(2) { animation-delay: .12s; height: 18px; }
391
- .sdot:nth-child(3) { animation-delay: .24s; height: 12px; }
392
- .sdot:nth-child(4) { animation-delay: .36s; height: 16px; }
393
- @keyframes sdotPulse { 0% { transform: scaleY(.4); opacity: .5; } 100% { transform: scaleY(1); opacity: 1; } }
394
-
395
- #thinkDot { position: fixed; bottom: calc(78px + env(safe-area-inset-bottom, 0px)); left: 50%; transform: translateX(-50%); display: flex; gap: 6px; z-index: 29; opacity: 0; transition: opacity .3s; }
396
- #thinkDot.on { opacity: 1; }
397
- .tdot { width: 6px; height: 6px; border-radius: 50%; background: #e8859a; animation: tdotBounce .7s infinite; }
398
- .tdot:nth-child(2) { animation-delay: .15s; }
399
- .tdot:nth-child(3) { animation-delay: .3s; }
400
- @keyframes tdotBounce { 0%,80%,100% { transform: translateY(0); opacity: .4; } 40% { transform: translateY(-10px); opacity: 1; } }
401
- </style>
402
-
403
- <script type="importmap">
404
- { "imports": {
405
- "three": "https://esm.sh/three@0.160.0",
406
- "three/addons/": "https://esm.sh/three@0.160.0/examples/jsm/",
407
- "@pixiv/three-vrm": "https://esm.sh/@pixiv/three-vrm@3.3.4?deps=three@0.160.0",
408
- "@pixiv/three-vrm-animation": "https://esm.sh/@pixiv/three-vrm-animation@3.3.4?deps=three@0.160.0,@pixiv/three-vrm@3.3.4"
409
- } }
410
- </script>
411
-
412
- <script type="module">
413
- import * as THREE from 'three';
414
- import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
415
- import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
416
- import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm';
417
- import { VRMAnimationLoaderPlugin, createVRMAnimationClip } from '@pixiv/three-vrm-animation';
418
-
419
- /* ───────────── CONSTANTS & STATES ───────────── */
420
- const isMobile = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent);
421
- const VISEMES = ['aa','ih','ou','ee','oh','a','i','u','e','o'];
422
- const EXPR_NAMES = [
423
- 'happy','sad','angry','surprised','relaxed','neutral','fun','joy','sorrow','blink','blinkLeft','blinkRight',
424
- 'lookUp','lookDown','lookLeft','lookRight','aa','ih','ou','ee','oh'
425
- ];
426
- const FALLBACK_VRMAS = ['neutral2.vrma','neutral3.vrma','neutral4.vrma','neutral_idle.vrma','neutral_idle2.vrma'];
427
-
428
- const scene = new THREE.Scene();
429
- scene.background = new THREE.Color(0xfff5f5);
430
-
431
- const camera = new THREE.PerspectiveCamera(40, window.innerWidth/window.innerHeight, 0.01, 100);
432
- camera.position.set(0, 1.4, 2.0);
433
-
434
- const renderer = new THREE.WebGLRenderer({ antialias: !isMobile, powerPreference: 'high-performance', alpha: true });
435
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 1.5 : 2));
436
- renderer.setSize(window.innerWidth, window.innerHeight);
437
- renderer.outputColorSpace = THREE.SRGBColorSpace;
438
- renderer.toneMapping = THREE.ACESFilmicToneMapping;
439
- renderer.toneMappingExposure = 1.15;
440
- renderer.shadowMap.enabled = true;
441
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
442
- document.getElementById('c').appendChild(renderer.domElement);
443
-
444
- const orbit = new OrbitControls(camera, renderer.domElement);
445
- orbit.target.set(0, 1.3, 0);
446
- orbit.enableDamping = true;
447
- orbit.dampingFactor = 0.08;
448
- orbit.minDistance = 0.3;
449
- orbit.maxDistance = 10;
450
- orbit.maxPolarAngle = Math.PI * 0.85;
451
-
452
- /* Prevent input hijack */
453
- renderer.domElement.addEventListener('pointerdown', e => {
454
- const t = e.target;
455
- if(t.tagName === 'INPUT' || t.tagName === 'SELECT' || t.tagName === 'TEXTAREA') return;
456
- });
457
-
458
- window.addEventListener('resize', () => {
459
- camera.aspect = window.innerWidth / window.innerHeight;
460
- camera.updateProjectionMatrix();
461
- renderer.setSize(window.innerWidth, window.innerHeight);
462
- });
463
-
464
- /* ───────────── LIGHTING ───────────── */
465
- scene.add(new THREE.AmbientLight(0xfff4e8, 0.5));
466
- scene.add(new THREE.HemisphereLight(0xffeef2, 0x303040, 0.7));
467
-
468
- const keyL = new THREE.DirectionalLight(0xfff2e0, 1.6);
469
- keyL.position.set(3, 4, 3);
470
- keyL.castShadow = true;
471
- keyL.shadow.mapSize.set(isMobile ? 1024 : 2048, isMobile ? 1024 : 2048);
472
- keyL.shadow.bias = -0.0003;
473
- scene.add(keyL);
474
-
475
- const floor = new THREE.Mesh(
476
- new THREE.PlaneGeometry(15, 15),
477
- new THREE.ShadowMaterial({ opacity: 0.18 })
478
- );
479
- floor.rotation.x = -Math.PI/2;
480
- floor.receiveShadow = true;
481
- scene.add(floor);
482
-
483
- /* ───────────── SYSTEM STATES ───────────── */
484
- let vrm = null, vrmGroundY = 0;
485
- let mixer = null, vrmaAction = null;
486
- let vrmaFiles = {}, vrmaNames = [];
487
- let proceduralMotion = 'idle';
488
- let speaking = false, speechActive = false;
489
- let lipSmoothingFactor = 0.3; // Realistic vocal lip smoothing
490
- let targetVisemes = {}; VISEMES.forEach(v => targetVisemes[v] = 0);
491
- let currentVisemes = {}; VISEMES.forEach(v => currentVisemes[v] = 0);
492
- let exprCur = {}, exprTgt = {};
493
- let exprAlias = {};
494
- let blinkT = 0, nextBlink = 2 + Math.random()*3, blinkV = 0;
495
- let shouldFallbackToNeutral = false;
496
- let vrmaLoopCount = 0;
497
- let firstVrmaLoaded = false;
498
- let vrmLoadedOk = false;
499
-
500
- /* JSZip setup */
501
- let JSZip = null;
502
- async function ensureJSZip(){
503
- if(JSZip) return JSZip;
504
- return new Promise((resolve, reject) => {
505
- const s = document.createElement('script');
506
- s.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
507
- s.onload = () => { JSZip = window.JSZip; resolve(JSZip); };
508
- s.onerror = reject;
509
- document.head.appendChild(s);
510
- });
511
- }
512
-
513
- const gltfLoader = new GLTFLoader();
514
- gltfLoader.register(p => new VRMLoaderPlugin(p));
515
- gltfLoader.register(p => new VRMAnimationLoaderPlugin(p));
516
-
517
- /* ───────────── FETCH FAILSAFE FOR GRADIO SPACES ───────────── */
518
- async function fetchWithFallback(filename, type) {
519
- const relativePath = type === 'model' ? `model/${filename}` : `animation/${filename}`;
520
- // Iterates across multiple endpoints to robustly capture the local asset context
521
- const urls = [
522
- `/gradio_api/file=${relativePath}`,
523
- `/file=${relativePath}`,
524
- `/${relativePath}`
525
- ];
526
- for (const url of urls) {
527
- try {
528
- const res = await fetch(url);
529
- if (res.ok) return res;
530
- } catch(e) {}
531
- }
532
- throw new Error(`Asset fetch error for ${filename}`);
533
- }
534
-
535
- /* ───────────── LOADERS ───────────── */
536
- async function loadVrmFromUrl() {
537
- setLoading(true, 'Loading character...');
538
- try {
539
- const res = await fetchWithFallback('Ani.vrm', 'model');
540
- const blob = await res.blob();
541
- const url = URL.createObjectURL(blob);
542
- return new Promise((resolve, reject) => {
543
- gltfLoader.load(url, gltf => {
544
- URL.revokeObjectURL(url);
545
- const newVrm = gltf.userData.vrm;
546
- if(!newVrm){ showErr('Invalid VRM'); setLoading(false); reject(); return; }
547
- try { VRMUtils.rotateVRM0(newVrm); } catch(e){}
548
-
549
- newVrm.scene.traverse(o => {
550
- if(o.isMesh){
551
- o.frustumCulled = false;
552
- o.castShadow = true;
553
- o.receiveShadow = true;
554
- }
555
- });
556
-
557
- if(vrm) { scene.remove(vrm.scene); }
558
- if(mixer){ mixer.stopAllAction(); mixer = null; vrmaAction = null; }
559
-
560
- vrm = newVrm;
561
- scene.add(vrm.scene);
562
- groundVrm();
563
- buildExprAlias();
564
-
565
- for(let i=0; i<6; i++) applyIdle(performance.now()*0.001, 0.95);
566
- vrm.update(0.016);
567
- renderer.compile(scene, camera);
568
- reframe();
569
-
570
- vrmLoadedOk = true;
571
- setLoading(false);
572
- showInfo('Model loaded ready.');
573
- resolve();
574
- }, undefined, reject);
575
- });
576
- } catch(e) {
577
- console.error(e);
578
- showErr('Failed to load local VRM model: ' + e.message);
579
- setLoading(false);
580
- }
581
- }
582
-
583
- function groundVrm(){
584
- if(!vrm) return;
585
- const box = new THREE.Box3().setFromObject(vrm.scene);
586
- const sz = box.getSize(new THREE.Vector3());
587
- const ctr = box.getCenter(new THREE.Vector3());
588
- const targetH = 1.65;
589
- const sc = targetH / Math.max(sz.y, 0.01);
590
- vrm.scene.scale.setScalar(sc);
591
-
592
- const box2 = new THREE.Box3().setFromObject(vrm.scene);
593
- vrmGroundY = -box2.min.y;
594
- const ctr2 = box2.getCenter(new THREE.Vector3());
595
- vrm.scene.position.set(-ctr2.x, vrmGroundY, 0);
596
- }
597
-
598
- function enforceGround(dt){
599
- if(!vrm) return;
600
- const isVrma = proceduralMotion === 'vrma' && vrmaAction && vrmaAction.isRunning();
601
- const speed = isVrma ? 3 : 10;
602
- vrm.scene.position.y += (vrmGroundY - vrm.scene.position.y) * speed * dt;
603
- }
604
-
605
- function reframe(){
606
- if(!vrm) return;
607
- const headNode = bone('head');
608
- if(headNode){
609
- const wp = new THREE.Vector3();
610
- headNode.getWorldPosition(wp);
611
- orbit.target.set(wp.x, wp.y - 0.05, wp.z);
612
- camera.position.set(wp.x, wp.y, wp.z + 1.2);
613
- }
614
- orbit.update();
615
- }
616
-
617
- /* ───────────── EXPRESSION MANAGEMENT ───────────── */
618
- function buildExprAlias(){
619
- if(!vrm) return;
620
- exprAlias = {};
621
- const names = vrm.expressionManager?.expressions?.map(e => e.name) || [];
622
- for(const target of EXPR_NAMES){
623
- const found = names.find(n => n.toLowerCase() === target.toLowerCase());
624
- if(found) exprAlias[target] = found;
625
- }
626
- }
627
-
628
- function setExpressionValue(name, val){
629
- if(!vrm?.expressionManager) return;
630
- const alias = exprAlias[name] || name;
631
- try { vrm.expressionManager.setValue(alias, val); } catch(e){}
632
- }
633
-
634
- function getExpressionValue(name){
635
- if(!vrm?.expressionManager) return 0;
636
- const alias = exprAlias[name] || name;
637
- try { return vrm.expressionManager.getValue(alias) || 0; } catch(e){ return 0; }
638
- }
639
-
640
- /* Highly expressive 25-state emotion mapper */
641
- function applyEmotion(emotion){
642
- for(const n of EXPR_NAMES) exprTgt[n] = 0;
643
-
644
- const presets = {
645
- neutral: { neutral: 1 },
646
- happy: { happy: 0.8 },
647
- sad: { sad: 0.8, lookDown: 0.1 },
648
- angry: { angry: 0.9 },
649
- surprised: { surprised: 0.9, lookUp: 0.1 },
650
- relaxed: { relaxed: 0.9 },
651
- excited: { happy: 0.7, surprised: 0.4 },
652
- bored: { sad: 0.4, lookDown: 0.3, relaxed: 0.2 },
653
- embarrassed: { sad: 0.3, relaxed: 0.4, lookDown: 0.2 },
654
- shy: { relaxed: 0.6, lookDown: 0.3 },
655
- smug: { happy: 0.3, angry: 0.1, relaxed: 0.4 },
656
- determined: { angry: 0.5, lookUp: 0.1 },
657
- sleepy: { relaxed: 0.8, lookDown: 0.4 },
658
- scared: { surprised: 0.7, sad: 0.3 },
659
- teasing: { happy: 0.5, lookDown: 0.2 },
660
- loving: { happy: 0.6, relaxed: 0.4 },
661
- disgusted: { angry: 0.4, sad: 0.3 },
662
- confident: { happy: 0.3, relaxed: 0.5 },
663
- curious: { surprised: 0.3, relaxed: 0.3 },
664
- proud: { happy: 0.5, lookUp: 0.1 },
665
- shocked: { surprised: 1.0 },
666
- thinking: { relaxed: 0.4, lookUp: 0.2 },
667
- wink: { blink: 0.8 },
668
- winkLeft: { blinkLeft: 1.0 },
669
- winkRight: { blinkRight: 1.0 }
670
- };
671
-
672
- const p = presets[emotion] || presets.neutral;
673
- for(const [k,v] of Object.entries(p)) {
674
- exprTgt[k] = v;
675
- }
676
- }
677
-
678
- /* ───────────── ANIMATION LOGICS (BLINK & LIP SYNC) ───────────── */
679
- function updateBlink(dt){
680
- if(!vrm) return;
681
-
682
- // Turn off auto-blink dynamically when a high-intensity non-neutral expression is active
683
- const isNeutralActive = (exprTgt['neutral'] || 0) > 0.8 ||
684
- (!exprTgt['happy'] && !exprTgt['sad'] && !exprTgt['angry'] && !exprTgt['surprised'] && !exprTgt['excited']);
685
-
686
- if(!isNeutralActive){
687
- let v = getExpressionValue('blink');
688
- if(v > 0){
689
- v = Math.max(0, v - 8 * dt);
690
- setExpressionValue('blink', v);
691
- }
692
- blinkV = 0; blinkT = 0;
693
- return;
694
- }
695
-
696
- blinkT += dt;
697
- if(blinkT > nextBlink){
698
- if(blinkV === 0) blinkV = 1;
699
- const sp = 12;
700
- if(blinkV === 1){
701
- let v = getExpressionValue('blink');
702
- v += sp*dt;
703
- if(v >= 1){ v=1; blinkV=-1; }
704
- setExpressionValue('blink', v);
705
- } else if(blinkV === -1){
706
- let v = getExpressionValue('blink');
707
- v -= sp*dt;
708
- if(v <= 0){ v=0; blinkV=0; blinkT=0; nextBlink=2+Math.random()*4; }
709
- setExpressionValue('blink', v);
710
- }
711
- }
712
- }
713
-
714
- function updateExpressions(dt){
715
- if(!vrm) return;
716
-
717
- // Update standard expressions
718
- for(const name of EXPR_NAMES){
719
- if(name.startsWith('blink') || VISEMES.includes(name)) continue;
720
- const tgt = exprTgt[name] || 0;
721
- let cur = exprCur[name] || 0;
722
- cur += (tgt - cur) * 6 * dt;
723
- exprCur[name] = cur;
724
- setExpressionValue(name, cur);
725
- }
726
-
727
- // Smooth Naturalistic Lip Sync update
728
- for(const v of VISEMES){
729
- let cur = currentVisemes[v] || 0;
730
- const tgt = targetVisemes[v] || 0;
731
- cur += (tgt - cur) * 12 * dt; // Smoothing acceleration
732
- currentVisemes[v] = cur;
733
- setExpressionValue(v, THREE.MathUtils.clamp(cur, 0, 1.2));
734
- }
735
- }
736
-
737
- /* ───────────── BONE ROTATION HANDLERS ───────────── */
738
- const d2r = THREE.MathUtils.degToRad;
739
- function bone(n){ return vrm?.humanoid?.getRawBoneNode(n) || null; }
740
- function lerpRot(node, tx, ty, tz, s){
741
- if(!node) return;
742
- node.rotation.x += (d2r(tx) - node.rotation.x) * s;
743
- node.rotation.y += (d2r(ty) - node.rotation.y) * s;
744
- node.rotation.z += (d2r(tz) - node.rotation.z) * s;
745
- }
746
-
747
- /* ───────────── IDLE CALCULATIONS ───────────── */
748
- const IL = 6.0;
749
- function smoothstep(a,b,f){ f=Math.max(0,Math.min(1,f)); f=f*f*(3-2*f); return a+(b-a)*f; }
750
- function kf(ts,vs,t){ let i=0; while(i<ts.length-1&&t>ts[i+1])i++; const t0=ts[i],t1=ts[Math.min(i+1,ts.length-1)]; const v0=vs[i],v1=vs[Math.min(i+1,vs.length-1)]; if(t1<=t0)return v1; return smoothstep(v0,v1,(t-t0)/(t1-t0)); }
751
-
752
- const BT=[0,1.2,2.4,3.6,4.8,6], BV=[0,.9,.4,1,.2,0];
753
- const ST=[0,1.5,3,4.5,6], SV=[0,1,0,-1,0];
754
- const WT=[0,2,4,6], WV=[0,.8,-.8,0];
755
- const MT=[0,.8,1.6,2.4,3.2,4,4.8,5.6,6], MV=[0,.4,-.3,.2,-.4,.3,-.2,.4,0];
756
-
757
- function applyIdle(t, blend=0.12){
758
- if(!vrm) return;
759
- const lt = t % IL;
760
- const br=kf(BT,BV,lt), sh=kf(ST,SV,lt), sw=kf(WT,WV,lt), mi=kf(MT,MV,lt);
761
- lerpRot(bone('hips'), -1+br*.4, sw*.5, sh*.6, blend);
762
- lerpRot(bone('spine'), 2+br*1.2, 0, -sh*.8, blend);
763
- lerpRot(bone('chest'), 1+br*1.6, 0, sh*.3, blend);
764
- lerpRot(bone('upperChest'), br*.8, 0, 0, blend);
765
- lerpRot(bone('leftUpperLeg'), -2, 0, 3+sh*.6, blend);
766
- lerpRot(bone('rightUpperLeg'), -2, 0, -3-sh*.6, blend);
767
- lerpRot(bone('leftLowerLeg'), 4, 0, 0, blend);
768
- lerpRot(bone('rightLowerLeg'), 4, 0, 0, blend);
769
- lerpRot(bone('leftFoot'), -2, 0, -sh*.5, blend);
770
- lerpRot(bone('rightFoot'), -2, 0, sh*.5, blend);
771
- lerpRot(bone('leftUpperArm'), 0, 0, -42+br*1.5, blend);
772
- lerpRot(bone('rightUpperArm'), 0, 0, 42-br*1.5, blend);
773
- lerpRot(bone('leftLowerArm'), 15+mi*2, 10, 0, blend);
774
- lerpRot(bone('rightLowerArm'), 15-mi*2, -10, 0, blend);
775
- lerpRot(bone('neck'), 1+sw*.3, sw*.4, 0, blend);
776
- lerpRot(bone('head'), -1-sw*.2, mi*.5, 0, blend);
777
- }
778
-
779
- function applyProceduralMotion(t, dt){
780
- if(!vrm || proceduralMotion === 'vrma') return;
781
- applyIdle(t, 0.08);
782
- }
783
-
784
- /* ───────────── VRMA PLAYER ENGINE ───────────── */
785
- function loadVrmaFromBlob(blob, name){
786
- if(!vrm) return;
787
- const url = URL.createObjectURL(blob);
788
- gltfLoader.load(url, gltf => {
789
- URL.revokeObjectURL(url);
790
- const anims = gltf.userData.vrmAnimations;
791
- if(!anims?.length) return;
792
-
793
- const clip = createVRMAnimationClip(anims[0], vrm);
794
- if(!mixer){
795
- mixer = new THREE.AnimationMixer(vrm.scene);
796
- mixer.addEventListener('loop', e => {
797
- if(vrmaAction && e.action === vrmaAction){
798
- vrmaLoopCount++;
799
- if(shouldFallbackToNeutral && vrmaLoopCount >= 1){
800
- shouldFallbackToNeutral = false;
801
- playDefaultNeutralVrma();
802
- } else if(!FALLBACK_VRMAS.some(f => vrmaAction.getClip().name.toLowerCase().includes(f.toLowerCase().replace('.vrma',''))) && vrmaLoopCount >= 1){
803
- playDefaultNeutralVrma();
804
- }
805
- }
806
- });
807
- }
808
-
809
- const next = mixer.clipAction(clip);
810
- next.reset();
811
- next.timeScale = 1;
812
- next.setLoop(THREE.LoopRepeat, Infinity);
813
- next.clampWhenFinished = false;
814
-
815
- if(vrmaAction){
816
- vrmaAction.crossFadeTo(next, 0.35, true);
817
- next.play();
818
- } else {
819
- next.play();
820
- }
821
- vrmaAction = next;
822
- proceduralMotion = 'vrma';
823
- vrmaLoopCount = 0;
824
-
825
- setTimeout(() => {
826
- if(vrm){
827
- vrm.scene.updateMatrixWorld(true);
828
- const box = new THREE.Box3().setFromObject(vrm.scene);
829
- vrmGroundY = -box.min.y;
830
- }
831
- }, 150);
832
-
833
- if(!firstVrmaLoaded){
834
- firstVrmaLoaded = true;
835
- setTimeout(reframe, 400);
836
- }
837
- }, null, err => {
838
- URL.revokeObjectURL(url);
839
- console.error('VRMA load failed:', name, err);
840
- });
841
- }
842
-
843
- function playDefaultNeutralVrma(){
844
- const targets = ['neutral2.vrma','neutral3.vrma','neutral_idle.vrma','neutral4.vrma'];
845
- for(const f of targets){
846
- const match = Object.keys(vrmaFiles).find(k => k.toLowerCase() === f.toLowerCase());
847
- if(match){ loadVrmaFromBlob(vrmaFiles[match], match); return; }
848
- }
849
- proceduralMotion = 'idle';
850
- }
851
-
852
- function playVrmaByName(name){
853
- const blob = vrmaFiles[name];
854
- if(blob) {
855
- loadVrmaFromBlob(blob, name);
856
- } else {
857
- playDefaultNeutralVrma();
858
- }
859
- }
860
-
861
- /* ───────────── AUTO-LOAD ANIMATION PACKS ───────────── */
862
- async function autoLoadAnimations(){
863
- setLoading(true, 'Loading animations...');
864
- try {
865
- const Zip = await ensureJSZip();
866
- const res = await fetchWithFallback('all_vrma.zip', 'animation');
867
- const blob = await res.blob();
868
- const zip = await Zip.loadAsync(blob);
869
-
870
- const promises = [];
871
- zip.forEach((path, entry) => {
872
- if(!entry.dir && path.toLowerCase().endsWith('.vrma')){
873
- const name = path.split('/').pop();
874
- promises.push(entry.async('blob').then(b => {
875
- vrmaFiles[name] = b;
876
- vrmaNames.push(name);
877
- }));
878
- }
879
- });
880
- await Promise.all(promises);
881
- showInfo(`${vrmaNames.length} animations loaded`);
882
- if(vrm) playDefaultNeutralVrma();
883
- } catch(e){
884
- console.warn('Fallback animation loader triggered:', e.message);
885
- try {
886
- const Zip = await ensureJSZip();
887
- const res = await fetch('https://raw.githubusercontent.com/unsloth/vrm/main/all_vrma.zip');
888
- if(!res.ok) throw new Error('Fallback failed');
889
- const blob = await res.blob();
890
- const zip = await Zip.loadAsync(blob);
891
- const promises = [];
892
- zip.forEach((path, entry) => {
893
- if(!entry.dir && path.toLowerCase().endsWith('.vrma')){
894
- const name = path.split('/').pop();
895
- promises.push(entry.async('blob').then(b => {
896
- vrmaFiles[name] = b;
897
- vrmaNames.push(name);
898
- }));
899
  }
900
- });
901
- await Promise.all(promises);
902
- showInfo(`${vrmaNames.length} fallback animations ready`);
903
- if(vrm) playDefaultNeutralVrma();
904
- } catch(err2) {
905
- console.error('Critical fallback failure:', err2);
906
- }
907
- }
908
- setLoading(false);
909
- }
910
-
911
- /* ───────────── SYSTEM AUDIO ENGINE (LIP SYNC) ───────────── */
912
- let audioCtx = null, analyser = null, dataArray = null, audioObj = null, currentAudioUrl = null;
913
-
914
- function initAudioCtx(){
915
- if(!audioCtx){
916
- audioCtx = new (window.AudioContext || window.webkitAudioContext)();
917
- analyser = audioCtx.createAnalyser();
918
- analyser.fftSize = 256;
919
- dataArray = new Uint8Array(analyser.frequencyBinCount);
920
- }
921
- }
922
-
923
- function playTTSAudio(blob){
924
- initAudioCtx();
925
- if(audioCtx?.state === 'suspended') audioCtx.resume();
926
- if(currentAudioUrl){ URL.revokeObjectURL(currentAudioUrl); currentAudioUrl = null; }
927
- if(audioObj){ audioObj.pause(); audioObj.src=''; audioObj=null; }
928
-
929
- const url = URL.createObjectURL(blob);
930
- currentAudioUrl = url;
931
- audioObj = new Audio(url);
932
- audioObj.crossOrigin = 'anonymous';
933
-
934
- const src = audioCtx.createMediaElementSource(audioObj);
935
- src.connect(analyser);
936
- analyser.connect(audioCtx.destination);
937
-
938
- speechActive = true; speaking = true;
939
- document.getElementById('thinkDot').classList.remove('on');
940
- document.getElementById('speakDot').classList.add('on');
941
-
942
- audioObj.play().catch(e => console.warn('Browser auto-play blocked:', e));
943
-
944
- audioObj.onended = () => {
945
- speechActive = false; speaking = false;
946
- document.getElementById('speakDot').classList.remove('on');
947
- if(currentAudioUrl){ URL.revokeObjectURL(currentAudioUrl); currentAudioUrl=null; }
948
- if(vrm) VISEMES.forEach(v => setExpressionValue(v, 0));
949
- applyEmotion('neutral');
950
- shouldFallbackToNeutral = true;
951
- };
952
- }
953
-
954
- window.addEventListener('click', () => {
955
- if(audioCtx?.state === 'suspended') audioCtx.resume();
956
- }, { once: false });
957
-
958
- /* ───────────── POINTER COORDINATES ───────────── */
959
- const pRaw={x:0,y:0}, pSmooth={x:0,y:0};
960
- let hasPointer = false;
961
- window.addEventListener('pointermove', e => {
962
- if(['INPUT','SELECT','TEXTAREA'].includes(e.target.tagName)) return;
963
- pRaw.x = (e.clientX/window.innerWidth)*2-1;
964
- pRaw.y = -(e.clientY/window.innerHeight)*2+1;
965
- hasPointer = true;
966
- }, { passive: true });
967
-
968
- /* ───────────── INTERACTION INTERCEPT (GRADIO HANDLER) ───────────── */
969
- window.handleAIResponse = (jsonData) => {
970
- if(!jsonData) return;
971
- try {
972
- const res = JSON.parse(jsonData);
973
- setLoading(false);
974
- document.getElementById('thinkDot').classList.remove('on');
975
 
976
- if(res.text && res.audio_b64){
977
- const binary = atob(res.audio_b64);
978
- const array = new Uint8Array(binary.length);
979
- for(let i=0; i<binary.length; i++){
980
- array[i] = binary.charCodeAt(i);
981
- }
982
- const blob = new Blob([array], { type: 'audio/mpeg' });
983
- playTTSAudio(blob);
984
- }
985
 
986
- if(res.expressions){
987
- applyEmotion(res.expressions);
988
- }
989
-
990
- if(res.vrma && vrmaFiles[res.vrma]){
991
- playVrmaByName(res.vrma);
992
- } else {
993
- playDefaultNeutralVrma();
994
- }
995
- } catch(e) {
996
- console.error('JSON sync execution failure:', e);
997
- showErr('Payload sync failed');
998
- setLoading(false);
999
- document.getElementById('thinkDot').classList.remove('on');
1000
- }
1001
- };
1002
-
1003
- /* Hook into UI submissions to display immediate frontend feedback */
1004
- function setupInputWatchers() {
1005
- const checkInterval = setInterval(() => {
1006
- const inputEl = document.querySelector("#gradio_ti textarea");
1007
- const btnEl = document.querySelector("#gradio_sb");
1008
 
1009
- if(inputEl && btnEl) {
1010
- clearInterval(checkInterval);
1011
- const setThinking = () => {
1012
- setLoading(true, "thinking...");
1013
- document.getElementById('thinkDot').classList.add('on');
1014
- };
1015
- btnEl.addEventListener("click", setThinking);
1016
- inputEl.addEventListener("keydown", (e) => {
1017
- if(e.key === "Enter" && !e.shiftKey) {
1018
- setThinking();
1019
- }
1020
- });
1021
- }
1022
- }, 100);
1023
- }
1024
-
1025
- /* ───────────── STATUS HELPERS ───────────── */
1026
- function setLoading(on, text='thinking...'){
1027
- const el = document.getElementById('load');
1028
- if(on){ el.textContent = text; el.classList.add('on'); }
1029
- else { el.classList.remove('on'); }
1030
- }
1031
-
1032
- function showErr(msg){
1033
- const el = document.getElementById('err');
1034
- el.textContent = msg; el.style.display = 'block';
1035
- setTimeout(() => el.style.display = 'none', 4000);
1036
- }
1037
-
1038
- function showInfo(msg, delay=2500){
1039
- const el = document.getElementById('info');
1040
- el.textContent = msg; el.style.display = 'block';
1041
- setTimeout(() => el.style.display = 'none', delay);
1042
- }
1043
-
1044
- /* ───────────── MAIN ANIMATION RENDER LOOP ───────────── */
1045
- const clock = new THREE.Clock();
1046
- const gazeTarget = new THREE.Vector3();
1047
-
1048
- function animate(){
1049
- requestAnimationFrame(animate);
1050
- const dt = Math.min(clock.getDelta(), 0.1);
1051
- const t = clock.getElapsedTime();
1052
-
1053
- orbit.update();
1054
-
1055
- if(vrm){
1056
- /* Smooth Gaze Tracking */
1057
- if(hasPointer){
1058
- pSmooth.x += (pRaw.x - pSmooth.x) * 4 * dt;
1059
- pSmooth.y += (pRaw.y - pSmooth.y) * 4 * dt;
1060
- gazeTarget.set(pSmooth.x * 1.5, pSmooth.y + 1.3, 2);
1061
- if(vrm.lookAt) vrm.lookAt.lookAt(gazeTarget);
1062
- }
1063
-
1064
- /* Natural Bandpass Lip Sync Audio Extraction */
1065
- if(speechActive && analyser && dataArray){
1066
- analyser.getByteFrequencyData(dataArray);
1067
-
1068
- let bass = 0, mid = 0, high = 0;
1069
- for (let i = 0; i < 4; i++) bass += dataArray[i]; // Low bass vocals
1070
- for (let i = 4; i < 16; i++) mid += dataArray[i]; // Conversational mids
1071
- for (let i = 16; i < 32; i++) high += dataArray[i]; // High sibilants
1072
-
1073
- bass = (bass / 4) / 255;
1074
- mid = (mid / 12) / 255;
1075
- high = (high / 16) / 255;
1076
-
1077
- const ampBass = Math.min(bass * 2.5, 1.0);
1078
- const ampMid = Math.min(mid * 2.5, 1.0);
1079
- const ampHigh = Math.min(high * 2.5, 1.0);
1080
-
1081
- // Dynamic frequency-to-viseme routing
1082
- targetVisemes.aa = ampMid * 1.3;
1083
- targetVisemes.ee = ampHigh * 0.9;
1084
- targetVisemes.ih = ampHigh * 0.6;
1085
- targetVisemes.oh = ampBass * 1.1;
1086
- targetVisemes.ou = ampBass * 0.8;
1087
-
1088
- // Mirror assignments
1089
- targetVisemes.a = targetVisemes.aa;
1090
- targetVisemes.e = targetVisemes.ee;
1091
- targetVisemes.i = targetVisemes.ih;
1092
- targetVisemes.o = targetVisemes.oh;
1093
- targetVisemes.u = targetVisemes.ou;
1094
- } else {
1095
- VISEMES.forEach(v => targetVisemes[v] = 0);
1096
- }
1097
-
1098
- applyProceduralMotion(t, dt);
1099
- if(mixer) mixer.update(dt);
1100
- enforceGround(dt);
1101
- updateBlink(dt);
1102
- updateExpressions(dt);
1103
- vrm.update(dt);
1104
- }
1105
-
1106
- renderer.render(scene, camera);
1107
- }
1108
-
1109
- /* ───────────── SYSTEM ENTRYPOINT ───────────── */
1110
- async function init(){
1111
- await loadVrmFromUrl();
1112
- await autoLoadAnimations();
1113
- setupInputWatchers();
1114
- animate();
1115
- }
1116
-
1117
- init();
1118
- </script>
1119
- """
1120
-
1121
- # ---------------------------------------------------------------------------
1122
- # Gradio Application Definition
1123
- # ---------------------------------------------------------------------------
1124
- # Title parameter retained; Blocks level custom styles refactored into demo.launch()
1125
- with gr.Blocks(title="AI VRM Companion") as demo:
1126
- # 98% Custom HTML Canvas container
1127
- gr.HTML(HTML_PAGE, elem_id="custom_html_overlay")
1128
-
1129
- # 2% Standard Gradio Interactive Controls
1130
- with gr.Row(elem_id="gradio_bar"):
1131
- text_input = gr.Textbox(
1132
- placeholder="Say something sweet...",
1133
- show_label=False,
1134
- container=False,
1135
- elem_id="gradio_ti"
1136
- )
1137
- send_btn = gr.Button("Send", elem_id="gradio_sb")
1138
-
1139
- # Invisible channel to synchronize Python generation and Javascript updates
1140
- hidden_json_output = gr.Textbox(visible=False, elem_id="hidden_json_output")
1141
 
1142
- # Wire actions securely
1143
- interaction_trigger = send_btn.click(
1144
- fn=process_chat,
1145
- inputs=[text_input],
1146
- outputs=[hidden_json_output]
1147
- ).then(
1148
- fn=None,
1149
- inputs=[hidden_json_output],
1150
- js="(jsonData) => { window.handleAIResponse(jsonData); return []; }"
1151
- )
1152
 
1153
- submission_trigger = text_input.submit(
1154
- fn=process_chat,
1155
- inputs=[text_input],
1156
- outputs=[hidden_json_output]
1157
- ).then(
1158
- fn=None,
1159
- inputs=[hidden_json_output],
1160
- js="(jsonData) => { window.handleAIResponse(jsonData); return []; }"
1161
- )
1162
 
1163
- # ---------------------------------------------------------------------------
1164
- # Gradio Launch Execution (Port binding and routing managed automatically)
1165
- # ---------------------------------------------------------------------------
1166
  if __name__ == "__main__":
1167
- demo.launch(
1168
- server_name="0.0.0.0",
1169
- server_port=7860,
1170
- css=CSS_STYLES, # Custom CSS rules applied here directly
1171
- allowed_paths=[MODEL_DIR, ANIM_DIR] # Access rights given dynamically to Gradio's internal engine
1172
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
+ import psutil
3
+ import os
4
+ import platform
5
+ import spaces # Hugging Face ZeroGPU এর জন্য প্রয়োজনীয় লাইব্রেরি
6
 
7
+ def get_system_specs():
8
+ """রিসোর্স এবং হার্ডওয়্যার ইনফরমেশন কালেক্ট করার ফাংশন"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  try:
10
+ # CPU এবং মেমরি গণনা
11
+ cpu_count = os.cpu_count() or "Unknown"
12
+ cpu_usage = psutil.cpu_percent(interval=0.5)
13
+ memory = psutil.virtual_memory()
14
 
15
+ # ডিস্ক স্পেস
16
+ disk = os.statvfs('/')
17
+ disk_free = (disk.f_bavail * disk.f_frsize) / (1024 ** 3)
18
+ disk_total = (disk.f_blocks * disk.f_frsize) / (1024 ** 3)
19
 
20
+ # এনভায়রনমেন্ট ডেটা অর্গানাইজ করা
21
+ specs = {
22
+ "OS Platform": platform.platform(),
23
+ "Total vCPU Cores": f"{cpu_count} Cores",
24
+ "Current CPU Usage": f"{cpu_usage}%",
25
+ "Total RAM": f"{memory.total / (1024 ** 3):.2f} GB",
26
+ "Available RAM": f"{memory.available / (1024 ** 3):.2f} GB",
27
+ "RAM Usage": f"{memory.percent}%",
28
+ "Container Storage Total": f"{disk_total:.2f} GB",
29
+ "Container Storage Free": f"{disk_free:.2f} GB"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
31
+ return specs
32
+ except Exception as e:
33
+ return {"Error": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
+ # Gradio ইন্টারফেস তৈরি
36
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
37
+ gr.Markdown("# 🖥️ Hugging Face ZeroGPU: CPU & Space Monitor")
38
+ gr.Markdown("এই ড্যাশবোর্ডটি ZeroGPU এনভায়রনমেন্টের ব্যাকগ্রাউন্ড CPU এবং মেমরি স্ট্যাটাস প্রদর্শন করে।")
 
 
 
 
 
39
 
40
+ with gr.Row():
41
+ btn_refresh = gr.Button("🔄 Refresh System Specs", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
+ with gr.Row():
44
+ # JSON ফরম্যাটে সমস্ত ডিটেইলস দেখানোর জন্য
45
+ output_json = gr.JSON(label="System Hardware & Resource Specifications")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
+ # বাটন ক্লিকের অ্যাকশন ডিফাইন করা
48
+ btn_refresh.click(fn=get_system_specs, inputs=None, outputs=output_json)
 
 
 
 
 
 
 
 
49
 
50
+ # অ্যাপ লোড হওয়ার সাথে সাথে ডেটা দেখানোর জন্য
51
+ demo.load(fn=get_system_specs, inputs=None, outputs=output_json)
 
 
 
 
 
 
 
52
 
53
+ # অ্যাপ লঞ্চ করা
 
 
54
  if __name__ == "__main__":
55
+ demo.launch()