openfree commited on
Commit
e80ec12
ยท
verified ยท
1 Parent(s): c872798

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +85 -39
app.py CHANGED
@@ -1,8 +1,12 @@
 
 
 
1
  #!/usr/bin/env python
2
 
3
  import os
4
  import re
5
  import tempfile
 
6
  from collections.abc import Iterator
7
  from threading import Thread
8
  import json
@@ -20,6 +24,15 @@ import pandas as pd
20
  # PDF ํ…์ŠคํŠธ ์ถ”์ถœ
21
  import PyPDF2
22
 
 
 
 
 
 
 
 
 
 
23
  ##############################################################################
24
  # SERPHouse API key from environment variable
25
  ##############################################################################
@@ -122,13 +135,11 @@ def do_web_search(query: str) -> str:
122
  # ๋ชจ๋ธ์—๊ฒŒ ๋ช…ํ™•ํ•œ ์ง€์นจ ์ถ”๊ฐ€
123
  instructions = """
124
  # ์›น ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ
125
-
126
  ์•„๋ž˜๋Š” ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค. ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•  ๋•Œ ์ด ์ •๋ณด๋ฅผ ํ™œ์šฉํ•˜์„ธ์š”:
127
  1. ๊ฐ ๊ฒฐ๊ณผ์˜ ์ œ๋ชฉ, ๋‚ด์šฉ, ์ถœ์ฒ˜ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”
128
  2. ๋‹ต๋ณ€์— ๊ด€๋ จ ์ •๋ณด์˜ ์ถœ์ฒ˜๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ธ์šฉํ•˜์„ธ์š” (์˜ˆ: "X ์ถœ์ฒ˜์— ๋”ฐ๋ฅด๋ฉด...")
129
  3. ์‘๋‹ต์— ์‹ค์ œ ์ถœ์ฒ˜ ๋งํฌ๋ฅผ ํฌํ•จํ•˜์„ธ์š”
130
  4. ์—ฌ๋Ÿฌ ์ถœ์ฒ˜์˜ ์ •๋ณด๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ ๋‹ต๋ณ€ํ•˜์„ธ์š”
131
-
132
  """
133
 
134
  search_results = instructions + "\n".join(summary_lines)
@@ -144,14 +155,15 @@ def do_web_search(query: str) -> str:
144
  # ๋ชจ๋ธ/ํ”„๋กœ์„ธ์„œ ๋กœ๋”ฉ
145
  ##############################################################################
146
  MAX_CONTENT_CHARS = 4000
147
- model_id = os.getenv("MODEL_ID", "VIDraft/Gemma3-R1945-27B")
 
148
 
149
  processor = AutoProcessor.from_pretrained(model_id, padding_side="left")
150
  model = Gemma3ForConditionalGeneration.from_pretrained(
151
  model_id,
152
  device_map="auto",
153
  torch_dtype=torch.bfloat16,
154
- attn_implementation="eager"
155
  )
156
  MAX_NUM_IMAGES = int(os.getenv("MAX_NUM_IMAGES", "5"))
157
 
@@ -284,7 +296,7 @@ def validate_media_constraints(message: dict, history: list[dict]) -> bool:
284
 
285
 
286
  ##############################################################################
287
- # ๋น„๋””์˜ค ์ฒ˜๋ฆฌ
288
  ##############################################################################
289
  def downsample_video(video_path: str) -> list[tuple[Image.Image, float]]:
290
  vidcap = cv2.VideoCapture(video_path)
@@ -298,6 +310,8 @@ def downsample_video(video_path: str) -> list[tuple[Image.Image, float]]:
298
  success, image = vidcap.read()
299
  if success:
300
  image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
 
 
301
  pil_image = Image.fromarray(image)
302
  timestamp = round(i / fps, 2)
303
  frames.append((pil_image, timestamp))
@@ -308,17 +322,20 @@ def downsample_video(video_path: str) -> list[tuple[Image.Image, float]]:
308
  return frames
309
 
310
 
311
- def process_video(video_path: str) -> list[dict]:
312
  content = []
 
 
313
  frames = downsample_video(video_path)
