seawolf2357 commited on
Commit
c19847c
ยท
verified ยท
1 Parent(s): 657527f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +91 -74
app.py CHANGED
@@ -22,35 +22,33 @@ import pandas as pd
22
  import PyPDF2
23
 
24
  ##############################################################################
25
- # SERPHouse API key for web search
26
  ##############################################################################
27
- SERPHOUSE_API_KEY = "V38CNn4HXpLtynJQyOeoUensTEYoFy8PBUxKpDqAW1pawT1vfJ2BWtPQ98h6"
28
 
29
  ##############################################################################
30
- # [์ƒˆ๋กœ ์ถ”๊ฐ€] ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€๋กœ๋ถ€ํ„ฐ ๊ฐ„๋‹จํžˆ ํ‚ค์›Œ๋“œ ์ถ”์ถœํ•˜๋Š” ํ•จ์ˆ˜ ์˜ˆ์‹œ
31
- # - ์‹ค์ œ ํ™˜๊ฒฝ์— ๋งž๊ฒŒ stopwords, ํ˜•ํƒœ์†Œ ๋ถ„์„ ๋“ฑ ๊ณ ๋„ํ™” ๊ฐ€๋Šฅ
32
  ##############################################################################
33
  def extract_keywords(text: str, top_k: int = 5) -> str:
34
- # 1) ์†Œ๋ฌธ์ž๋กœ
 
 
 
 
 
 
35
  text = text.lower()
36
- # 2) ์•ŒํŒŒ๋ฒณ/์ˆซ์ž/๊ณต๋ฐฑ ์ œ์™ธ ๋ฌธ์ž ์ œ๊ฑฐ
37
  text = re.sub(r"[^a-z0-9\s]", "", text)
38
- # 3) ๊ณต๋ฐฑ๋‹จ์œ„ ํ† ํฐ
39
  tokens = text.split()
40
- # 4) ์šฐ์„ ์€ ์•ž์—์„œ ๋ช‡ ๊ฐœ ํ† ํฐ๋งŒ ์‚ฌ์šฉ (top_k=5)
41
- # - ํ•„์š”์‹œ stopword ์ œ๊ฑฐ๋‚˜ ๋นˆ๋„์ˆ˜ ๊ณ„์‚ฐ ํ›„ ์ƒ์œ„ k๊ฐœ ์ถ”์ถœํ•˜๋„๋ก ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ
42
  key_tokens = tokens[:top_k]
43
- # 5) ๊ณต๋ฐฑ์œผ๋กœ join
44
  return " ".join(key_tokens)
45
 
46
  ##############################################################################
47
- # Simple function to call the SERPHouse Live endpoint
48
- # https://api.serphouse.com/serp/live
49
  ##############################################################################
50
  def do_web_search(query: str) -> str:
51
  """
52
- Calls SERPHouse live endpoint with the given query (q).
53
- Returns top-20 results' titles as a bullet list, or an error message.
54
  """
55
  try:
56
  url = "https://api.serphouse.com/serp/live"
@@ -60,11 +58,11 @@ def do_web_search(query: str) -> str:
60
  "lang": "en",
61
  "device": "desktop",
62
  "serp_type": "web",
63
- "num_result": "20", # [์ƒˆ๋กœ ์ถ”๊ฐ€] ์ƒ์œ„ 20๊ฐœ ๊ฒฐ๊ณผ
64
  "api_token": SERPHOUSE_API_KEY,
65
  }
66
  resp = requests.get(url, params=params, timeout=30)
67
- resp.raise_for_status() # Raise an exception for 4xx/5xx errors
68
  data = resp.json()
69
 
70
  results = data.get("results", {})
@@ -72,21 +70,21 @@ def do_web_search(query: str) -> str:
72
  if not organic:
73
  return "No web search results found."
74
 
75
- # ์ƒ์œ„ 20๊ฐœ ์ œ๋ชฉ๋งŒ ๋ฝ‘์•„์„œ ์ •๋ฆฌ
76
  summary_lines = []
77
  for idx, item in enumerate(organic[:20], start=1):
78
  title = item.get("title", "No Title")
79
  summary_lines.append(f"{idx}. {title}")
80
 
81
- # 20๊ฐœ๋ฅผ \n ์œผ๋กœ ์—ฐ๊ฒฐ
82
  return "\n".join(summary_lines)
83
  except Exception as e:
84
  logger.error(f"Web search failed: {e}")
85
  return f"Web search failed: {str(e)}"
86
 
87
 
88
- MAX_CONTENT_CHARS = 4000 # ๋„ˆ๋ฌด ํฐ ํŒŒ์ผ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ์ตœ๋Œ€ ํ‘œ์‹œ 4000์ž
89
-
 
 
90
  model_id = os.getenv("MODEL_ID", "google/gemma-3-27b-it")
91
  processor = AutoProcessor.from_pretrained(model_id, padding_side="left")
