openfree commited on
Commit
70e12bf
ยท
verified ยท
1 Parent(s): 3a3ac2f

Update app-backup.py

Browse files
Files changed (1) hide show
  1. app-backup.py +124 -170
app-backup.py CHANGED
@@ -5,8 +5,8 @@ import re
5
  import tempfile
6
  from collections.abc import Iterator
7
  from threading import Thread
8
-
9
- import requests # <-- For SERPHouse web search
10
  import cv2
11
  import gradio as gr
12
  import spaces
@@ -17,23 +17,36 @@ from transformers import AutoProcessor, Gemma3ForConditionalGeneration, TextIter
17
 
18
  # CSV/TXT ๋ถ„์„
19
  import pandas as pd
20
-
21
  # PDF ํ…์ŠคํŠธ ์ถ”์ถœ
22
  import PyPDF2
23
 
24
  ##############################################################################
25
- # SERPHouse API key for web search
 
 
 
 
 
26
  ##############################################################################
27
- SERPHOUSE_API_KEY = "V38CNn4HXpLtynJQyOeoUensTEYoFy8PBUxKpDqAW1pawT1vfJ2BWtPQ98h6"
 
 
 
 
 
 
 
 
 
28
 
29
  ##############################################################################
30
- # Simple function to call the SERPHouse Live endpoint
31
- # https://api.serphouse.com/serp/live
32
  ##############################################################################
33
  def do_web_search(query: str) -> str:
34
  """
35
- Calls SERPHouse live endpoint with the given query (q).
36
- Returns a simple text summary or error message.
37
  """
38
  try:
39
  url = "https://api.serphouse.com/serp/live"
@@ -43,35 +56,35 @@ def do_web_search(query: str) -> str:
43
  "lang": "en",
44
  "device": "desktop",
45
  "serp_type": "web",
 
46
  "api_token": SERPHOUSE_API_KEY,
47
  }
48
  resp = requests.get(url, params=params, timeout=30)
49
- resp.raise_for_status() # Raise an exception for 4xx/5xx errors
50
  data = resp.json()
51
 
52
- # For demonstration, let's extract top 3 organic results:
53
  results = data.get("results", {})
54
  organic = results.get("results", {}).get("organic", [])
55
  if not organic:
56
  return "No web search results found."
57
 
58
  summary_lines = []
59
- for item in organic[:3]:
60
- rank = item.get("position", "-")
61
- title = item.get("title", "No Title")
62
- link = item.get("link", "No Link")
63
- snippet = item.get("snippet", "(No snippet)")
64
- summary_lines.append(f"**Rank {rank}:** [{title}]({link})\n\n> {snippet}")
65
-
66
- return "\n\n".join(summary_lines) if summary_lines else "No web search results found."
67
  except Exception as e:
68
  logger.error(f"Web search failed: {e}")
69
  return f"Web search failed: {str(e)}"
70
 
71
 
72
- MAX_CONTENT_CHARS = 4000 # ๋„ˆ๋ฌด ํฐ ํŒŒ์ผ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ์ตœ๋Œ€ ํ‘œ์‹œ 4000์ž
73
-
 
 
74
  model_id = os.getenv("MODEL_ID", "google/gemma-3-27b-it")
 
75
  processor = AutoProcessor.from_pretrained(model_id, padding_side="left")
