Update app.py
Browse files
app.py
CHANGED
|
@@ -7,27 +7,20 @@ import traceback
|
|
| 7 |
import asyncio
|
| 8 |
from pathlib import Path
|
| 9 |
from flask import Flask, request, jsonify, send_from_directory, Response
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
from huggingface_hub import hf_hub_download
|
| 13 |
-
from llama_cpp import Llama
|
| 14 |
import edge_tts
|
| 15 |
|
| 16 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 17 |
# CONFIG
|
| 18 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 19 |
-
BASE_DIR = Path(__file__).parent
|
| 20 |
-
IMG_DIR = BASE_DIR / "img"
|
| 21 |
-
|
| 22 |
MAX_MEMORY = 20
|
| 23 |
MAX_NEW_TOKENS = int(os.environ.get("MAX_NEW_TOKENS", "300"))
|
| 24 |
TTS_VOICE = "zh-CN-XiaoyiNeural"
|
| 25 |
TTS_RATE = int(os.environ.get("TTS_RATE", "-4"))
|
| 26 |
TTS_PITCH = int(os.environ.get("TTS_PITCH", "7"))
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
REPO_ID = "unsloth/Qwen2.5-3B-Instruct-GGUF"
|
| 30 |
-
FILENAME = "qwen2.5-3b-instruct-q4_k_m.gguf"
|
| 31 |
|
| 32 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 33 |
# SYSTEM PROMPT
|
|
@@ -58,6 +51,7 @@ TTS FORMATTING:
|
|
| 58 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 59 |
# EMOTION TAG UTILITIES
|
| 60 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 61 |
EMOTION_RE = re.compile(r'\[([a-zA-Z_]+)\]')
|
| 62 |
|
| 63 |
def extract_emotions(text: str):
|
|
@@ -73,30 +67,31 @@ def clean_for_tts(text: str) -> str:
|
|
| 73 |
return clean
|
| 74 |
|
| 75 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 76 |
-
# MODEL LOADING
|
| 77 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 78 |
print("=" * 60)
|
| 79 |
-
print(" Visual AI -- Booting Systems
|
| 80 |
print("=" * 60)
|
| 81 |
|
| 82 |
-
|
|
|
|
| 83 |
|
| 84 |
try:
|
| 85 |
-
print(f"[MODEL]
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
n_gpu_layers=0, # Pure CPU inference (super fast for 3B/4B GGUF)
|
| 97 |
-
chat_format="chatml", # Qwen Native Format
|
| 98 |
-
verbose=False
|
| 99 |
)
|
|
|
|
|
|
|
|
|
|
| 100 |
print(" OK Model loaded successfully!")
|
| 101 |
except Exception as exc:
|
| 102 |
print(f" FAILED Model load error: {exc}")
|
|
@@ -122,8 +117,13 @@ def add_to_memory(sid: str, role: str, content: str):
|
|
| 122 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 123 |
# RESPONSE GENERATION
|
| 124 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
def generate_response(user_input: str, session_id: str) -> str:
|
| 126 |
-
if model is None:
|
| 127 |
return "[sad] My mind is offline right now. Please give me a moment."
|
| 128 |
|
| 129 |
memory = get_memory(session_id)
|
|
@@ -137,22 +137,66 @@ def generate_response(user_input: str, session_id: str) -> str:
|
|
| 137 |
})
|
| 138 |
messages.append({"role": "user", "content": user_input})
|
| 139 |
|
|
|
|
|
|
|
| 140 |
try:
|
| 141 |
-
|
| 142 |
-
messages
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
top_p=0.90,
|
| 147 |
-
repeat_penalty=1.1,
|
| 148 |
-
stop=["<|im_end|>", "<|im_start|>"]
|
| 149 |
)
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
except Exception as exc:
|
| 152 |
print(f"[GENERATE] Error: {exc}")
|
| 153 |
traceback.print_exc()
|
| 154 |
return "[sad] Something went wrong in my mind. Could you say that again?"
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
if not response or len(response) < 3:
|
| 157 |
response = "[thinking] I lost my train of thought. Could you say that again?"
|
| 158 |
|
|
@@ -192,7 +236,7 @@ def synthesize_speech(text: str, rate: int = 0, pitch: int = 0):
|
|
| 192 |
return base64.b64encode(audio).decode() if audio else None
|
| 193 |
|
| 194 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 195 |
-
# HTML -- Fast Loading,
|
| 196 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 197 |
HTML_PAGE = r"""<!DOCTYPE html>
|
| 198 |
<html lang="en">
|
|
@@ -221,18 +265,21 @@ body{
|
|
| 221 |
inset:0;
|
| 222 |
z-index:0;
|
| 223 |
background:#000;
|
|
|
|
|
|
|
|
|
|
| 224 |
}
|
| 225 |
|
| 226 |
-
/*
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
|
|
|
| 230 |
width:100%;
|
| 231 |
height:100%;
|
| 232 |
object-fit:contain;
|
| 233 |
object-position:center center;
|
| 234 |
display:block;
|
| 235 |
-
transition: opacity 0.6s ease-in-out;
|
| 236 |
}
|
| 237 |
|
| 238 |
#overlay{
|
|
@@ -362,8 +409,7 @@ body{
|
|
| 362 |
<body>
|
| 363 |
|
| 364 |
<div id="bg">
|
| 365 |
-
<img id="
|
| 366 |
-
<img id="bgImgA" class="bg-img" style="opacity: 1; z-index: 2;" src="/img/default.png" onerror="this.src='/img/default.png'">
|
| 367 |
</div>
|
| 368 |
|
| 369 |
<div id="overlay">
|
|
@@ -386,81 +432,48 @@ let busy = false, activeAudio = null;
|
|
| 386 |
const MA = document.getElementById('msgArea');
|
| 387 |
const MI = document.getElementById('msgIn');
|
| 388 |
const SB = document.getElementById('sendBtn');
|
|
|
|
| 389 |
|
| 390 |
-
//
|
| 391 |
-
let musicStarted = false;
|
| 392 |
-
const bgMusic = new Audio('/music.mp3');
|
| 393 |
-
bgMusic.volume = 0.12; // 12% Volume
|
| 394 |
-
bgMusic.loop = true;
|
| 395 |
-
|
| 396 |
-
function tryStartMusic() {
|
| 397 |
-
if (musicStarted) return;
|
| 398 |
-
bgMusic.play().then(() => {
|
| 399 |
-
musicStarted = true;
|
| 400 |
-
}).catch(err => {
|
| 401 |
-
// Fails silently if music.mp3 is 404 or autoplay is blocked
|
| 402 |
-
console.log("Background music skipped or not found:", err);
|
| 403 |
-
});
|
| 404 |
-
}
|
| 405 |
-
document.body.addEventListener('click', tryStartMusic, {once:true});
|
| 406 |
-
document.body.addEventListener('keydown', tryStartMusic, {once:true});
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
// --- IMAGE CROSSFADE SYSTEM ---
|
| 410 |
const availableImages = new Set();
|
| 411 |
-
|
| 412 |
-
let inactiveBg = document.getElementById('bgImgB');
|
| 413 |
|
|
|
|
| 414 |
fetch('/api/images')
|
| 415 |
.then(res => res.json())
|
| 416 |
.then(files => {
|
| 417 |
files.forEach(f => {
|
| 418 |
const name = f.toLowerCase();
|
| 419 |
availableImages.add(name);
|
|
|
|
| 420 |
const img = new Image();
|
| 421 |
-
img.src = `/img/${name}.png`; //
|
|
|
|
| 422 |
});
|
| 423 |
})
|
| 424 |
.catch(err => console.warn('Could not load image list:', err));
|
| 425 |
|
| 426 |
-
|
|
|
|
| 427 |
const key = emotion.toLowerCase();
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
// Load new image in the invisible layer behind
|
| 434 |
-
inactiveBg.src = targetSrc;
|
| 435 |
-
|
| 436 |
-
inactiveBg.onload = () => {
|
| 437 |
-
// Bring inactive to front, fade it in
|
| 438 |
-
inactiveBg.style.zIndex = "2";
|
| 439 |
-
inactiveBg.style.opacity = "1";
|
| 440 |
-
|
| 441 |
-
// Push active back, set it to fade out
|
| 442 |
-
activeBg.style.zIndex = "1";
|
| 443 |
-
activeBg.style.opacity = "0";
|
| 444 |
-
|
| 445 |
-
// Swap variables
|
| 446 |
-
let temp = activeBg;
|
| 447 |
-
activeBg = inactiveBg;
|
| 448 |
-
inactiveBg = temp;
|
| 449 |
-
};
|
| 450 |
}
|
| 451 |
|
| 452 |
function playImgSequence(emotions) {
|
| 453 |
-
if (!emotions || emotions.length === 0) {
|
| 454 |
const queue = [...emotions];
|
| 455 |
(function next() {
|
| 456 |
if (!queue.length) return;
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
if (queue.length) setTimeout(next, 1200);
|
| 460 |
})();
|
| 461 |
}
|
| 462 |
|
| 463 |
-
/* Parse emotion tags */
|
| 464 |
function parseResponse(raw) {
|
| 465 |
const tagRe = /\[([a-zA-Z_]+)\]/g;
|
| 466 |
const emotions = [];
|
|
@@ -545,7 +558,6 @@ async function send() {
|
|
| 545 |
}
|
| 546 |
|
| 547 |
busy = false; SB.disabled = false;
|
| 548 |
-
MI.focus();
|
| 549 |
}
|
| 550 |
|
| 551 |
MI.addEventListener('keydown', e => {
|
|
@@ -564,15 +576,15 @@ app = Flask(__name__)
|
|
| 564 |
def index():
|
| 565 |
return Response(HTML_PAGE, mimetype="text/html")
|
| 566 |
|
| 567 |
-
# API
|
| 568 |
@app.route("/api/images")
|
| 569 |
def api_images():
|
| 570 |
if not IMG_DIR.exists():
|
| 571 |
return jsonify([])
|
|
|
|
| 572 |
files = [f.stem for f in IMG_DIR.glob("*.png")]
|
| 573 |
return jsonify(files)
|
| 574 |
|
| 575 |
-
# Serve PNG Images
|
| 576 |
@app.route("/img/<path:filename>")
|
| 577 |
def serve_img(filename: str):
|
| 578 |
safe = Path(filename).name
|
|
@@ -580,21 +592,13 @@ def serve_img(filename: str):
|
|
| 580 |
if target.exists() and target.is_file():
|
| 581 |
return send_from_directory(str(IMG_DIR), safe)
|
| 582 |
|
|
|
|
| 583 |
fallback = IMG_DIR / "default.png"
|
| 584 |
if fallback.exists() and fallback.is_file():
|
| 585 |
return send_from_directory(str(IMG_DIR), "default.png")
|
| 586 |
|
| 587 |
return Response("", status=404)
|
| 588 |
|
| 589 |
-
# Serve Optional Background Music
|
| 590 |
-
@app.route("/music.mp3")
|
| 591 |
-
def serve_music():
|
| 592 |
-
music_file = BASE_DIR / "music.mp3"
|
| 593 |
-
if music_file.exists() and music_file.is_file():
|
| 594 |
-
return send_from_directory(str(BASE_DIR), "music.mp3")
|
| 595 |
-
return Response("Music file not found, skipping gracefully.", status=404)
|
| 596 |
-
|
| 597 |
-
# Chat Endpoint
|
| 598 |
@app.route("/chat", methods=["POST"])
|
| 599 |
def chat():
|
| 600 |
data = request.json or {}
|
|
@@ -610,7 +614,6 @@ def chat():
|
|
| 610 |
resp = "[sad] I encountered an unexpected error. Please try again."
|
| 611 |
return jsonify({"response": resp, "session_id": session_id})
|
| 612 |
|
| 613 |
-
# Voice TTS Endpoint
|
| 614 |
@app.route("/tts", methods=["POST"])
|
| 615 |
def tts_endpoint():
|
| 616 |
data = request.json or {}
|
|
@@ -633,7 +636,8 @@ def clear():
|
|
| 633 |
@app.route("/health")
|
| 634 |
def health():
|
| 635 |
return jsonify({
|
| 636 |
-
"model_loaded":
|
|
|
|
| 637 |
})
|
| 638 |
|
| 639 |
if __name__ == "__main__":
|
|
|
|
| 7 |
import asyncio
|
| 8 |
from pathlib import Path
|
| 9 |
from flask import Flask, request, jsonify, send_from_directory, Response
|
| 10 |
+
import torch
|
| 11 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
|
|
|
|
|
|
| 12 |
import edge_tts
|
| 13 |
|
| 14 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
# CONFIG
|
| 16 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
| 17 |
MAX_MEMORY = 20
|
| 18 |
MAX_NEW_TOKENS = int(os.environ.get("MAX_NEW_TOKENS", "300"))
|
| 19 |
TTS_VOICE = "zh-CN-XiaoyiNeural"
|
| 20 |
TTS_RATE = int(os.environ.get("TTS_RATE", "-4"))
|
| 21 |
TTS_PITCH = int(os.environ.get("TTS_PITCH", "7"))
|
| 22 |
+
IMG_DIR = Path(__file__).parent / "img"
|
| 23 |
+
MODEL_ID = "Qwen/Qwen2.5-1.5B-Instruct"
|
|
|
|
|
|
|
| 24 |
|
| 25 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
# SYSTEM PROMPT
|
|
|
|
| 51 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 52 |
# EMOTION TAG UTILITIES
|
| 53 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 54 |
+
# Now fully supports underscores (e.g. [lite_sad])
|
| 55 |
EMOTION_RE = re.compile(r'\[([a-zA-Z_]+)\]')
|
| 56 |
|
| 57 |
def extract_emotions(text: str):
|
|
|
|
| 67 |
return clean
|
| 68 |
|
| 69 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 70 |
+
# MODEL LOADING
|
| 71 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 72 |
print("=" * 60)
|
| 73 |
+
print(" Visual AI -- Booting Systems")
|
| 74 |
print("=" * 60)
|
| 75 |
|
| 76 |
+
tokenizer = None
|
| 77 |
+
model = None
|
| 78 |
|
| 79 |
try:
|
| 80 |
+
print(f"[MODEL] Loading {MODEL_ID} ...")
|
| 81 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 82 |
+
MODEL_ID,
|
| 83 |
+
trust_remote_code=True,
|
| 84 |
+
)
|
| 85 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 86 |
+
MODEL_ID,
|
| 87 |
+
dtype=torch.float32,
|
| 88 |
+
device_map="cpu",
|
| 89 |
+
trust_remote_code=True,
|
| 90 |
+
low_cpu_mem_usage=True,
|
|
|
|
|
|
|
|
|
|
| 91 |
)
|
| 92 |
+
model.eval()
|
| 93 |
+
if tokenizer.pad_token_id is None:
|
| 94 |
+
tokenizer.pad_token_id = tokenizer.eos_token_id
|
| 95 |
print(" OK Model loaded successfully!")
|
| 96 |
except Exception as exc:
|
| 97 |
print(f" FAILED Model load error: {exc}")
|
|
|
|
| 117 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 118 |
# RESPONSE GENERATION
|
| 119 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 120 |
+
STOP_TOKENS = [
|
| 121 |
+
"<end_of_turn>", "<start_of_turn>",
|
| 122 |
+
"Tur:", "User:", "<|endoftext|>", "[/INST]",
|
| 123 |
+
]
|
| 124 |
+
|
| 125 |
def generate_response(user_input: str, session_id: str) -> str:
|
| 126 |
+
if model is None or tokenizer is None:
|
| 127 |
return "[sad] My mind is offline right now. Please give me a moment."
|
| 128 |
|
| 129 |
memory = get_memory(session_id)
|
|
|
|
| 137 |
})
|
| 138 |
messages.append({"role": "user", "content": user_input})
|
| 139 |
|
| 140 |
+
input_ids = None
|
| 141 |
+
attention_mask = None
|
| 142 |
try:
|
| 143 |
+
enc = tokenizer.apply_chat_template(
|
| 144 |
+
messages,
|
| 145 |
+
return_tensors="pt",
|
| 146 |
+
add_generation_prompt=True,
|
| 147 |
+
return_dict=True,
|
|
|
|
|
|
|
|
|
|
| 148 |
)
|
| 149 |
+
input_ids = enc["input_ids"].to("cpu")
|
| 150 |
+
attention_mask = enc.get("attention_mask")
|
| 151 |
+
if attention_mask is not None:
|
| 152 |
+
attention_mask = attention_mask.to("cpu")
|
| 153 |
+
except Exception as e1:
|
| 154 |
+
print(f"[TOKENISE] chat_template failed ({e1}), using fallback")
|
| 155 |
+
try:
|
| 156 |
+
parts = [f"System: {SYSTEM_PROMPT}"]
|
| 157 |
+
for msg in recent:
|
| 158 |
+
label = "Tur" if msg["role"] == "user" else "Ana"
|
| 159 |
+
parts.append(f"{label}: {msg['content']}")
|
| 160 |
+
parts.append(f"Tur: {user_input}\nAna:")
|
| 161 |
+
enc = tokenizer("\n".join(parts), return_tensors="pt")
|
| 162 |
+
input_ids = enc["input_ids"].to("cpu")
|
| 163 |
+
attention_mask = enc.get("attention_mask")
|
| 164 |
+
if attention_mask is not None:
|
| 165 |
+
attention_mask = attention_mask.to("cpu")
|
| 166 |
+
except Exception as e2:
|
| 167 |
+
print(f"[TOKENISE] fallback failed: {e2}")
|
| 168 |
+
return "[sad] I could not process that. Please try again."
|
| 169 |
+
|
| 170 |
+
try:
|
| 171 |
+
gen_kwargs = dict(
|
| 172 |
+
max_new_tokens=MAX_NEW_TOKENS,
|
| 173 |
+
do_sample=True,
|
| 174 |
+
temperature=0.90,
|
| 175 |
+
top_k=50,
|
| 176 |
+
top_p=0.95,
|
| 177 |
+
repetition_penalty=1.1,
|
| 178 |
+
pad_token_id=tokenizer.eos_token_id,
|
| 179 |
+
)
|
| 180 |
+
if attention_mask is not None:
|
| 181 |
+
gen_kwargs["attention_mask"] = attention_mask
|
| 182 |
+
|
| 183 |
+
with torch.no_grad():
|
| 184 |
+
outputs = model.generate(input_ids, **gen_kwargs)
|
| 185 |
except Exception as exc:
|
| 186 |
print(f"[GENERATE] Error: {exc}")
|
| 187 |
traceback.print_exc()
|
| 188 |
return "[sad] Something went wrong in my mind. Could you say that again?"
|
| 189 |
|
| 190 |
+
new_tokens = outputs[0][input_ids.shape[-1]:]
|
| 191 |
+
response = tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
|
| 192 |
+
|
| 193 |
+
for stop in STOP_TOKENS:
|
| 194 |
+
if stop in response:
|
| 195 |
+
response = response.split(stop)[0].strip()
|
| 196 |
+
|
| 197 |
+
if "\n\n" in response:
|
| 198 |
+
response = response.split("\n\n")[0].strip()
|
| 199 |
+
|
| 200 |
if not response or len(response) < 3:
|
| 201 |
response = "[thinking] I lost my train of thought. Could you say that again?"
|
| 202 |
|
|
|
|
| 236 |
return base64.b64encode(audio).decode() if audio else None
|
| 237 |
|
| 238 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 239 |
+
# HTML -- Fast Loading, Instant Swap, Contain Image View
|
| 240 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 241 |
HTML_PAGE = r"""<!DOCTYPE html>
|
| 242 |
<html lang="en">
|
|
|
|
| 265 |
inset:0;
|
| 266 |
z-index:0;
|
| 267 |
background:#000;
|
| 268 |
+
display:flex;
|
| 269 |
+
align-items:center;
|
| 270 |
+
justify-content:center;
|
| 271 |
}
|
| 272 |
|
| 273 |
+
/*
|
| 274 |
+
object-fit: contain prevents cuts/overflow and displays the full image intact.
|
| 275 |
+
No transitions = INSTANT image swapping.
|
| 276 |
+
*/
|
| 277 |
+
#bgImg{
|
| 278 |
width:100%;
|
| 279 |
height:100%;
|
| 280 |
object-fit:contain;
|
| 281 |
object-position:center center;
|
| 282 |
display:block;
|
|
|
|
| 283 |
}
|
| 284 |
|
| 285 |
#overlay{
|
|
|
|
| 409 |
<body>
|
| 410 |
|
| 411 |
<div id="bg">
|
| 412 |
+
<img id="bgImg" src="/img/default.png" alt="" onerror="this.src='/img/default.png'">
|
|
|
|
| 413 |
</div>
|
| 414 |
|
| 415 |
<div id="overlay">
|
|
|
|
| 432 |
const MA = document.getElementById('msgArea');
|
| 433 |
const MI = document.getElementById('msgIn');
|
| 434 |
const SB = document.getElementById('sendBtn');
|
| 435 |
+
const BG = document.getElementById('bgImg');
|
| 436 |
|
| 437 |
+
// Background Image Preloading System
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
const availableImages = new Set();
|
| 439 |
+
const imageCache = {};
|
|
|
|
| 440 |
|
| 441 |
+
// 1. Fetch available images from the server and preload them into browser memory
|
| 442 |
fetch('/api/images')
|
| 443 |
.then(res => res.json())
|
| 444 |
.then(files => {
|
| 445 |
files.forEach(f => {
|
| 446 |
const name = f.toLowerCase();
|
| 447 |
availableImages.add(name);
|
| 448 |
+
|
| 449 |
const img = new Image();
|
| 450 |
+
img.src = `/img/${name}.png`; // Pre-cache request
|
| 451 |
+
imageCache[name] = img;
|
| 452 |
});
|
| 453 |
})
|
| 454 |
.catch(err => console.warn('Could not load image list:', err));
|
| 455 |
|
| 456 |
+
// 2. Instant swap logic (No transition delays, loaded instantly from browser memory)
|
| 457 |
+
function instantSwap(emotion) {
|
| 458 |
const key = emotion.toLowerCase();
|
| 459 |
+
if (availableImages.has(key)) {
|
| 460 |
+
BG.src = `/img/${key}.png`;
|
| 461 |
+
} else {
|
| 462 |
+
BG.src = '/img/default.png'; // Fallback
|
| 463 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
}
|
| 465 |
|
| 466 |
function playImgSequence(emotions) {
|
| 467 |
+
if (!emotions || emotions.length === 0) { instantSwap('default'); return; }
|
| 468 |
const queue = [...emotions];
|
| 469 |
(function next() {
|
| 470 |
if (!queue.length) return;
|
| 471 |
+
instantSwap(queue.shift());
|
| 472 |
+
if (queue.length) setTimeout(next, 750); // Pause briefly between multiple emotions
|
|
|
|
| 473 |
})();
|
| 474 |
}
|
| 475 |
|
| 476 |
+
/* Parse emotion tags (Fully supports underscores) */
|
| 477 |
function parseResponse(raw) {
|
| 478 |
const tagRe = /\[([a-zA-Z_]+)\]/g;
|
| 479 |
const emotions = [];
|
|
|
|
| 558 |
}
|
| 559 |
|
| 560 |
busy = false; SB.disabled = false;
|
|
|
|
| 561 |
}
|
| 562 |
|
| 563 |
MI.addEventListener('keydown', e => {
|
|
|
|
| 576 |
def index():
|
| 577 |
return Response(HTML_PAGE, mimetype="text/html")
|
| 578 |
|
| 579 |
+
# Preload API for the frontend
|
| 580 |
@app.route("/api/images")
|
| 581 |
def api_images():
|
| 582 |
if not IMG_DIR.exists():
|
| 583 |
return jsonify([])
|
| 584 |
+
# Find all png files and return their filenames without extension
|
| 585 |
files = [f.stem for f in IMG_DIR.glob("*.png")]
|
| 586 |
return jsonify(files)
|
| 587 |
|
|
|
|
| 588 |
@app.route("/img/<path:filename>")
|
| 589 |
def serve_img(filename: str):
|
| 590 |
safe = Path(filename).name
|
|
|
|
| 592 |
if target.exists() and target.is_file():
|
| 593 |
return send_from_directory(str(IMG_DIR), safe)
|
| 594 |
|
| 595 |
+
# Safely fallback to default.png if specific image is missing server-side
|
| 596 |
fallback = IMG_DIR / "default.png"
|
| 597 |
if fallback.exists() and fallback.is_file():
|
| 598 |
return send_from_directory(str(IMG_DIR), "default.png")
|
| 599 |
|
| 600 |
return Response("", status=404)
|
| 601 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
@app.route("/chat", methods=["POST"])
|
| 603 |
def chat():
|
| 604 |
data = request.json or {}
|
|
|
|
| 614 |
resp = "[sad] I encountered an unexpected error. Please try again."
|
| 615 |
return jsonify({"response": resp, "session_id": session_id})
|
| 616 |
|
|
|
|
| 617 |
@app.route("/tts", methods=["POST"])
|
| 618 |
def tts_endpoint():
|
| 619 |
data = request.json or {}
|
|
|
|
| 636 |
@app.route("/health")
|
| 637 |
def health():
|
| 638 |
return jsonify({
|
| 639 |
+
"model_loaded": model is not None,
|
| 640 |
+
"tokenizer_loaded": tokenizer is not None,
|
| 641 |
})
|
| 642 |
|
| 643 |
if __name__ == "__main__":
|