Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
| 19 |
-
|
| 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 |
-
#
|
| 244 |
-
|
|
|
|
|
|
|
| 245 |
|
| 246 |
-
#
|
| 247 |
-
|
|
|
|
|
|
|
| 248 |
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 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 |
-
|
| 902 |
-
|
| 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 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
array[i] = binary.charCodeAt(i);
|
| 981 |
-
}
|
| 982 |
-
const blob = new Blob([array], { type: 'audio/mpeg' });
|
| 983 |
-
playTTSAudio(blob);
|
| 984 |
-
}
|
| 985 |
|
| 986 |
-
|
| 987 |
-
|
| 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 |
-
|
| 1010 |
-
|
| 1011 |
-
|
| 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 |
-
#
|
| 1143 |
-
|
| 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 |
-
|
| 1154 |
-
|
| 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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|