OrbitMC commited on
Commit
a71972d
Β·
verified Β·
1 Parent(s): 269c854

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +115 -111
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
- # GGUF Imports
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
- # UNSLOTH GGUF CONFIG
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 (GGUF via llama.cpp)
77
  # ══════════════════════════════════════════════════════════════════
78
  print("=" * 60)
79
- print(" Visual AI -- Booting Systems (GGUF Fast Mode)")
80
  print("=" * 60)
81
 
82
- model = None
 
83
 
84
  try:
85
- print(f"[MODEL] Downloading/Locating {FILENAME} ...")
86
- model_path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME)
87
-
88
- print(f"[MODEL] Loading into RAM...")
89
- # Auto-detect best thread count
90
- threads = max(1, os.cpu_count() - 1) if os.cpu_count() else 4
91
-
92
- model = Llama(
93
- model_path=model_path,
94
- n_ctx=2048,
95
- n_threads=threads,
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
- outputs = model.create_chat_completion(
142
- messages=messages,
143
- max_tokens=MAX_NEW_TOKENS,
144
- temperature=0.85,
145
- top_k=40,
146
- top_p=0.90,
147
- repeat_penalty=1.1,
148
- stop=["<|im_end|>", "<|im_start|>"]
149
  )
150
- response = outputs["choices"][0]["message"]["content"].strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, Crossfade Image Switch, Background Music
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
- /* CROSSFADE IMAGE SETUP */
227
- .bg-img {
228
- position:absolute;
229
- inset:0;
 
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="bgImgB" class="bg-img" style="opacity: 0; z-index: 1;">
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
- // --- BACKGROUND MUSIC SYSTEM ---
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
- let activeBg = document.getElementById('bgImgA');
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`; // Preload
 
422
  });
423
  })
424
  .catch(err => console.warn('Could not load image list:', err));
425
 
426
- function smoothSwap(emotion) {
 
427
  const key = emotion.toLowerCase();
428
- const targetSrc = availableImages.has(key) ? `/img/${key}.png` : '/img/default.png';
429
-
430
- // Prevent crossfading to the image we are already looking at
431
- if (activeBg.getAttribute('src') === targetSrc || activeBg.src.endsWith(targetSrc)) return;
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) { smoothSwap('default'); return; }
454
  const queue = [...emotions];
455
  (function next() {
456
  if (!queue.length) return;
457
- smoothSwap(queue.shift());
458
- // Give enough time for the 0.6s CSS transition to finish before next emotion
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 to send list of available images to frontend
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": model is not None,
 
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__":