314
  for frame in frames:
315
  pil_image, timestamp = frame
316
  with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
317
  pil_image.save(temp_file.name)
 
318
  content.append({"type": "text", "text": f"Frame {timestamp}:"})
319
  content.append({"type": "image", "url": temp_file.name})
320
- logger.debug(f"{content=}")
321
- return content
322
 
323
 
324
  ##############################################################################
@@ -360,9 +377,11 @@ def is_document_file(file_path: str) -> bool:
360
  )
361
 
362
 
363
- def process_new_user_message(message: dict) -> list[dict]:
 
 
364
  if not message["files"]:
365
- return [{"type": "text", "text": message["text"]}]
366
 
367
  video_files = [f for f in message["files"] if is_video_file(f)]
368
  image_files = [f for f in message["files"] if is_image_file(f)]
@@ -385,19 +404,21 @@ def process_new_user_message(message: dict) -> list[dict]:
385
  content_list.append({"type": "text", "text": pdf_markdown})
386
 
387
  if video_files:
388
- content_list += process_video(video_files[0])
389
- return content_list
 
 
390
 
391
  if "<image>" in message["text"] and image_files:
392
  interleaved_content = process_interleaved_images({"text": message["text"], "files": image_files})
393
  if content_list and content_list[0]["type"] == "text":
394
  content_list = content_list[1:]
395
- return interleaved_content + content_list
396
  else:
397
  for img_path in image_files:
398
  content_list.append({"type": "image", "url": img_path})
399
 
400
- return content_list
401
 
402
 
403
  ##############################################################################
@@ -429,6 +450,25 @@ def process_history(history: list[dict]) -> list[dict]:
429
  return messages
430
 
431
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  ##############################################################################
433
  # ๋ฉ”์ธ ์ถ”๋ก  ํ•จ์ˆ˜ (web search ์ฒดํฌ ์‹œ ์ž๋™ ํ‚ค์›Œ๋“œ์ถ”์ถœ->๊ฒ€์ƒ‰->๊ฒฐ๊ณผ system msg)
434
  ##############################################################################