92
  model = Gemma3ForConditionalGeneration.from_pretrained(
@@ -95,16 +93,19 @@ model = Gemma3ForConditionalGeneration.from_pretrained(
95
  torch_dtype=torch.bfloat16,
96
  attn_implementation="eager"
97
  )
98
-
99
  MAX_NUM_IMAGES = int(os.getenv("MAX_NUM_IMAGES", "5"))
100
 
101
 
102
- ##################################################
103
  # CSV, TXT, PDF ๋ถ„์„ ํ•จ์ˆ˜
104
- ##################################################
105
  def analyze_csv_file(path: str) -> str:
 
 
 
106
  try:
107
  df = pd.read_csv(path)
 
108
  if df.shape[0] > 50 or df.shape[1] > 10:
109
  df = df.iloc[:50, :10]
110
  df_str = df.to_string()
@@ -116,6 +117,9 @@ def analyze_csv_file(path: str) -> str:
116
 
117
 
118
  def analyze_txt_file(path: str) -> str:
 
 
 
119
  try:
120
  with open(path, "r", encoding="utf-8") as f:
121
  text = f.read()
@@ -127,6 +131,9 @@ def analyze_txt_file(path: str) -> str:
127
 
128
 
129
  def pdf_to_markdown(pdf_path: str) -> str:
 
 
 
130
  text_chunks = []
131
  try:
132
  with open(pdf_path, "rb") as f:
@@ -137,6 +144,7 @@ def pdf_to_markdown(pdf_path: str) -> str:
137
  page_text = page.extract_text() or ""
138
  page_text = page_text.strip()
139
  if page_text:
 
140
  if len(page_text) > MAX_CONTENT_CHARS // max_pages:
141
  page_text = page_text[:MAX_CONTENT_CHARS // max_pages] + "...(truncated)"
142
  text_chunks.append(f"## Page {page_num+1}\n\n{page_text}\n")
@@ -152,9 +160,9 @@ def pdf_to_markdown(pdf_path: str) -> str:
152
  return f"**[PDF File: {os.path.basename(pdf_path)}]**\n\n{full_text}"
153
 
154
 
155
- ##################################################
156
  # ์ด๋ฏธ์ง€/๋น„๋””์˜ค ์—…๋กœ๋“œ ์ œํ•œ ๊ฒ€์‚ฌ
157
- ##################################################
158
  def count_files_in_new_message(paths: list[str]) -> tuple[int, int]:
159
  image_count = 0
160
  video_count = 0
@@ -183,6 +191,13 @@ def count_files_in_history(history: list[dict]) -> tuple[int, int]:
183
 
184
 
185
  def validate_media_constraints(message: dict, history: list[dict]) -> bool:
 
 
 
 
 
 
 
186
  media_files = []
187
  for f in message["files"]:
188
  if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE) or f.endswith(".mp4"):
@@ -217,14 +232,14 @@ def validate_media_constraints(message: dict, history: list[dict]) -> bool:
217
  return True
218
 
219
 
220
- ##################################################
221
  # ๋น„๋””์˜ค ์ฒ˜๋ฆฌ
222
- ##################################################
223
  def downsample_video(video_path: str) -> list[tuple[Image.Image, float]]:
224
  vidcap = cv2.VideoCapture(video_path)
225
  fps = vidcap.get(cv2.CAP_PROP_FPS)
226
  total_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
227
- frame_interval = max(int(fps), int(total_frames / 10))
228
  frames = []
229
 
230
  for i in range(0, total_frames, frame_interval):
@@ -255,9 +270,9 @@ def process_video(video_path: str) -> list[dict]:
255
  return content
256
 
257
 
258
- ##################################################
259
  # interleaved <image> ์ฒ˜๋ฆฌ
260
- ##################################################
261
  def process_interleaved_images(message: dict) -> list[dict]:
262
  parts = re.split(r"(<image>)", message["text"])
263
  content = []
@@ -277,9 +292,9 @@ def process_interleaved_images(message: dict) -> list[dict]:
277
  return content
278
 
279
 
280
- ##################################################
281
  # PDF + CSV + TXT + ์ด๋ฏธ์ง€/๋น„๋””์˜ค
282
- ##################################################
283
  def is_image_file(file_path: str) -> bool:
284
  return bool(re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE))
285
 
@@ -287,9 +302,12 @@ def is_video_file(file_path: str) -> bool:
287
  return file_path.endswith(".mp4")
288
 
289
  def is_document_file(file_path: str) -> bool:
290
- return (file_path.lower().endswith(".pdf") or
291
- file_path.lower().endswith(".csv") or
292
- file_path.lower().endswith(".txt"))
 
 
 
293
 
294
  def process_new_user_message(message: dict) -> list[dict]:
295
  if not message["files"]:
@@ -321,7 +339,7 @@ def process_new_user_message(message: dict) -> list[dict]:
321
 
322
  if "<image>" in message["text"] and image_files:
323
  interleaved_content = process_interleaved_images({"text": message["text"], "files": image_files})
324
- if content_list[0]["type"] == "text":
325
  content_list = content_list[1:]
326
  return interleaved_content + content_list
327
  else:
@@ -331,9 +349,9 @@ def process_new_user_message(message: dict) -> list[dict]:
331
  return content_list