76
  model = Gemma3ForConditionalGeneration.from_pretrained(
77
  model_id,
@@ -79,23 +92,20 @@ model = Gemma3ForConditionalGeneration.from_pretrained(
79
  torch_dtype=torch.bfloat16,
80
  attn_implementation="eager"
81
  )
82
-
83
  MAX_NUM_IMAGES = int(os.getenv("MAX_NUM_IMAGES", "5"))
84
 
85
 
86
- ##################################################
87
  # CSV, TXT, PDF ๋ถ„์„ ํ•จ์ˆ˜
88
- ##################################################
89
  def analyze_csv_file(path: str) -> str:
90
  """
91
  CSV ํŒŒ์ผ์„ ์ „์ฒด ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜. ๋„ˆ๋ฌด ๊ธธ ๊ฒฝ์šฐ ์ผ๋ถ€๋งŒ ํ‘œ์‹œ.
92
  """
93
  try:
94
  df = pd.read_csv(path)
95
- # ๋ฐ์ดํ„ฐ ํ”„๋ ˆ์ž„ ํฌ๊ธฐ ์ œํ•œ (ํ–‰/์—ด ์ˆ˜๊ฐ€ ๋งŽ์€ ๊ฒฝ์šฐ)
96
  if df.shape[0] > 50 or df.shape[1] > 10:
97
  df = df.iloc[:50, :10]
98
-
99
  df_str = df.to_string()
100
  if len(df_str) > MAX_CONTENT_CHARS:
101
  df_str = df_str[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
@@ -126,18 +136,15 @@ def pdf_to_markdown(pdf_path: str) -> str:
126
  try:
127
  with open(pdf_path, "rb") as f:
128
  reader = PyPDF2.PdfReader(f)
129
- # ์ตœ๋Œ€ 5ํŽ˜์ด์ง€๋งŒ ์ฒ˜๋ฆฌ
130
  max_pages = min(5, len(reader.pages))
131
  for page_num in range(max_pages):
132
  page = reader.pages[page_num]
133
  page_text = page.extract_text() or ""
134
  page_text = page_text.strip()
135
  if page_text:
136
- # ํŽ˜์ด์ง€๋ณ„ ํ…์ŠคํŠธ๋„ ์ œํ•œ
137
  if len(page_text) > MAX_CONTENT_CHARS // max_pages:
138
  page_text = page_text[:MAX_CONTENT_CHARS // max_pages] + "...(truncated)"
139
  text_chunks.append(f"## Page {page_num+1}\n\n{page_text}\n")
140
-
141
  if len(reader.pages) > max_pages:
142
  text_chunks.append(f"\n...(Showing {max_pages} of {len(reader.pages)} pages)...")
143
  except Exception as e:
@@ -150,9 +157,9 @@ def pdf_to_markdown(pdf_path: str) -> str:
150
  return f"**[PDF File: {os.path.basename(pdf_path)}]**\n\n{full_text}"
151
 
152
 
153
- ##################################################
154
  # ์ด๋ฏธ์ง€/๋น„๋””์˜ค ์—…๋กœ๋“œ ์ œํ•œ ๊ฒ€์‚ฌ
155
- ##################################################
156
  def count_files_in_new_message(paths: list[str]) -> tuple[int, int]:
157
  image_count = 0
158
  video_count = 0
@@ -181,14 +188,6 @@ def count_files_in_history(history: list[dict]) -> tuple[int, int]:
181
 
182
 
183
  def validate_media_constraints(message: dict, history: list[dict]) -> bool:
184
- """
185
- - ๋น„๋””์˜ค 1๊ฐœ ์ดˆ๊ณผ ๋ถˆ๊ฐ€
186
- - ๋น„๋””์˜ค์™€ ์ด๋ฏธ์ง€ ํ˜ผํ•ฉ ๋ถˆ๊ฐ€
187
- - ์ด๋ฏธ์ง€ ๊ฐœ์ˆ˜ MAX_NUM_IMAGES ์ดˆ๊ณผ ๋ถˆ๊ฐ€
188
- - <image> ํƒœ๊ทธ๊ฐ€ ์žˆ์œผ๋ฉด ํƒœ๊ทธ ์ˆ˜์™€ ์‹ค์ œ ์ด๋ฏธ์ง€ ์ˆ˜ ์ผ์น˜
189
- - CSV, TXT, PDF ๋“ฑ์€ ์—ฌ๊ธฐ์„œ ์ œํ•œํ•˜์ง€ ์•Š์Œ
190
- """
191
- # ์ด๋ฏธ์ง€์™€ ๋น„๋””์˜ค ํŒŒ์ผ๋งŒ ํ•„ํ„ฐ๋ง
192
  media_files = []
193
  for f in message["files"]:
194
  if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE) or f.endswith(".mp4"):
@@ -213,9 +212,7 @@ def validate_media_constraints(message: dict, history: list[dict]) -> bool:
213
  gr.Warning(f"You can upload up to {MAX_NUM_IMAGES} images.")
214
  return False
215
 
216
- # ์ด๋ฏธ์ง€ ํƒœ๊ทธ ๊ฒ€์ฆ (์‹ค์ œ ์ด๋ฏธ์ง€ ํŒŒ์ผ๋งŒ ๊ณ„์‚ฐ)
217
  if "<image>" in message["text"]:
218
- # ์ด๋ฏธ์ง€ ํŒŒ์ผ๋งŒ ํ•„ํ„ฐ๋ง
219
  image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
220
  image_tag_count = message["text"].count("<image>")
221
  if image_tag_count != len(image_files):
@@ -225,16 +222,14 @@ def validate_media_constraints(message: dict, history: list[dict]) -> bool:
225
  return True
226
 
227
 
228
- ##################################################
229
  # ๋น„๋””์˜ค ์ฒ˜๋ฆฌ
230
- ##################################################
231
  def downsample_video(video_path: str) -> list[tuple[Image.Image, float]]:
232
  vidcap = cv2.VideoCapture(video_path)
233
  fps = vidcap.get(cv2.CAP_PROP_FPS)
234
  total_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
235
-
236
- # ๋” ์ ์€ ํ”„๋ ˆ์ž„์„ ์ถ”์ถœํ•˜๋„๋ก ์กฐ์ •
237
- frame_interval = max(int(fps), int(total_frames / 10)) # ์ดˆ๋‹น 1ํ”„๋ ˆ์ž„ ๋˜๋Š” ์ตœ๋Œ€ 10ํ”„๋ ˆ์ž„
238
  frames = []
239
 
240
  for i in range(0, total_frames, frame_interval):
@@ -245,8 +240,6 @@ def downsample_video(video_path: str) -> list[tuple[Image.Image, float]]:
245
  pil_image = Image.fromarray(image)
246
  timestamp = round(i / fps, 2)
247
  frames.append((pil_image, timestamp))
248
-
249
- # ์ตœ๋Œ€ 5ํ”„๋ ˆ์ž„๋งŒ ์‚ฌ์šฉ
250
  if len(frames) >= 5:
251
  break
252
 
@@ -267,15 +260,14 @@ def process_video(video_path: str) -> list[dict]:
267
  return content
268
 
269
 
270
- ##################################################
271
  # interleaved <image> ์ฒ˜๋ฆฌ
272
- ##################################################
273
  def process_interleaved_images(message: dict) -> list[dict]:
274
  parts = re.split(r"(<image>)", message["text"])
275
  content = []
276
  image_index = 0
277
 
278
- # ์ด๋ฏธ์ง€ ํŒŒ์ผ๋งŒ ํ•„ํ„ฐ๋ง
279
  image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
280
 
281
  for part in parts:
@@ -285,98 +277,81 @@ def process_interleaved_images(message: dict) -> list[dict]:
285
  elif part.strip():
286
  content.append({"type": "text", "text": part.strip()})
287
  else:
288
- # ๊ณต๋ฐฑ์ด๊ฑฐ๋‚˜ \n ๊ฐ™์€ ๊ฒฝ์šฐ
289
  if isinstance(part, str) and part != "<image>":
290
  content.append({"type": "text", "text": part})
291
  return content
292
 
293
 
294
- ##################################################
295
  # PDF + CSV + TXT + ์ด๋ฏธ์ง€/๋น„๋””์˜ค
296
- ##################################################
297
  def is_image_file(file_path: str) -> bool:
298
- """์ด๋ฏธ์ง€ ํŒŒ์ผ์ธ์ง€ ํ™•์ธ"""
299
  return bool(re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE))
300
 
301
-
302
  def is_video_file(file_path: str) -> bool:
303
- """๋น„๋””์˜ค ํŒŒ์ผ์ธ์ง€ ํ™•์ธ"""
304
  return file_path.endswith(".mp4")
305
 
306
-
307
  def is_document_file(file_path: str) -> bool:
308
- """๋ฌธ์„œ ํŒŒ์ผ์ธ์ง€ ํ™•์ธ (PDF, CSV, TXT)"""
309
- return (file_path.lower().endswith(".pdf") or
310
- file_path.lower().endswith(".csv") or
311
- file_path.lower().endswith(".txt"))
 
312
 
313
 
314
  def process_new_user_message(message: dict) -> list[dict]:
315
  if not message["files"]:
316
  return [{"type": "text", "text": message["text"]}]
317
 
318
- # 1) ํŒŒ์ผ ๋ถ„๋ฅ˜
319
  video_files = [f for f in message["files"] if is_video_file(f)]
320
  image_files = [f for f in message["files"] if is_image_file(f)]
321
  csv_files = [f for f in message["files"] if f.lower().endswith(".csv")]
322
  txt_files = [f for f in message["files"] if f.lower().endswith(".txt")]
323
  pdf_files = [f for f in message["files"] if f.lower().endswith(".pdf")]
324
 
325
- # 2) ์‚ฌ์šฉ์ž ์›๋ณธ text ์ถ”๊ฐ€
326
  content_list = [{"type": "text", "text": message["text"]}]
327
 
328
- # 3) CSV
329
  for csv_path in csv_files:
330
  csv_analysis = analyze_csv_file(csv_path)
331
  content_list.append({"type": "text", "text": csv_analysis})
332
 
333
- # 4) TXT
334
  for txt_path in txt_files:
335
  txt_analysis = analyze_txt_file(txt_path)
336
  content_list.append({"type": "text", "text": txt_analysis})
337
 
338
- # 5) PDF
339
  for pdf_path in pdf_files:
340
  pdf_markdown = pdf_to_markdown(pdf_path)
341
  content_list.append({"type": "text", "text": pdf_markdown})
342
 
343
- # 6) ๋น„๋””์˜ค (ํ•œ ๊ฐœ๋งŒ ํ—ˆ์šฉ)
344
  if video_files:
345
  content_list += process_video(video_files[0])
346
  return content_list
347
 
348
- # 7) ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ
349
  if "<image>" in message["text"] and image_files:
350
- # interleaved
351
  interleaved_content = process_interleaved_images({"text": message["text"], "files": image_files})
352
- # ์›๋ณธ content_list ์•ž๋ถ€๋ถ„(ํ…์ŠคํŠธ)์„ ์ œ๊ฑฐํ•˜๊ณ  interleaved๋กœ ๋Œ€์ฒด
353
- if content_list[0]["type"] == "text":
354
- content_list = content_list[1:] # ์›๋ณธ ํ…์ŠคํŠธ ์ œ๊ฑฐ
355
- return interleaved_content + content_list # interleaved + ๋‚˜๋จธ์ง€ ๋ฌธ์„œ ๋ถ„์„ ๋‚ด์šฉ
356
  else:
357
- # ์ผ๋ฐ˜ ์—ฌ๋Ÿฌ ์žฅ
358
  for img_path in image_files:
359
  content_list.append({"type": "image", "url": img_path})
360
 
361
  return content_list
362
 
363
 
364
- ##################################################
365
  # history -> LLM ๋ฉ”์‹œ์ง€ ๋ณ€ํ™˜
366
- ##################################################
367
  def process_history(history: list[dict]) -> list[dict]:
368
  messages = []
369
  current_user_content: list[dict] = []
370
  for item in history:
371
  if item["role"] == "assistant":
372
- # user_content๊ฐ€ ์Œ“์—ฌ์žˆ๋‹ค๋ฉด user ๋ฉ”์‹œ์ง€๋กœ ์ €์žฅ
373
  if current_user_content:
374
  messages.append({"role": "user", "content": current_user_content})
375
  current_user_content = []
376
- # ๊ทธ ๋’ค item์€ assistant
377
  messages.append({"role": "assistant", "content": [{"type": "text", "text": item["content"]}]})
378
  else:
379
- # user
380
  content = item["content"]
381
  if isinstance(content, str):
382
  current_user_content.append({"type": "text", "text": content})
@@ -385,19 +360,17 @@ def process_history(history: list[dict]) -> list[dict]:
385
  if is_image_file(file_path):
386
  current_user_content.append({"type": "image", "url": file_path})
387
  else:
388
- # ๋น„์ด๋ฏธ์ง€ ํŒŒ์ผ์€ ํ…์ŠคํŠธ๋กœ ์ฒ˜๋ฆฌ
389
  current_user_content.append({"type": "text", "text": f"[File: {os.path.basename(file_path)}]"})
390
-
391
- # ๋งˆ์ง€๋ง‰ ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€๊ฐ€ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ์ถ”๊ฐ€
392
  if current_user_content:
393
  messages.append({"role": "user", "content": current_user_content})
394
 
395
  return messages
396
 
397
 
398
- ##################################################
399
- # ๋ฉ”์ธ ์ถ”๋ก  ํ•จ์ˆ˜
400
- ##################################################
401
  @spaces.GPU(duration=120)
402
  def run(
403
  message: dict,
@@ -407,60 +380,42 @@ def run(
407
  use_web_search: bool = False,
408
  web_search_query: str = "",
409
  ) -> Iterator[str]:
410
- """
411
- The main inference function. Now extended with optional web_search arguments:
412
- - use_web_search: bool
413
- - web_search_query: str
414
- If `use_web_search` is True, calls SERPHouse for the given `web_search_query`.
415
- """
416
- # Validate media constraints first
417
  if not validate_media_constraints(message, history):
418
  yield ""
419
  return
420
 
421
  try:
422
- # If user opted for "Web Search", do it here and yield a prefix message
423
- if use_web_search and web_search_query.strip():
424
- ws_result = do_web_search(web_search_query.strip())
425
- yield f"**[Web Search Results for '{web_search_query.strip()}':]**\n\n{ws_result}\n\n---\n"
 
 
 
 
 
 
 
 
 
 
426
 
427
  messages = []
428
- if system_prompt:
429
- messages.append({"role": "system", "content": [{"type": "text", "text": system_prompt}]})
 
 
 
 
430
  messages.extend(process_history(history))
431
-
432
- # ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
433
  user_content = process_new_user_message(message)
434
-
435
- # ํ† ํฐ ์ˆ˜๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•ด ๋„ˆ๋ฌด ๊ธด ํ…์ŠคํŠธ๋Š” ์ž˜๋ผ๋‚ด๊ธฐ
436
  for item in user_content:
437
  if item["type"] == "text" and len(item["text"]) > MAX_CONTENT_CHARS:
438
  item["text"] = item["text"][:MAX_CONTENT_CHARS] + "\n...(truncated)..."
439
-
440
  messages.append({"role": "user", "content": user_content})
441
 
442
- # ๋ชจ๋ธ ์ž…๋ ฅ ์ƒ์„ฑ ์ „ ์ตœ์ข… ํ™•์ธ
443
- for msg in messages:
444
- if msg["role"] != "user":
445
- continue
446
-
447
- filtered_content = []
448
- for item in msg["content"]:
449
- if item["type"] == "image":
450
- if is_image_file(item["url"]):
451
- filtered_content.append(item)
452
- else:
453
- # ์ด๋ฏธ์ง€ ํŒŒ์ผ์ด ์•„๋‹Œ ๊ฒฝ์šฐ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜
454
- filtered_content.append({
455
- "type": "text",
456
- "text": f"[Non-image file: {os.path.basename(item['url'])}]"
457
- })
458
- else:
459
- filtered_content.append(item)
460
-
461
- msg["content"] = filtered_content
462
-
463
- # ๋ชจ๋ธ ์ž…๋ ฅ ์ƒ์„ฑ
464
  inputs = processor.apply_chat_template(
465
  messages,
466
  add_generation_prompt=True,
@@ -469,35 +424,46 @@ def run(
469
  return_tensors="pt",
470
  ).to(device=model.device, dtype=torch.bfloat16)
471
 
472
- # ํ…์ŠคํŠธ ์ƒ์„ฑ ์ŠคํŠธ๋ฆฌ๋จธ ์„ค์ •
473
  streamer = TextIteratorStreamer(processor, timeout=30.0, skip_prompt=True, skip_special_tokens=True)
474
  gen_kwargs = dict(
475
  inputs,
476
  streamer=streamer,
477
  max_new_tokens=max_new_tokens,
478
  )
479
-
480
- # ๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ ํ…์ŠคํŠธ ์ƒ์„ฑ
481
- t = Thread(target=model.generate, kwargs=gen_kwargs)
482
  t.start()
483
 
484
- # ๊ฒฐ๊ณผ ์ŠคํŠธ๋ฆฌ๋ฐ
485
  output = ""
486
  for new_text in streamer:
487
  output += new_text
488
  yield output
489
-
490
  except Exception as e:
491
  logger.error(f"Error in run: {str(e)}")
492
  yield f"์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
493
 
494
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
 
496
- ##################################################
497
- # ์˜ˆ์‹œ๋“ค (ํ•œ๊ธ€ํ™” ๋ฒ„์ „)
498
- ##################################################
499
  examples = [
500
-
501
  [
502
  {
503
  "text": "๋‘ PDF ํŒŒ์ผ ๋‚ด์šฉ์„ ๋น„๊ตํ•˜๋ผ.",
@@ -505,7 +471,7 @@ examples = [
505
  "files": [
506
  "assets/additional-examples/before.pdf",
507
  "assets/additional-examples/after.pdf",
508
- ],
509
  }
510
  ],
511
  [
@@ -513,37 +479,37 @@ examples = [
513
  "text": "CSV ํŒŒ์ผ ๋‚ด์šฉ์„ ์š”์•ฝ, ๋ถ„์„ํ•˜๋ผ",
514
  "files": ["assets/additional-examples/sample-csv.csv"],
515
  }
516
- ],
517
  [
518
  {
519
  "text": "์ด ์˜์ƒ์˜ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•˜๋ผ",
520
  "files": ["assets/additional-examples/tmp.mp4"],
521
  }
522
- ],
523
  [
524
  {
525
  "text": "ํ‘œ์ง€ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•˜๊ณ  ๊ธ€์ž๋ฅผ ์ฝ์–ด์ฃผ์„ธ์š”.",
526
  "files": ["assets/additional-examples/maz.jpg"],
527
  }
528
- ],
529
  [
530
  {
531
  "text": "์ด๋ฏธ ์ด ์˜์–‘์ œ๋ฅผ <image> ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ์ด ์ œํ’ˆ <image>์„ ์ƒˆ๋กœ ์‚ฌ๋ ค ํ•ฉ๋‹ˆ๋‹ค. ํ•จ๊ป˜ ์„ญ์ทจํ•  ๋•Œ ์ฃผ์˜ํ•ด์•ผ ํ•  ์ ์ด ์žˆ์„๊นŒ์š”?",
532
  "files": ["assets/additional-examples/pill1.png", "assets/additional-examples/pill2.png"],
533
  }
534
- ],
535
  [
536
  {
537
  "text": "์ด ์ ๋ถ„์„ ํ’€์–ด์ฃผ์„ธ์š”.",
538
  "files": ["assets/additional-examples/4.png"],
539
  }
540
- ],
541
  [
542
  {
543
  "text": "์ด ํ‹ฐ์ผ“์€ ์–ธ์ œ ๋ฐœ๊ธ‰๋œ ๊ฒƒ์ด๊ณ , ๊ฐ€๊ฒฉ์€ ์–ผ๋งˆ์ธ๊ฐ€์š”?",
544
  "files": ["assets/additional-examples/2.png"],
545
  }
546
- ],
547
  [
548
  {
549
  "text": "์ด๋ฏธ์ง€๋“ค์˜ ์ˆœ์„œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์งง์€ ์ด์•ผ๊ธฐ๋ฅผ ๋งŒ๋“ค์–ด ์ฃผ์„ธ์š”.",
@@ -567,24 +533,19 @@ examples = [
567
  "text": "๋™์ผํ•œ ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„๋ฅผ ๊ทธ๋ฆฌ๋Š” matplotlib ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”.",
568
  "files": ["assets/additional-examples/barchart.png"],
569
  }
570
- ],
571
-
572
  [
573
  {
574
  "text": "์ด ์„ธ๊ณ„์—์„œ ์‚ด๊ณ  ์žˆ์„ ์ƒ๋ฌผ๋“ค์„ ์ƒ์ƒํ•ด์„œ ๋ฌ˜์‚ฌํ•ด์ฃผ์„ธ์š”.",
575
  "files": ["assets/sample-images/08.png"],
576
  }
577
  ],
578
-
579
-
580
  [
581
  {
582
  "text": "์ด๋ฏธ์ง€์— ์žˆ๋Š” ํ…์ŠคํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ์ฝ์–ด์„œ ๋งˆํฌ๋‹ค์šด ํ˜•ํƒœ๋กœ ์ ์–ด์ฃผ์„ธ์š”.",
583
  "files": ["assets/additional-examples/3.png"],
584
  }
585
  ],
586
-
587
-
588
  [
589
  {
590
  "text": "์ด ํ‘œ์ง€ํŒ์—๋Š” ๋ฌด์Šจ ๋ฌธ๊ตฌ๊ฐ€ ์ ํ˜€ ์žˆ๋‚˜์š”?",
@@ -597,15 +558,11 @@ examples = [
597
  "files": ["assets/sample-images/03.png"],
598
  }
599
  ],
600
-
601
  ]
602
 
603
 
604
-
605
-
606
-
607
  ##############################################################################
608
- # Custom CSS similar to second example (colorful background, panel, etc.)
609
  ##############################################################################
610
  css = """
611
  body {
@@ -662,18 +619,13 @@ button:hover, .btn:hover {
662
  """
663
 
664
  title_html = """
665
- <h1 align="center" style="margin-bottom: 0.2em;"> ๐Ÿค— Vidraft-Gemma-3-27B </h1>
666
  <p align="center" style="font-size:1.1em; color:#555;">
667
  Multimodal Chat Interface + Optional Web Search
668
  </p>
669
  """
670
 
671
- ##############################################################################
672
- # Build a Blocks layout that includes:
673
- # - A left sidebar with "Web Search" controls
674
- # - The main ChatInterface in the center or right
675
- ##############################################################################
676
- with gr.Blocks(css=css, title="Vidraft-Gemma-3-27B") as demo:
677
  gr.Markdown(title_html)
678
 
679
  with gr.Row():
@@ -684,12 +636,12 @@ with gr.Blocks(css=css, title="Vidraft-Gemma-3-27B") as demo:
684
  web_search_checkbox = gr.Checkbox(
685
  label="Web Search",
686
  value=False,
687
- info="Check to enable a SERPHouse web search before the chat reply"
688
  )
689
  web_search_text = gr.Textbox(
690
  lines=1,
691
- label="Web Search Query",
692
- placeholder="Enter search keywords..."
693
  )
694
 
695
  gr.Markdown("---")
@@ -707,12 +659,12 @@ with gr.Blocks(css=css, title="Vidraft-Gemma-3-27B") as demo:
707
  minimum=100,
708
  maximum=8000,
709
  step=50,
710
- value=2000,
711
  )
712
 
713
- gr.Markdown("<br><br>") # spacing
714
 
715
- # Main ChatInterface to the right
716
  with gr.Column(scale=7):
717
  chat = gr.ChatInterface(
718
  fn=run,
@@ -734,7 +686,7 @@ with gr.Blocks(css=css, title="Vidraft-Gemma-3-27B") as demo:
734
  web_search_text,
735
  ],
736
  stop_btn=False,
737
- title="Vidraft-Gemma-3-27B",
738
  examples=examples,
739
  run_examples_on_click=False,
740
  cache_examples=False,
@@ -745,12 +697,14 @@ with gr.Blocks(css=css, title="Vidraft-Gemma-3-27B") as demo:
745
  with gr.Row(elem_id="examples_row"):
746
  with gr.Column(scale=12, elem_id="examples_container"):
747
  gr.Markdown("### Example Inputs (click to load)")
748
- # The fix: pass an empty list to avoid the "None" error, so we keep the code structure.
749
  gr.Examples(
750
  examples=examples,
751
- inputs=[], # Instead of None or chat.
752
  cache_examples=False
753
  )
754
 
755
  if __name__ == "__main__":
 
 
756
  demo.launch()
 
 
5
  import tempfile
6
  from collections.abc import Iterator
7
  from threading import Thread
8
+ import json
9
+ import requests
10
  import cv2
11
  import gradio as gr
12
  import spaces
 
17
 
18
  # CSV/TXT ๋ถ„์„
19
  import pandas as pd
 
20
  # PDF ํ…์ŠคํŠธ ์ถ”์ถœ
21
  import PyPDF2
22
 
23
  ##############################################################################
24
+ # SERPHouse API key from environment variable
25
+ ##############################################################################
26
+ SERPHOUSE_API_KEY = os.getenv("SERPHOUSE_API_KEY", "")
27
+
28
+ ##############################################################################
29
+ # ๊ฐ„๋‹จํ•œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ ํ•จ์ˆ˜ (ํ•œ๊ธ€ + ์•ŒํŒŒ๋ฒณ + ์ˆซ์ž + ๊ณต๋ฐฑ ๋ณด์กด)
30
  ##############################################################################
31
+ def extract_keywords(text: str, top_k: int = 5) -> str:
32
+ """
33
+ 1) ํ•œ๊ธ€(๊ฐ€-ํžฃ), ์˜์–ด(a-zA-Z), ์ˆซ์ž(0-9), ๊ณต๋ฐฑ๋งŒ ๋‚จ๊น€
34
+ 2) ๊ณต๋ฐฑ ๊ธฐ์ค€ ํ† ํฐ ๋ถ„๋ฆฌ
35
+ 3) ์ตœ๋Œ€ top_k๊ฐœ๋งŒ
36
+ """
37
+ text = re.sub(r"[^a-zA-Z0-9๊ฐ€-ํžฃ\s]", "", text)
38
+ tokens = text.split()
39
+ key_tokens = tokens[:top_k]
40
+ return " ".join(key_tokens)
41
 
42
  ##############################################################################
43
+ # SERPHouse Live endpoint ํ˜ธ์ถœ
44
+ # - ์ƒ์œ„ 20๊ฐœ ๊ฒฐ๊ณผ JSON์„ LLM์— ๋„˜๊ธธ ๋•Œ link, snippet ๋“ฑ ๋ชจ๋‘ ํฌํ•จ
45
  ##############################################################################
46
  def do_web_search(query: str) -> str:
47
  """
48
+ ์ƒ์œ„ 20๊ฐœ 'organic' ๊ฒฐ๊ณผ item ์ „์ฒด(์ œ๋ชฉ, link, snippet ๋“ฑ)๋ฅผ
49
+ JSON ๋ฌธ์ž์—ด ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜
50
  """
51
  try:
52
  url = "https://api.serphouse.com/serp/live"
 
56
  "lang": "en",
57
  "device": "desktop",
58
  "serp_type": "web",
59
+ "num_result": "20",
60
  "api_token": SERPHOUSE_API_KEY,
61
  }
62
  resp = requests.get(url, params=params, timeout=30)
63
+ resp.raise_for_status()
64
  data = resp.json()
65
 
 
66
  results = data.get("results", {})
67
  organic = results.get("results", {}).get("organic", [])
68
  if not organic:
69
  return "No web search results found."
70
 
71
  summary_lines = []
72
+ for idx, item in enumerate(organic[:20], start=1):
73
+ item_json = json.dumps(item, ensure_ascii=False, indent=2)
74
+ summary_lines.append(f"Result {idx}:\n{item_json}\n")
75
+
76
+ return "\n".join(summary_lines)
 
 
 
77
  except Exception as e:
78
  logger.error(f"Web search failed: {e}")
79
  return f"Web search failed: {str(e)}"
80
 
81
 
82
+ ##############################################################################
83
+ # ๋ชจ๋ธ/ํ”„๋กœ์„ธ์„œ ๋กœ๋”ฉ
84
+ ##############################################################################
85
+ MAX_CONTENT_CHARS = 4000
86
  model_id = os.getenv("MODEL_ID", "google/gemma-3-27b-it")
87
+
88
  processor = AutoProcessor.from_pretrained(model_id, padding_side="left")
89
  model = Gemma3ForConditionalGeneration.from_pretrained(
90
  model_id,
 
92
  torch_dtype=torch.bfloat16,
93
  attn_implementation="eager"
94
  )
 
95
  MAX_NUM_IMAGES = int(os.getenv("MAX_NUM_IMAGES", "5"))
96
 
97
 
98
+ ##############################################################################
99
  # CSV, TXT, PDF ๋ถ„์„ ํ•จ์ˆ˜
100
+ ##############################################################################
101
  def analyze_csv_file(path: str) -> str:
102
  """
103
  CSV ํŒŒ์ผ์„ ์ „์ฒด ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜. ๋„ˆ๋ฌด ๊ธธ ๊ฒฝ์šฐ ์ผ๋ถ€๋งŒ ํ‘œ์‹œ.
104
  """
105
  try:
106
  df = pd.read_csv(path)
 
107
  if df.shape[0] > 50 or df.shape[1] > 10:
108
  df = df.iloc[:50, :10]
 
109
  df_str = df.to_string()
110
  if len(df_str) > MAX_CONTENT_CHARS:
111
  df_str = df_str[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
 
136
  try:
137
  with open(pdf_path, "rb") as f:
138
  reader = PyPDF2.PdfReader(f)
 
139
  max_pages = min(5, len(reader.pages))
140
  for page_num in range(max_pages):
141
  page = reader.pages[page_num]
142
  page_text = page.extract_text() or ""
143
  page_text = page_text.strip()
144
  if page_text:
 
145
  if len(page_text) > MAX_CONTENT_CHARS // max_pages:
146
  page_text = page_text[:MAX_CONTENT_CHARS // max_pages] + "...(truncated)"
147
  text_chunks.append(f"## Page {page_num+1}\n\n{page_text}\n")
 
148
  if len(reader.pages) > max_pages:
149
  text_chunks.append(f"\n...(Showing {max_pages} of {len(reader.pages)} pages)...")
150
  except Exception as e:
 
157
  return f"**[PDF File: {os.path.basename(pdf_path)}]**\n\n{full_text}"
158
 
159
 
160
+ ##############################################################################
161
  # ์ด๋ฏธ์ง€/๋น„๋””์˜ค ์—…๋กœ๋“œ ์ œํ•œ ๊ฒ€์‚ฌ
162
+ ##############################################################################
163
  def count_files_in_new_message(paths: list[str]) -> tuple[int, int]:
164
  image_count = 0
165
  video_count = 0
 
188
 
189
 
190
  def validate_media_constraints(message: dict, history: list[dict]) -> bool:
 
 
 
 
 
 
 
 
191
  media_files = []
192
  for f in message["files"]:
193
  if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE) or f.endswith(".mp4"):
 
212
  gr.Warning(f"You can upload up to {MAX_NUM_IMAGES} images.")
213
  return False
214
 
 
215
  if "<image>" in message["text"]:
 
216
  image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
217
  image_tag_count = message["text"].count("<image>")
218
  if image_tag_count != len(image_files):
 
222
  return True
223
 
224
 
225
+ ##############################################################################
226
  # ๋น„๋””์˜ค ์ฒ˜๋ฆฌ
227
+ ##############################################################################
228
  def downsample_video(video_path: str) -> list[tuple[Image.Image, float]]:
229
  vidcap = cv2.VideoCapture(video_path)
230
  fps = vidcap.get(cv2.CAP_PROP_FPS)
231
  total_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
232
+ frame_interval = max(int(fps), int(total_frames / 10))
 
 
233
  frames = []
234
 
235
  for i in range(0, total_frames, frame_interval):
 
240
  pil_image = Image.fromarray(image)
241
  timestamp = round(i / fps, 2)
242
  frames.append((pil_image, timestamp))
 
 
243
  if len(frames) >= 5:
244
  break
245
 
 
260
  return content
261
 
262
 
263
+ ##############################################################################
264
  # interleaved <image> ์ฒ˜๋ฆฌ
265
+ ##############################################################################
266
  def process_interleaved_images(message: dict) -> list[dict]:
267
  parts = re.split(r"(<image>)", message["text"])
268
  content = []
269
  image_index = 0
270
 
 
271
  image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
272
 
273
  for part in parts:
 
277
  elif part.strip():
278
  content.append({"type": "text", "text": part.strip()})
279
  else:
 
280
  if isinstance(part, str) and part != "<image>":
281
  content.append({"type": "text", "text": part})
282
  return content
283
 
284
 
285
+ ##############################################################################
286
  # PDF + CSV + TXT + ์ด๋ฏธ์ง€/๋น„๋””์˜ค
287
+ ##############################################################################
288
  def is_image_file(file_path: str) -> bool:
 
289
  return bool(re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE))
290
 
 
291
  def is_video_file(file_path: str) -> bool:
 
292
  return file_path.endswith(".mp4")
293
 
 
294
  def is_document_file(file_path: str) -> bool:
295
+ return (
296
+ file_path.lower().endswith(".pdf")
297
+ or file_path.lower().endswith(".csv")
298
+ or file_path.lower().endswith(".txt")
299
+ )
300
 
301
 
302
  def process_new_user_message(message: dict) -> list[dict]:
303
  if not message["files"]:
304
  return [{"type": "text", "text": message["text"]}]
305
 
 
306
  video_files = [f for f in message["files"] if is_video_file(f)]
307
  image_files = [f for f in message["files"] if is_image_file(f)]
308
  csv_files = [f for f in message["files"] if f.lower().endswith(".csv")]
309
  txt_files = [f for f in message["files"] if f.lower().endswith(".txt")]
310
  pdf_files = [f for f in message["files"] if f.lower().endswith(".pdf")]
311
 
 
312
  content_list = [{"type": "text", "text": message["text"]}]
313
 
 
314
  for csv_path in csv_files:
315
  csv_analysis = analyze_csv_file(csv_path)
316
  content_list.append({"type": "text", "text": csv_analysis})
317
 
 
318
  for txt_path in txt_files:
319
  txt_analysis = analyze_txt_file(txt_path)
320
  content_list.append({"type": "text", "text": txt_analysis})
321
 
 
322
  for pdf_path in pdf_files:
323
  pdf_markdown = pdf_to_markdown(pdf_path)
324
  content_list.append({"type": "text", "text": pdf_markdown})
325
 
 
326
  if video_files:
327
  content_list += process_video(video_files[0])
328
  return content_list
329
 
 
330
  if "<image>" in message["text"] and image_files:
 
331
  interleaved_content = process_interleaved_images({"text": message["text"], "files": image_files})
332
+ if content_list and content_list[0]["type"] == "text":
333
+ content_list = content_list[1:]
334
+ return interleaved_content + content_list
 
335
  else:
 
336
  for img_path in image_files:
337
  content_list.append({"type": "image", "url": img_path})
338
 
339
  return content_list
340
 
341
 
342
+ ##############################################################################
343
  # history -> LLM ๋ฉ”์‹œ์ง€ ๋ณ€ํ™˜
344
+ ##############################################################################
345
  def process_history(history: list[dict]) -> list[dict]:
346
  messages = []
347
  current_user_content: list[dict] = []
348
  for item in history:
349
  if item["role"] == "assistant":
 
350
  if current_user_content:
351
  messages.append({"role": "user", "content": current_user_content})
352
  current_user_content = []
 
353
  messages.append({"role": "assistant", "content": [{"type": "text", "text": item["content"]}]})
354
  else:
 
355
  content = item["content"]
356
  if isinstance(content, str):
357
  current_user_content.append({"type": "text", "text": content})
 
360
  if is_image_file(file_path):
361
  current_user_content.append({"type": "image", "url": file_path})
362
  else:
 
363
  current_user_content.append({"type": "text", "text": f"[File: {os.path.basename(file_path)}]"})
364
+
 
365
  if current_user_content:
366
  messages.append({"role": "user", "content": current_user_content})
367
 
368
  return messages
369
 
370
 
371
+ ##############################################################################
372
+ # ๋ฉ”์ธ ์ถ”๋ก  ํ•จ์ˆ˜ (web search ์ฒดํฌ ์‹œ ์ž๋™ ํ‚ค์›Œ๋“œ์ถ”์ถœ->๊ฒ€์ƒ‰->๊ฒฐ๊ณผ system msg)
373
+ ##############################################################################
374
  @spaces.GPU(duration=120)
375
  def run(
376
  message: dict,
 
380
  use_web_search: bool = False,
381
  web_search_query: str = "",
382
  ) -> Iterator[str]:
383
+
 
 
 
 
 
 
384
  if not validate_media_constraints(message, history):
385
  yield ""
386
  return
387
 
388
  try:
389
+ combined_system_msg = ""
390
+
391
+ if system_prompt.strip():
392
+ combined_system_msg += f"[System Prompt]\n{system_prompt.strip()}\n\n"
393
+
394
+ if use_web_search:
395
+ user_text = message["text"]
396
+ ws_query = extract_keywords(user_text, top_k=5)
397
+ if ws_query.strip():
398
+ logger.info(f"[Auto WebSearch Keyword] {ws_query!r}")
399
+ ws_result = do_web_search(ws_query)
400
+ combined_system_msg += f"[Search top-20 Full Items Based on user prompt]\n{ws_result}\n\n"
401
+ else:
402
+ combined_system_msg += "[No valid keywords found, skipping WebSearch]\n\n"
403
 
404
  messages = []
405
+ if combined_system_msg.strip():
406
+ messages.append({
407
+ "role": "system",
408
+ "content": [{"type": "text", "text": combined_system_msg.strip()}],
409
+ })
410
+
411
  messages.extend(process_history(history))
412
+
 
413
  user_content = process_new_user_message(message)
 
 
414
  for item in user_content:
415
  if item["type"] == "text" and len(item["text"]) > MAX_CONTENT_CHARS:
416
  item["text"] = item["text"][:MAX_CONTENT_CHARS] + "\n...(truncated)..."
 
417
  messages.append({"role": "user", "content": user_content})
418
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  inputs = processor.apply_chat_template(
420
  messages,
421
  add_generation_prompt=True,
 
424
  return_tensors="pt",
425
  ).to(device=model.device, dtype=torch.bfloat16)
426
 
 
427
  streamer = TextIteratorStreamer(processor, timeout=30.0, skip_prompt=True, skip_special_tokens=True)
428
  gen_kwargs = dict(
429
  inputs,
430
  streamer=streamer,
431
  max_new_tokens=max_new_tokens,
432
  )
433
+
434
+ t = Thread(target=_model_gen_with_oom_catch, kwargs=gen_kwargs)
 
435
  t.start()
436
 
 
437
  output = ""
438
  for new_text in streamer:
439
  output += new_text
440
  yield output
441
+
442
  except Exception as e:
443
  logger.error(f"Error in run: {str(e)}")
444
  yield f"์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
445
 
446
 
447
+ ##############################################################################
448
+ # [์ถ”๊ฐ€] ๋ณ„๋„ ํ•จ์ˆ˜์—์„œ model.generate(...)๋ฅผ ํ˜ธ์ถœ, OOM ์บ์น˜
449
+ ##############################################################################
450
+ def _model_gen_with_oom_catch(**kwargs):
451
+ """
452
+ ๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ OutOfMemoryError๋ฅผ ์žก์•„์ฃผ๊ธฐ ์œ„ํ•ด
453
+ """
454
+ try:
455
+ model.generate(**kwargs)
456
+ except torch.cuda.OutOfMemoryError:
457
+ raise RuntimeError(
458
+ "[OutOfMemoryError] GPU ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. "
459
+ "Max New Tokens์„ ์ค„์ด๊ฑฐ๋‚˜, ํ”„๋กฌํ”„ํŠธ ๊ธธ์ด๋ฅผ ์ค„์—ฌ์ฃผ์„ธ์š”."
460
+ )
461
+
462
 
463
+ ##############################################################################
464
+ # ์˜ˆ์‹œ๋“ค (ํ•œ๊ธ€ํ™”)
465
+ ##############################################################################
466
  examples = [
 
467
  [
468
  {
469
  "text": "๋‘ PDF ํŒŒ์ผ ๋‚ด์šฉ์„ ๋น„๊ตํ•˜๋ผ.",
 
471
  "files": [
472
  "assets/additional-examples/before.pdf",
473
  "assets/additional-examples/after.pdf",
474
+ ],
475
  }
476
  ],
477
  [
 
479
  "text": "CSV ํŒŒ์ผ ๋‚ด์šฉ์„ ์š”์•ฝ, ๋ถ„์„ํ•˜๋ผ",
480
  "files": ["assets/additional-examples/sample-csv.csv"],
481
  }
482
+ ],
483
  [
484
  {
485
  "text": "์ด ์˜์ƒ์˜ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•˜๋ผ",
486
  "files": ["assets/additional-examples/tmp.mp4"],
487
  }
488
+ ],
489
  [
490
  {
491
  "text": "ํ‘œ์ง€ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•˜๊ณ  ๊ธ€์ž๋ฅผ ์ฝ์–ด์ฃผ์„ธ์š”.",
492
  "files": ["assets/additional-examples/maz.jpg"],
493
  }
494
+ ],
495
  [
496
  {
497
  "text": "์ด๋ฏธ ์ด ์˜์–‘์ œ๋ฅผ <image> ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ์ด ์ œํ’ˆ <image>์„ ์ƒˆ๋กœ ์‚ฌ๋ ค ํ•ฉ๋‹ˆ๋‹ค. ํ•จ๊ป˜ ์„ญ์ทจํ•  ๋•Œ ์ฃผ์˜ํ•ด์•ผ ํ•  ์ ์ด ์žˆ์„๊นŒ์š”?",
498
  "files": ["assets/additional-examples/pill1.png", "assets/additional-examples/pill2.png"],
499
  }
500
+ ],
501
  [
502
  {
503
  "text": "์ด ์ ๋ถ„์„ ํ’€์–ด์ฃผ์„ธ์š”.",
504
  "files": ["assets/additional-examples/4.png"],
505
  }
506
+ ],
507
  [
508
  {
509
  "text": "์ด ํ‹ฐ์ผ“์€ ์–ธ์ œ ๋ฐœ๊ธ‰๋œ ๊ฒƒ์ด๊ณ , ๊ฐ€๊ฒฉ์€ ์–ผ๋งˆ์ธ๊ฐ€์š”?",
510
  "files": ["assets/additional-examples/2.png"],
511
  }
512
+ ],
513
  [
514
  {
515
  "text": "์ด๋ฏธ์ง€๋“ค์˜ ์ˆœ์„œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์งง์€ ์ด์•ผ๊ธฐ๋ฅผ ๋งŒ๋“ค์–ด ์ฃผ์„ธ์š”.",
 
533
  "text": "๋™์ผํ•œ ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„๋ฅผ ๊ทธ๋ฆฌ๋Š” matplotlib ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”.",
534
  "files": ["assets/additional-examples/barchart.png"],
535
  }
536
+ ],
 
537
  [
538
  {
539
  "text": "์ด ์„ธ๊ณ„์—์„œ ์‚ด๊ณ  ์žˆ์„ ์ƒ๋ฌผ๋“ค์„ ์ƒ์ƒํ•ด์„œ ๋ฌ˜์‚ฌํ•ด์ฃผ์„ธ์š”.",
540
  "files": ["assets/sample-images/08.png"],
541
  }
542
  ],
 
 
543
  [
544
  {
545
  "text": "์ด๋ฏธ์ง€์— ์žˆ๋Š” ํ…์ŠคํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ์ฝ์–ด์„œ ๋งˆํฌ๋‹ค์šด ํ˜•ํƒœ๋กœ ์ ์–ด์ฃผ์„ธ์š”.",
546
  "files": ["assets/additional-examples/3.png"],
547
  }
548
  ],
 
 
549
  [
550
  {
551
  "text": "์ด ํ‘œ์ง€ํŒ์—๋Š” ๋ฌด์Šจ ๋ฌธ๊ตฌ๊ฐ€ ์ ํ˜€ ์žˆ๋‚˜์š”?",
 
558
  "files": ["assets/sample-images/03.png"],
559
  }
560
  ],
 
561
  ]
562
 
563
 
 
 
 
564
  ##############################################################################
565
+ # Gradio UI (Blocks) ๊ตฌ์„ฑ
566
  ##############################################################################
567
  css = """
568
  body {
 
619
  """
620
 
621
  title_html = """
622
+ <h1 align="center" style="margin-bottom: 0.2em;"> ๐Ÿค— Vidraft-G3-27B : Multimodal + VLM + Deep Research </h1>
623
  <p align="center" style="font-size:1.1em; color:#555;">
624
  Multimodal Chat Interface + Optional Web Search
625
  </p>
626
  """
627
 
628
+ with gr.Blocks(css=css, title="Vidraft-G3-27B") as demo:
 
 
 
 
 
629
  gr.Markdown(title_html)
630
 
631
  with gr.Row():
 
636
  web_search_checkbox = gr.Checkbox(
637
  label="Web Search",
638
  value=False,
639
+ info="Check to enable a Deep Research(auto keywords) before the chat reply"
640
  )
641
  web_search_text = gr.Textbox(
642
  lines=1,
643
+ label="(Unused) Web Search Query",
644
+ placeholder="No direct input needed"
645
  )
646
 
647
  gr.Markdown("---")
 
659
  minimum=100,
660
  maximum=8000,
661
  step=50,
662
+ value=2000, # GPU ๋ฉ”๋ชจ๋ฆฌ ์ ˆ์•ฝ ์œ„ํ•ด ๊ธฐ๋ณธ๊ฐ’ ์•ฝ๊ฐ„ ์ถ•์†Œ
663
  )
664
 
665
+ gr.Markdown("<br><br>")
666
 
667
+ # Main ChatInterface
668
  with gr.Column(scale=7):
669
  chat = gr.ChatInterface(
670
  fn=run,
 
686
  web_search_text,
687
  ],
688
  stop_btn=False,
689
+ title="Vidraft-G3-27B",
690
  examples=examples,
691
  run_examples_on_click=False,
692
  cache_examples=False,
 
697
  with gr.Row(elem_id="examples_row"):
698
  with gr.Column(scale=12, elem_id="examples_container"):
699
  gr.Markdown("### Example Inputs (click to load)")
 
700
  gr.Examples(
701
  examples=examples,
702
+ inputs=[],
703
  cache_examples=False
704
  )
705
 
706
  if __name__ == "__main__":
707
+ # share=True ์‹œ HF Spaces์—์„œ ๊ฒฝ๊ณ  ๋ฐœ์ƒ - ๋กœ์ปฌ์—์„œ๋งŒ ๋™์ž‘
708
+ # demo.launch(share=True)
709
  demo.launch()
710
+