@@ -446,6 +486,8 @@ def run(
446
  yield ""
447
  return
448
 
 
 
449
  try:
450
  combined_system_msg = ""
451
 
@@ -481,7 +523,9 @@ def run(
481
 
482
  messages.extend(process_history(history))
483
 
484
- user_content = process_new_user_message(message)
 
 
485
  for item in user_content:
486
  if item["type"] == "text" and len(item["text"]) > MAX_CONTENT_CHARS:
487
  item["text"] = item["text"][:MAX_CONTENT_CHARS] + "\n...(truncated)..."
@@ -494,7 +538,13 @@ def run(
494
  return_dict=True,
495
  return_tensors="pt",
496
  ).to(device=model.device, dtype=torch.bfloat16)
497
-
 
 
 
 
 
 
498
  streamer = TextIteratorStreamer(processor, timeout=30.0, skip_prompt=True, skip_special_tokens=True)
499
  gen_kwargs = dict(
500
  inputs,
@@ -513,22 +563,24 @@ def run(
513
  except Exception as e:
514
  logger.error(f"Error in run: {str(e)}")
515
  yield f"์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
516
-
517
-
518
- ##############################################################################
519
- # [์ถ”๊ฐ€] ๋ณ„๋„ ํ•จ์ˆ˜์—์„œ model.generate(...)๋ฅผ ํ˜ธ์ถœ, OOM ์บ์น˜
520
- ##############################################################################
521
- def _model_gen_with_oom_catch(**kwargs):
522
- """
523
- ๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ OutOfMemoryError๋ฅผ ์žก์•„์ฃผ๊ธฐ ์œ„ํ•ด
524
- """
525
- try:
526
- model.generate(**kwargs)
527
- except torch.cuda.OutOfMemoryError:
528
- raise RuntimeError(
529
- "[OutOfMemoryError] GPU ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. "
530
- "Max New Tokens์„ ์ค„์ด๊ฑฐ๋‚˜, ํ”„๋กฌํ”„ํŠธ ๊ธธ์ด๋ฅผ ์ค„์—ฌ์ฃผ์„ธ์š”."
531
- )
 
 
532
 
533
 
534
  ##############################################################################
@@ -632,12 +684,10 @@ css = """
632
  width: 100% !important;
633
  max-width: none !important; /* 1200px ์ œํ•œ ์ œ๊ฑฐ */
634
  }
635
-
636
  .fillable {
637
  width: 100% !important;
638
  max-width: 100% !important;
639
  }
640
-
641
  /* 2) ๋ฐฐ๊ฒฝ์„ ์—ฐํ•˜๊ณ  ํˆฌ๋ช…ํ•œ ํŒŒ์Šคํ…” ํ†ค ๊ทธ๋ผ๋””์–ธํŠธ๋กœ ๋ณ€๊ฒฝ */
642
  body {
643
  background: #f5f5f5; /* ๊ทธ๋ผ๋””์–ธํŠธ ๋Œ€์‹  ๋‹จ์ƒ‰ ์‚ฌ์šฉ */
@@ -646,7 +696,6 @@ body {
646
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
647
  color: #333;
648
  }
649
-
650
  /* ๋ฒ„ํŠผ ์ƒ‰์ƒ๋„ ๊ธฐ์กด์˜ ์ง™์€ ๋ถ‰์€-์ฃผํ™ฉ โ†’ ํŒŒ์Šคํ…” ๊ณ„์—ด๋กœ ์—ฐ๏ฟฝ๏ฟฝ๏ฟฝ๊ฒŒ */
651
  button, .btn {
652
  background: #ffb6c1 !important; /* ๊ทธ๋ผ๋””์–ธํŠธ ๋Œ€์‹  ๋‹จ์ƒ‰ ์‚ฌ์šฉ */
@@ -660,24 +709,21 @@ button, .btn {
660
  cursor: pointer;
661
  /* transition: transform 0.2s ease-in-out; - ํ˜น์‹œ ๋ชจ๋ฅผ ๋ฌธ์ œ ๋ถ€๋ถ„ ์ œ๊ฑฐ */
662
  }
663
-
664
  button:hover, .btn:hover {
665
  /* transform: scale(1.03); - ํ˜น์‹œ ๋ชจ๋ฅผ ๋ฌธ์ œ ๋ถ€๋ถ„ ์ œ๊ฑฐ */
666
  background: #ff69b4 !important;
667
  }
668
-
669
  #examples_container {
670
  margin: auto;
671
  width: 90%;
672
  }
673
-
674
  #examples_row {
675
  justify-content: center;
676
  }
677
  """
678
 
679
  title_html = """
680
- <h1 align="center" style="margin-bottom: 0.2em; font-size: 1.6em;"> ๐Ÿค— Gemma3-uncensored-R27B </h1>
681
  <p align="center" style="font-size:1.1em; color:#555;">
682
  โœ…Agentic AI Platform โœ…Reasoning & Uncensored โœ…Multimodal & VLM โœ…Deep-Research & RAG <br>
683
  Operates on an โœ…'NVIDIA A100 GPU' as an independent local server, enhancing security and preventing information leakage.<br>
 
1
+
2
+
3
+
4
  #!/usr/bin/env python
5
 
6
  import os
7
  import re
8
  import tempfile
9
+ import gc # garbage collector ์ถ”๊ฐ€
10
  from collections.abc import Iterator
11
  from threading import Thread
12
  import json
 
24
  # PDF ํ…์ŠคํŠธ ์ถ”์ถœ
25
  import PyPDF2
26
 
27
+ ##############################################################################
28
+ # ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ ํ•จ์ˆ˜ ์ถ”๊ฐ€
29
+ ##############################################################################
30
+ def clear_cuda_cache():
31
+ """CUDA ์บ์‹œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋น„์›๋‹ˆ๋‹ค."""
32
+ if torch.cuda.is_available():
33
+ torch.cuda.empty_cache()
34
+ gc.collect()
35
+
36
  ##############################################################################
37
  # SERPHouse API key from environment variable
38
  ##############################################################################
 
135
  # ๋ชจ๋ธ์—๊ฒŒ ๋ช…ํ™•ํ•œ ์ง€์นจ ์ถ”๊ฐ€
136
  instructions = """
137
  # ์›น ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ
 
138
  ์•„๋ž˜๋Š” ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค. ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•  ๋•Œ ์ด ์ •๋ณด๋ฅผ ํ™œ์šฉํ•˜์„ธ์š”:
139
  1. ๊ฐ ๊ฒฐ๊ณผ์˜ ์ œ๋ชฉ, ๋‚ด์šฉ, ์ถœ์ฒ˜ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”
140
  2. ๋‹ต๋ณ€์— ๊ด€๋ จ ์ •๋ณด์˜ ์ถœ์ฒ˜๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ธ์šฉํ•˜์„ธ์š” (์˜ˆ: "X ์ถœ์ฒ˜์— ๋”ฐ๋ฅด๋ฉด...")
141
  3. ์‘๋‹ต์— ์‹ค์ œ ์ถœ์ฒ˜ ๋งํฌ๋ฅผ ํฌํ•จํ•˜์„ธ์š”
142
  4. ์—ฌ๋Ÿฌ ์ถœ์ฒ˜์˜ ์ •๋ณด๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ ๋‹ต๋ณ€ํ•˜์„ธ์š”
 
143
  """
144
 
145
  search_results = instructions + "\n".join(summary_lines)
 
155
  # ๋ชจ๋ธ/ํ”„๋กœ์„ธ์„œ ๋กœ๋”ฉ
156
  ##############################################################################
157
  MAX_CONTENT_CHARS = 4000
158
+ MAX_INPUT_LENGTH = 4096 # ์ตœ๋Œ€ ์ž…๋ ฅ ํ† ํฐ ์ˆ˜ ์ œํ•œ ์ถ”๊ฐ€
159
+ model_id = os.getenv("MODEL_ID", "mlabonne/gemma-3-27b-it-abliterated")
160
 
161
  processor = AutoProcessor.from_pretrained(model_id, padding_side="left")
162
  model = Gemma3ForConditionalGeneration.from_pretrained(
163
  model_id,
164
  device_map="auto",
165
  torch_dtype=torch.bfloat16,
166
+ attn_implementation="eager" # ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด "flash_attention_2"๋กœ ๋ณ€๊ฒฝ
167
  )
168
  MAX_NUM_IMAGES = int(os.getenv("MAX_NUM_IMAGES", "5"))
169
 
 
296
 
297
 
298
  ##############################################################################
299
+ # ๋น„๋””์˜ค ์ฒ˜๋ฆฌ - ์ž„์‹œ ํŒŒ์ผ ์ถ”์  ์ฝ”๋“œ ์ถ”๊ฐ€
300
  ##############################################################################
301
  def downsample_video(video_path: str) -> list[tuple[Image.Image, float]]:
302
  vidcap = cv2.VideoCapture(video_path)
 
310
  success, image = vidcap.read()
311
  if success:
312
  image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
313
+ # ์ด๋ฏธ์ง€ ํฌ๊ธฐ ์ค„์ด๊ธฐ ์ถ”๊ฐ€
314
+ image = cv2.resize(image, (0, 0), fx=0.5, fy=0.5)
315
  pil_image = Image.fromarray(image)
316
  timestamp = round(i / fps, 2)
317
  frames.append((pil_image, timestamp))
 
322
  return frames
323
 
324
 
325
+ def process_video(video_path: str) -> tuple[list[dict], list[str]]:
326
  content = []
327
+ temp_files = [] # ์ž„์‹œ ํŒŒ์ผ ์ถ”์ ์„ ์œ„ํ•œ ๋ฆฌ์ŠคํŠธ
328
+
329
  frames = downsample_video(video_path)
330
  for frame in frames:
331
  pil_image, timestamp = frame
332
  with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
333
  pil_image.save(temp_file.name)
334
+ temp_files.append(temp_file.name) # ์ถ”์ ์„ ์œ„ํ•ด ๊ฒฝ๋กœ ์ €์žฅ
335
  content.append({"type": "text", "text": f"Frame {timestamp}:"})
336
  content.append({"type": "image", "url": temp_file.name})
337
+
338
+ return content, temp_files
339
 
340
 
341
  ##############################################################################
 
377
  )
378
 
379
 
380
+ def process_new_user_message(message: dict) -> tuple[list[dict], list[str]]:
381
+ temp_files = [] # ์ž„์‹œ ํŒŒ์ผ ์ถ”์ ์šฉ ๋ฆฌ์ŠคํŠธ
382
+
383
  if not message["files"]:
384
+ return [{"type": "text", "text": message["text"]}], temp_files
385
 
386
  video_files = [f for f in message["files"] if is_video_file(f)]
387
  image_files = [f for f in message["files"] if is_image_file(f)]
 
404
  content_list.append({"type": "text", "text": pdf_markdown})
405
 
406
  if video_files:
407
+ video_content, video_temp_files = process_video(video_files[0])
408
+ content_list += video_content
409
+ temp_files.extend(video_temp_files)
410
+ return content_list, temp_files
411
 
412
  if "<image>" in message["text"] and image_files:
413
  interleaved_content = process_interleaved_images({"text": message["text"], "files": image_files})
414
  if content_list and content_list[0]["type"] == "text":
415
  content_list = content_list[1:]
416
+ return interleaved_content + content_list, temp_files
417
  else:
418
  for img_path in image_files:
419
  content_list.append({"type": "image", "url": img_path})
420
 
421
+ return content_list, temp_files
422
 
423
 
424
  ##############################################################################
 
450
  return messages
451
 
452
 
453
+ ##############################################################################
454
+ # ๋ชจ๋ธ ์ƒ์„ฑ ํ•จ์ˆ˜์—์„œ OOM ์บ์น˜
455
+ ##############################################################################
456
+ def _model_gen_with_oom_catch(**kwargs):
457
+ """
458
+ ๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ OutOfMemoryError๋ฅผ ์žก์•„์ฃผ๊ธฐ ์œ„ํ•ด
459
+ """
460
+ try:
461
+ model.generate(**kwargs)
462
+ except torch.cuda.OutOfMemoryError:
463
+ raise RuntimeError(
464
+ "[OutOfMemoryError] GPU ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. "
465
+ "Max New Tokens์„ ์ค„์ด๊ฑฐ๋‚˜, ํ”„๋กฌํ”„ํŠธ ๊ธธ์ด๋ฅผ ์ค„์—ฌ์ฃผ์„ธ์š”."
466
+ )
467
+ finally:
468
+ # ์ƒ์„ฑ ์™„๋ฃŒ ํ›„ ํ•œ๋ฒˆ ๋” ์บ์‹œ ๋น„์šฐ๊ธฐ
469
+ clear_cuda_cache()
470
+
471
+
472
  ##############################################################################
473
  # ๋ฉ”์ธ ์ถ”๋ก  ํ•จ์ˆ˜ (web search ์ฒดํฌ ์‹œ ์ž๋™ ํ‚ค์›Œ๋“œ์ถ”์ถœ->๊ฒ€์ƒ‰->๊ฒฐ๊ณผ system msg)
474
  ##############################################################################
 
486
  yield ""
487
  return
488
 
489
+ temp_files = [] # ์ž„์‹œ ํŒŒ์ผ ์ถ”์ ์šฉ
490
+
491
  try:
492
  combined_system_msg = ""
493
 
 
523
 
524
  messages.extend(process_history(history))
525
 
526
+ user_content, user_temp_files = process_new_user_message(message)
527
+ temp_files.extend(user_temp_files) # ์ž„์‹œ ํŒŒ์ผ ์ถ”์ 
528
+
529
  for item in user_content:
530
  if item["type"] == "text" and len(item["text"]) > MAX_CONTENT_CHARS:
531
  item["text"] = item["text"][:MAX_CONTENT_CHARS] + "\n...(truncated)..."
 
538
  return_dict=True,
539
  return_tensors="pt",
540
  ).to(device=model.device, dtype=torch.bfloat16)
541
+
542
+ # ์ž…๋ ฅ ํ† ํฐ ์ˆ˜ ์ œํ•œ ์ถ”๊ฐ€
543
+ if inputs.input_ids.shape[1] > MAX_INPUT_LENGTH:
544
+ inputs.input_ids = inputs.input_ids[:, -MAX_INPUT_LENGTH:]
545
+ if 'attention_mask' in inputs:
546
+ inputs.attention_mask = inputs.attention_mask[:, -MAX_INPUT_LENGTH:]
547
+
548
  streamer = TextIteratorStreamer(processor, timeout=30.0, skip_prompt=True, skip_special_tokens=True)
549
  gen_kwargs = dict(
550
  inputs,
 
563
  except Exception as e:
564
  logger.error(f"Error in run: {str(e)}")
565
  yield f"์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
566
+
567
+ finally:
568
+ # ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
569
+ for temp_file in temp_files:
570
+ try:
571
+ if os.path.exists(temp_file):
572
+ os.unlink(temp_file)
573
+ logger.info(f"Deleted temp file: {temp_file}")
574
+ except Exception as e:
575
+ logger.warning(f"Failed to delete temp file {temp_file}: {e}")
576
+
577
+ # ๋ช…์‹œ์  ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
578
+ try:
579
+ del inputs, streamer
580
+ except:
581
+ pass
582
+
583
+ clear_cuda_cache()
584
 
585
 
586
  ##############################################################################
 
684
  width: 100% !important;
685
  max-width: none !important; /* 1200px ์ œํ•œ ์ œ๊ฑฐ */
686
  }
 
687
  .fillable {
688
  width: 100% !important;
689
  max-width: 100% !important;
690
  }
 
691
  /* 2) ๋ฐฐ๊ฒฝ์„ ์—ฐํ•˜๊ณ  ํˆฌ๋ช…ํ•œ ํŒŒ์Šคํ…” ํ†ค ๊ทธ๋ผ๋””์–ธํŠธ๋กœ ๋ณ€๊ฒฝ */
692
  body {
693
  background: #f5f5f5; /* ๊ทธ๋ผ๋””์–ธํŠธ ๋Œ€์‹  ๋‹จ์ƒ‰ ์‚ฌ์šฉ */
 
696
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
697
  color: #333;
698
  }
 
699
  /* ๋ฒ„ํŠผ ์ƒ‰์ƒ๋„ ๊ธฐ์กด์˜ ์ง™์€ ๋ถ‰์€-์ฃผํ™ฉ โ†’ ํŒŒ์Šคํ…” ๊ณ„์—ด๋กœ ์—ฐ๏ฟฝ๏ฟฝ๏ฟฝ๊ฒŒ */
700
  button, .btn {
701
  background: #ffb6c1 !important; /* ๊ทธ๋ผ๋””์–ธํŠธ ๋Œ€์‹  ๋‹จ์ƒ‰ ์‚ฌ์šฉ */
 
709
  cursor: pointer;
710
  /* transition: transform 0.2s ease-in-out; - ํ˜น์‹œ ๋ชจ๋ฅผ ๋ฌธ์ œ ๋ถ€๋ถ„ ์ œ๊ฑฐ */
711
  }
 
712
  button:hover, .btn:hover {
713
  /* transform: scale(1.03); - ํ˜น์‹œ ๋ชจ๋ฅผ ๋ฌธ์ œ ๋ถ€๋ถ„ ์ œ๊ฑฐ */
714
  background: #ff69b4 !important;
715
  }
 
716
  #examples_container {
717
  margin: auto;
718
  width: 90%;
719
  }
 
720
  #examples_row {
721
  justify-content: center;
722
  }
723
  """
724
 
725
  title_html = """
726
+ <h1 align="center" style="margin-bottom: 0.2em; font-size: 1.6em;"> ๐Ÿค— Gemma3-uncensored-R12B </h1>
727
  <p align="center" style="font-size:1.1em; color:#555;">
728
  โœ…Agentic AI Platform โœ…Reasoning & Uncensored โœ…Multimodal & VLM โœ…Deep-Research & RAG <br>
729
  Operates on an โœ…'NVIDIA A100 GPU' as an independent local server, enhancing security and preventing information leakage.<br>