332
 
333
 
334
- ##################################################
335
  # history -> LLM ๋ฉ”์‹œ์ง€ ๋ณ€ํ™˜
336
- ##################################################
337
  def process_history(history: list[dict]) -> list[dict]:
338
  messages = []
339
  current_user_content: list[dict] = []
@@ -360,9 +378,9 @@ def process_history(history: list[dict]) -> list[dict]:
360
  return messages
361
 
362
 
363
- ##################################################
364
- # ๋ฉ”์ธ ์ถ”๋ก  ํ•จ์ˆ˜
365
- ##################################################
366
  @spaces.GPU(duration=120)
367
  def run(
368
  message: dict,
@@ -378,19 +396,18 @@ def run(
378
  return
379
 
380
  try:
381
- # [์ƒˆ๋กœ ์ถ”๊ฐ€] web search ์ฒดํฌ๋œ ๊ฒฝ์šฐ, ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ "web_search_query" ๋Œ€์‹ 
382
- # ์‚ฌ์šฉ์ž์˜ ๋ฉ”์‹œ์ง€์—์„œ ํ‚ค์›Œ๋“œ๋ฅผ ์ถ”์ถœํ•˜์—ฌ ๊ฒ€์ƒ‰
 
383
  if use_web_search:
384
  user_text = message["text"]
385
- # ํ‚ค์›Œ๋“œ ์ถ”์ถœ
386
  ws_query = extract_keywords(user_text, top_k=5)
387
  logger.info(f"[Auto WebSearch Keyword] {ws_query!r}")
388
- # ์ƒ์œ„ 20๊ฐœ ๊ฒฐ๊ณผ ๊ฐ€์ ธ์˜ค๊ธฐ
389
  ws_result = do_web_search(ws_query)
390
- # ๊ฒ€์ƒ‰๋œ 20๊ฐœ ์ œ๋ชฉ์„ system ๋ฉ”์‹œ์ง€์— ์ถ”๊ฐ€
391
  system_search_content = f"[Search top-20 Titles Based on user prompt]\n{ws_result}\n"
392
- # system ๋ฉ”์‹œ์ง€๋กœ ์ถ”๊ฐ€
393
- # (LLM์ด ์ด ์ •๋ณด๋ฅผ ์ฐธ๊ณ ํ•˜๋„๋ก)
394
  if system_search_content.strip():
395
  history_system_msg = {
396
  "role": "system",
@@ -401,25 +418,26 @@ def run(
401
  "role": "system",
402
  "content": [{"type": "text", "text": "No web search results"}]
403
  }
404
- else:
405
- history_system_msg = None
406
 
 
407
  messages = []
408
  if system_prompt:
409
  messages.append({"role": "system", "content": [{"type": "text", "text": system_prompt}]})
410
- # ๋งŒ์•ฝ web search๊ฐ€ ์žˆ์—ˆ๋‹ค๋ฉด, ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์ถ”๊ฐ€ system ๋ฉ”์‹œ์ง€๋กœ ์‚ฝ์ž…
411
  if history_system_msg:
412
  messages.append(history_system_msg)
413
 
 
414
  messages.extend(process_history(history))
415
 
 
416
  user_content = process_new_user_message(message)
417
  for item in user_content:
418
  if item["type"] == "text" and len(item["text"]) > MAX_CONTENT_CHARS:
419
  item["text"] = item["text"][:MAX_CONTENT_CHARS] + "\n...(truncated)..."
420
-
421
  messages.append({"role": "user", "content": user_content})
422
 
 
423
  inputs = processor.apply_chat_template(
424
  messages,
425
  add_generation_prompt=True,
@@ -442,15 +460,16 @@ def run(
442
  for new_text in streamer:
443
  output += new_text
444
  yield output
445
-
446
  except Exception as e:
447
  logger.error(f"Error in run: {str(e)}")
448
  yield f"์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
449
 
450
 
451
-
 
 
452
  examples = [
453
-
454
  [
455
  {
456
  "text": "๋‘ PDF ํŒŒ์ผ ๋‚ด์šฉ์„ ๋น„๊ตํ•˜๋ผ.",
@@ -458,7 +477,7 @@ examples = [
458
  "files": [
459
  "assets/additional-examples/before.pdf",
460
  "assets/additional-examples/after.pdf",
461
- ],
462
  }
463
  ],
464
  [
@@ -466,37 +485,37 @@ examples = [
466
  "text": "CSV ํŒŒ์ผ ๋‚ด์šฉ์„ ์š”์•ฝ, ๋ถ„์„ํ•˜๋ผ",
467
  "files": ["assets/additional-examples/sample-csv.csv"],
468
  }
469
- ],
470
  [
471
  {
472
  "text": "์ด ์˜์ƒ์˜ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•˜๋ผ",
473
  "files": ["assets/additional-examples/tmp.mp4"],
474
  }
475
- ],
476
  [
477
  {
478
  "text": "ํ‘œ์ง€ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•˜๊ณ  ๊ธ€์ž๋ฅผ ์ฝ์–ด์ฃผ์„ธ์š”.",
479
  "files": ["assets/additional-examples/maz.jpg"],
480
  }
481
- ],
482
  [
483
  {
484
  "text": "์ด๋ฏธ ์ด ์˜์–‘์ œ๋ฅผ <image> ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ์ด ์ œํ’ˆ <image>์„ ์ƒˆ๋กœ ์‚ฌ๋ ค ํ•ฉ๋‹ˆ๋‹ค. ํ•จ๊ป˜ ์„ญ์ทจํ•  ๋•Œ ์ฃผ์˜ํ•ด์•ผ ํ•  ์ ์ด ์žˆ์„๊นŒ์š”?",
485
  "files": ["assets/additional-examples/pill1.png", "assets/additional-examples/pill2.png"],
486
  }
487
- ],
488
  [
489
  {
490
  "text": "์ด ์ ๋ถ„๏ฟฝ๏ฟฝ๏ฟฝ ํ’€์–ด์ฃผ์„ธ์š”.",
491
  "files": ["assets/additional-examples/4.png"],
492
  }
493
- ],
494
  [
495
  {
496
  "text": "์ด ํ‹ฐ์ผ“์€ ์–ธ์ œ ๋ฐœ๊ธ‰๋œ ๊ฒƒ์ด๊ณ , ๊ฐ€๊ฒฉ์€ ์–ผ๋งˆ์ธ๊ฐ€์š”?",
497
  "files": ["assets/additional-examples/2.png"],
498
  }
499
- ],
500
  [
501
  {
502
  "text": "์ด๋ฏธ์ง€๋“ค์˜ ์ˆœ์„œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์งง์€ ์ด์•ผ๊ธฐ๋ฅผ ๋งŒ๋“ค์–ด ์ฃผ์„ธ์š”.",
@@ -520,24 +539,19 @@ examples = [
520
  "text": "๋™์ผํ•œ ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„๋ฅผ ๊ทธ๋ฆฌ๋Š” matplotlib ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”.",
521
  "files": ["assets/additional-examples/barchart.png"],
522
  }
523
- ],
524
-
525
  [
526
  {
527
  "text": "์ด ์„ธ๊ณ„์—์„œ ์‚ด๊ณ  ์žˆ์„ ์ƒ๋ฌผ๋“ค์„ ์ƒ์ƒํ•ด์„œ ๋ฌ˜์‚ฌํ•ด์ฃผ์„ธ์š”.",
528
  "files": ["assets/sample-images/08.png"],
529
  }
530
  ],
531
-
532
-
533
  [
534
  {
535
  "text": "์ด๋ฏธ์ง€์— ์žˆ๋Š” ํ…์ŠคํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ์ฝ์–ด์„œ ๋งˆํฌ๋‹ค์šด ํ˜•ํƒœ๋กœ ์ ์–ด์ฃผ์„ธ์š”.",
536
  "files": ["assets/additional-examples/3.png"],
537
  }
538
  ],
539
-
540
-
541
  [
542
  {
543
  "text": "์ด ํ‘œ์ง€ํŒ์—๋Š” ๋ฌด์Šจ ๋ฌธ๊ตฌ๊ฐ€ ์ ํ˜€ ์žˆ๋‚˜์š”?",
@@ -550,10 +564,12 @@ examples = [
550
  "files": ["assets/sample-images/03.png"],
551
  }
552
  ],
553
-
554
  ]
555
 
556
 
 
 
 
557
  css = """
558
  body {
559
  background: linear-gradient(135deg, #667eea, #764ba2);
@@ -626,9 +642,9 @@ with gr.Blocks(css=css, title="Vidraft-Gemma-3-27B") as demo:
626
  web_search_checkbox = gr.Checkbox(
627
  label="Web Search",
628
  value=False,
629
- info="Check to enable a SERPHouse web search before the chat reply"
630
  )
631
- # [์ค‘์š”] web_search_text๋Š” ์‚ฌ์‹ค์ƒ ์‚ฌ์šฉ ์•ˆ ํ•จ (์ž๋™์ถ”์ถœ๋กœ ๊ฒ€์ƒ‰)
632
  web_search_text = gr.Textbox(
633
  lines=1,
634
  label="(Unused) Web Search Query",
@@ -653,8 +669,9 @@ with gr.Blocks(css=css, title="Vidraft-Gemma-3-27B") as demo:
653
  value=2000,
654
  )
655
 
656
- gr.Markdown("<br><br>")
657
 
 
658
  with gr.Column(scale=7):
659
  chat = gr.ChatInterface(
660
  fn=run,
@@ -673,7 +690,7 @@ with gr.Blocks(css=css, title="Vidraft-Gemma-3-27B") as demo:
673
  system_prompt_box,
674
  max_tokens_slider,
675
  web_search_checkbox,
676
- web_search_text, # ์‹ค์ œ๋กœ๋Š” ์‚ฌ์šฉ ์•ˆํ•จ
677
  ],
678
  stop_btn=False,
679
  title="Vidraft-Gemma-3-27B",
@@ -689,7 +706,7 @@ with gr.Blocks(css=css, title="Vidraft-Gemma-3-27B") as demo:
689
  gr.Markdown("### Example Inputs (click to load)")
690
  gr.Examples(
691
  examples=examples,
692
- inputs=[], # Gradio๊ฐ€ dataset์— ์—ฐ๊ฒฐํ•  inputs๊ฐ€ ์—†์œผ๋ฏ€๋กœ ๋นˆ ๋ฆฌ์ŠคํŠธ
693
  cache_examples=False
694
  )
695
 
 
22
  import PyPDF2
23
 
24
  ##############################################################################
25
+ # SERPHouse API key from environment variable
26
  ##############################################################################
27
+ SERPHOUSE_API_KEY = os.getenv("SERPHOUSE_API_KEY", "")
28
 
29
  ##############################################################################
30
+ # ๊ฐ„๋‹จํ•œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ ํ•จ์ˆ˜ (์‚ฌ์šฉ์ž ํ”„๋กฌํ”„ํŠธ -> ํ‚ค์›Œ๋“œ)
 
31
  ##############################################################################
32
  def extract_keywords(text: str, top_k: int = 5) -> str:
33
+ """
34
+ ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ:
35
+ 1) ํ…์ŠคํŠธ๋ฅผ ์†Œ๋ฌธ์ž๋กœ
36
+ 2) ์•ŒํŒŒ๋ฒณ/์ˆซ์ž/๊ณต๋ฐฑ ์ œ์™ธ ๋ฌธ์ž ์ œ๊ฑฐ
37
+ 3) ๊ณต๋ฐฑ ํ† ํฐ ๋ถ„๋ฆฌ
38
+ 4) ์•ž ํ† ํฐ n๊ฐœ ์ถ”์ถœ
39
+ """
40
  text = text.lower()
 
41
  text = re.sub(r"[^a-z0-9\s]", "", text)
 
42
  tokens = text.split()
 
 
43
  key_tokens = tokens[:top_k]
 
44
  return " ".join(key_tokens)
45
 
46
  ##############################################################################
47
+ # SERPHouse Live endpoint ํ˜ธ์ถœ (์ƒ์œ„ 20๊ฐœ์˜ ์ œ๋ชฉ์„ ์–ป์Œ)
 
48
  ##############################################################################
49
  def do_web_search(query: str) -> str:
50
  """
51
+ SERPHouse ๋ผ์ด๋ธŒ ๊ฒ€์ƒ‰ ํ˜ธ์ถœ, ์ƒ์œ„ 20๊ฐœ ๊ฒฐ๊ณผ์˜ 'title'๋งŒ ๋ฌถ์–ด์„œ ๋ฐ˜ํ™˜.
 
52
  """
53
  try:
54
  url = "https://api.serphouse.com/serp/live"
 
58
  "lang": "en",
59
  "device": "desktop",
60
  "serp_type": "web",
61
+ "num_result": "20", # ์ƒ์œ„ 20๊ฐœ ๊ฒฐ๊ณผ
62
  "api_token": SERPHOUSE_API_KEY,
63
  }
64
  resp = requests.get(url, params=params, timeout=30)
65
+ resp.raise_for_status() # 4xx/5xx ์—๋Ÿฌ ์‹œ ์˜ˆ์™ธ
66
  data = resp.json()
67
 
68
  results = data.get("results", {})
 
70
  if not organic:
71
  return "No web search results found."
72
 
 
73
  summary_lines = []
74
  for idx, item in enumerate(organic[:20], start=1):
75
  title = item.get("title", "No Title")
76
  summary_lines.append(f"{idx}. {title}")
77
 
 
78
  return "\n".join(summary_lines)
79
  except Exception as e:
80
  logger.error(f"Web search failed: {e}")
81
  return f"Web search failed: {str(e)}"
82
 
83
 
84
+ ##############################################################################
85
+ # ์ƒ์ˆ˜ ์„ค์ •
86
+ ##############################################################################
87
+ MAX_CONTENT_CHARS = 4000 # ๋„ˆ๋ฌด ํฐ ํŒŒ์ผ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ์ตœ๋Œ€ 4000์ž๋งŒ ํ‘œ์‹œ
88
  model_id = os.getenv("MODEL_ID", "google/gemma-3-27b-it")
89
  processor = AutoProcessor.from_pretrained(model_id, padding_side="left")
90
  model = Gemma3ForConditionalGeneration.from_pretrained(
 
93
  torch_dtype=torch.bfloat16,
94
  attn_implementation="eager"
95
  )
 
96
  MAX_NUM_IMAGES = int(os.getenv("MAX_NUM_IMAGES", "5"))
97
 
98
 
99
+ ##############################################################################
100
  # CSV, TXT, PDF ๋ถ„์„ ํ•จ์ˆ˜
101
+ ##############################################################################
102
  def analyze_csv_file(path: str) -> str:
103
+ """
104
+ CSV ํŒŒ์ผ์„ ์ „์ฒด ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜. ๋„ˆ๋ฌด ๊ธธ ๊ฒฝ์šฐ ์ผ๋ถ€๋งŒ ํ‘œ์‹œ.
105
+ """
106
  try:
107
  df = pd.read_csv(path)
108
+ # ์ตœ๋Œ€ 50ํ–‰, 10์—ด๊นŒ์ง€๋งŒ ํ‘œ์‹œ
109
  if df.shape[0] > 50 or df.shape[1] > 10:
110
  df = df.iloc[:50, :10]
111
  df_str = df.to_string()
 
117
 
118
 
119
  def analyze_txt_file(path: str) -> str:
120
+ """
121
+ TXT ํŒŒ์ผ ์ „๋ฌธ ์ฝ๊ธฐ. ๋„ˆ๋ฌด ๊ธธ๋ฉด ์ผ๋ถ€๋งŒ ํ‘œ์‹œ.
122
+ """
123
  try:
124
  with open(path, "r", encoding="utf-8") as f:
125
  text = f.read()
 
131
 
132
 
133
  def pdf_to_markdown(pdf_path: str) -> str:
134
+ """
135
+ PDF โ†’ Markdown. ํŽ˜์ด์ง€๋ณ„๋กœ ๊ฐ„๋‹จํžˆ ํ…์ŠคํŠธ ์ถ”์ถœ.
136
+ """
137
  text_chunks = []
138
  try:
139
  with open(pdf_path, "rb") as f:
 
144
  page_text = page.extract_text() or ""
145
  page_text = page_text.strip()
146
  if page_text:
147
+ # ํŽ˜์ด์ง€๋ณ„ ํ…์ŠคํŠธ ์ œํ•œ
148
  if len(page_text) > MAX_CONTENT_CHARS // max_pages:
149
  page_text = page_text[:MAX_CONTENT_CHARS // max_pages] + "...(truncated)"
150
  text_chunks.append(f"## Page {page_num+1}\n\n{page_text}\n")
 
160
  return f"**[PDF File: {os.path.basename(pdf_path)}]**\n\n{full_text}"
161
 
162
 
163
+ ##############################################################################
164
  # ์ด๋ฏธ์ง€/๋น„๋””์˜ค ์—…๋กœ๋“œ ์ œํ•œ ๊ฒ€์‚ฌ
165
+ ##############################################################################
166
  def count_files_in_new_message(paths: list[str]) -> tuple[int, int]:
167
  image_count = 0
168
  video_count = 0
 
191
 
192
 
193
  def validate_media_constraints(message: dict, history: list[dict]) -> bool:
194
+ """
195
+ - ๋น„๋””์˜ค 1๊ฐœ ์ดˆ๊ณผ ๋ถˆ๊ฐ€
196
+ - ๋น„๋””์˜ค์™€ ์ด๋ฏธ์ง€ ํ˜ผํ•ฉ ๋ถˆ๊ฐ€
197
+ - ์ด๋ฏธ์ง€ ๊ฐœ์ˆ˜(MAX_NUM_IMAGES) ์ดˆ๊ณผ ๋ถˆ๊ฐ€
198
+ - <image> ํƒœ๊ทธ๊ฐ€ ์žˆ์œผ๋ฉด ํƒœ๊ทธ ์ˆ˜์™€ ์‹ค์ œ ์ด๋ฏธ์ง€ ์ˆ˜ ์ผ์น˜
199
+ - CSV, TXT, PDF ๋“ฑ์€ ์—ฌ๊ธฐ์„œ ์ œํ•œํ•˜์ง€ ์•Š์Œ
200
+ """
201
  media_files = []
202
  for f in message["files"]:
203
  if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE) or f.endswith(".mp4"):
 
232
  return True
233
 
234
 
235
+ ##############################################################################
236
  # ๋น„๋””์˜ค ์ฒ˜๋ฆฌ
237
+ ##############################################################################
238
  def downsample_video(video_path: str) -> list[tuple[Image.Image, float]]:
239
  vidcap = cv2.VideoCapture(video_path)
240
  fps = vidcap.get(cv2.CAP_PROP_FPS)
241
  total_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
242
+ frame_interval = max(int(fps), int(total_frames / 10))
243
  frames = []
244
 
245
  for i in range(0, total_frames, frame_interval):
 
270
  return content
271
 
272
 
273
+ ##############################################################################
274
  # interleaved <image> ์ฒ˜๋ฆฌ
275
+ ##############################################################################
276
  def process_interleaved_images(message: dict) -> list[dict]:
277
  parts = re.split(r"(<image>)", message["text"])
278
  content = []
 
292
  return content
293
 
294
 
295
+ ##############################################################################
296
  # PDF + CSV + TXT + ์ด๋ฏธ์ง€/๋น„๋””์˜ค
297
+ ##############################################################################
298
  def is_image_file(file_path: str) -> bool:
299
  return bool(re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE))
300
 
 
302
  return file_path.endswith(".mp4")
303
 
304
  def is_document_file(file_path: str) -> bool:
305
+ return (
306
+ file_path.lower().endswith(".pdf")
307
+ or file_path.lower().endswith(".csv")
308
+ or file_path.lower().endswith(".txt")
309
+ )
310
+
311
 
312
  def process_new_user_message(message: dict) -> list[dict]:
313
  if not message["files"]:
 
339
 
340
  if "<image>" in message["text"] and image_files:
341
  interleaved_content = process_interleaved_images({"text": message["text"], "files": image_files})
342
+ if content_list and content_list[0]["type"] == "text":
343
  content_list = content_list[1:]
344
  return interleaved_content + content_list
345
  else:
 
349
  return content_list
350
 
351
 
352
+ ##############################################################################
353
  # history -> LLM ๋ฉ”์‹œ์ง€ ๋ณ€ํ™˜
354
+ ##############################################################################
355
  def process_history(history: list[dict]) -> list[dict]:
356
  messages = []
357
  current_user_content: list[dict] = []
 
378
  return messages
379
 
380
 
381
+ ##############################################################################
382
+ # ๋ฉ”์ธ ์ถ”๋ก  ํ•จ์ˆ˜ (web search ์ฒดํฌ ์‹œ ์ž๋™ ํ‚ค์›Œ๋“œ์ถ”์ถœ->๊ฒ€์ƒ‰->๊ฒฐ๊ณผ system msg ๋ฐ˜์˜)
383
+ ##############################################################################
384
  @spaces.GPU(duration=120)
385
  def run(
386
  message: dict,
 
396
  return
397
 
398
  try:
399
+ # web_search๊ฐ€ True๋ฉด => ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ž…๋ ฅํ•œ web_search_query ๋Œ€์‹ ,
400
+ # message["text"]๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ‚ค์›Œ๋“œ ์ถ”์ถœํ•˜์—ฌ ๊ฒ€์ƒ‰
401
+ history_system_msg = None
402
  if use_web_search:
403
  user_text = message["text"]
404
+ # 1) ํ‚ค์›Œ๋“œ ์ถ”์ถœ
405
  ws_query = extract_keywords(user_text, top_k=5)
406
  logger.info(f"[Auto WebSearch Keyword] {ws_query!r}")
407
+ # 2) ์ƒ์œ„ 20๊ฐœ ๊ฒฐ๊ณผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
408
  ws_result = do_web_search(ws_query)
409
+ # 3) ์ด๋ฅผ system ๋ฉ”์‹œ์ง€๋กœ ์ถ”๊ฐ€
410
  system_search_content = f"[Search top-20 Titles Based on user prompt]\n{ws_result}\n"
 
 
411
  if system_search_content.strip():
412
  history_system_msg = {
413
  "role": "system",
 
418
  "role": "system",
419
  "content": [{"type": "text", "text": "No web search results"}]
420
  }
 
 
421
 
422
+ # ๊ธฐ์กด system prompt
423
  messages = []
424
  if system_prompt:
425
  messages.append({"role": "system", "content": [{"type": "text", "text": system_prompt}]})
426
+ # web ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ system msg
427
  if history_system_msg:
428
  messages.append(history_system_msg)
429
 
430
+ # ์ด์ „ ๋Œ€ํ™”์ด๋ ฅ(assistant/user)
431
  messages.extend(process_history(history))
432
 
433
+ # ์ƒˆ ์œ ์ € ๋ฉ”์‹œ์ง€ ๋ณ€ํ™˜
434
  user_content = process_new_user_message(message)
435
  for item in user_content:
436
  if item["type"] == "text" and len(item["text"]) > MAX_CONTENT_CHARS:
437
  item["text"] = item["text"][:MAX_CONTENT_CHARS] + "\n...(truncated)..."
 
438
  messages.append({"role": "user", "content": user_content})
439
 
440
+ # LLM ์ž…๋ ฅ ์ƒ์„ฑ
441
  inputs = processor.apply_chat_template(
442
  messages,
443
  add_generation_prompt=True,
 
460
  for new_text in streamer:
461
  output += new_text
462
  yield output
463
+
464
  except Exception as e:
465
  logger.error(f"Error in run: {str(e)}")
466
  yield f"์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
467
 
468
 
469
+ ##############################################################################
470
+ # ์˜ˆ์‹œ๋“ค (ํ•œ๊ธ€ํ™”)
471
+ ##############################################################################
472
  examples = [
 
473
  [
474
  {
475
  "text": "๋‘ PDF ํŒŒ์ผ ๋‚ด์šฉ์„ ๋น„๊ตํ•˜๋ผ.",
 
477
  "files": [
478
  "assets/additional-examples/before.pdf",
479
  "assets/additional-examples/after.pdf",
480
+ ],
481
  }
482
  ],
483
  [
 
485
  "text": "CSV ํŒŒ์ผ ๋‚ด์šฉ์„ ์š”์•ฝ, ๋ถ„์„ํ•˜๋ผ",
486
  "files": ["assets/additional-examples/sample-csv.csv"],
487
  }
488
+ ],
489
  [
490
  {
491
  "text": "์ด ์˜์ƒ์˜ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•˜๋ผ",
492
  "files": ["assets/additional-examples/tmp.mp4"],
493
  }
494
+ ],
495
  [
496
  {
497
  "text": "ํ‘œ์ง€ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•˜๊ณ  ๊ธ€์ž๋ฅผ ์ฝ์–ด์ฃผ์„ธ์š”.",
498
  "files": ["assets/additional-examples/maz.jpg"],
499
  }
500
+ ],
501
  [
502
  {
503
  "text": "์ด๋ฏธ ์ด ์˜์–‘์ œ๋ฅผ <image> ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ์ด ์ œํ’ˆ <image>์„ ์ƒˆ๋กœ ์‚ฌ๋ ค ํ•ฉ๋‹ˆ๋‹ค. ํ•จ๊ป˜ ์„ญ์ทจํ•  ๋•Œ ์ฃผ์˜ํ•ด์•ผ ํ•  ์ ์ด ์žˆ์„๊นŒ์š”?",
504
  "files": ["assets/additional-examples/pill1.png", "assets/additional-examples/pill2.png"],
505
  }
506
+ ],
507
  [
508
  {
509
  "text": "์ด ์ ๋ถ„๏ฟฝ๏ฟฝ๏ฟฝ ํ’€์–ด์ฃผ์„ธ์š”.",
510
  "files": ["assets/additional-examples/4.png"],
511
  }
512
+ ],
513
  [
514
  {
515
  "text": "์ด ํ‹ฐ์ผ“์€ ์–ธ์ œ ๋ฐœ๊ธ‰๋œ ๊ฒƒ์ด๊ณ , ๊ฐ€๊ฒฉ์€ ์–ผ๋งˆ์ธ๊ฐ€์š”?",
516
  "files": ["assets/additional-examples/2.png"],
517
  }
518
+ ],
519
  [
520
  {
521
  "text": "์ด๋ฏธ์ง€๋“ค์˜ ์ˆœ์„œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์งง์€ ์ด์•ผ๊ธฐ๋ฅผ ๋งŒ๋“ค์–ด ์ฃผ์„ธ์š”.",
 
539
  "text": "๋™์ผํ•œ ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„๋ฅผ ๊ทธ๋ฆฌ๋Š” matplotlib ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”.",
540
  "files": ["assets/additional-examples/barchart.png"],
541
  }
542
+ ],
 
543
  [
544
  {
545
  "text": "์ด ์„ธ๊ณ„์—์„œ ์‚ด๊ณ  ์žˆ์„ ์ƒ๋ฌผ๋“ค์„ ์ƒ์ƒํ•ด์„œ ๋ฌ˜์‚ฌํ•ด์ฃผ์„ธ์š”.",
546
  "files": ["assets/sample-images/08.png"],
547
  }
548
  ],
 
 
549
  [
550
  {
551
  "text": "์ด๋ฏธ์ง€์— ์žˆ๋Š” ํ…์ŠคํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ์ฝ์–ด์„œ ๋งˆํฌ๋‹ค์šด ํ˜•ํƒœ๋กœ ์ ์–ด์ฃผ์„ธ์š”.",
552
  "files": ["assets/additional-examples/3.png"],
553
  }
554
  ],
 
 
555
  [
556
  {
557
  "text": "์ด ํ‘œ์ง€ํŒ์—๋Š” ๋ฌด์Šจ ๋ฌธ๊ตฌ๊ฐ€ ์ ํ˜€ ์žˆ๋‚˜์š”?",
 
564
  "files": ["assets/sample-images/03.png"],
565
  }
566
  ],
 
567
  ]
568
 
569
 
570
+ ##############################################################################
571
+ # Gradio UI (Blocks) ๊ตฌ์„ฑ
572
+ ##############################################################################
573
  css = """
574
  body {
575
  background: linear-gradient(135deg, #667eea, #764ba2);
 
642
  web_search_checkbox = gr.Checkbox(
643
  label="Web Search",
644
  value=False,
645
+ info="Check to enable a SERPHouse web search (auto keywords) before the chat reply"
646
  )
647
+ # ์‹ค์ œ๋กœ๋Š” ์ž๋™์ถ”์ถœ. ์•„๋ž˜ textbox๋Š” ๋ฏธ์‚ฌ์šฉ.
648
  web_search_text = gr.Textbox(
649
  lines=1,
650
  label="(Unused) Web Search Query",
 
669
  value=2000,
670
  )
671
 
672
+ gr.Markdown("<br><br>") # spacing
673
 
674
+ # Main ChatInterface to the right
675
  with gr.Column(scale=7):
676
  chat = gr.ChatInterface(
677
  fn=run,
 
690
  system_prompt_box,
691
  max_tokens_slider,
692
  web_search_checkbox,
693
+ web_search_text, # ์‹ค์ œ๋กœ๋Š” auto search
694
  ],
695
  stop_btn=False,
696
  title="Vidraft-Gemma-3-27B",
 
706
  gr.Markdown("### Example Inputs (click to load)")
707
  gr.Examples(
708
  examples=examples,
709
+ inputs=[], # ๋งํฌํ•  inputs๊ฐ€ ์—†์œผ๋ฏ€๋กœ ๋นˆ ๋ฆฌ์ŠคํŠธ
710
  cache_examples=False
711
  )
712