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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +64 -123
app.py CHANGED
@@ -26,6 +26,23 @@ import PyPDF2
26
  ##############################################################################
27
  SERPHOUSE_API_KEY = "V38CNn4HXpLtynJQyOeoUensTEYoFy8PBUxKpDqAW1pawT1vfJ2BWtPQ98h6"
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  ##############################################################################
30
  # Simple function to call the SERPHouse Live endpoint
31
  # https://api.serphouse.com/serp/live
@@ -33,7 +50,7 @@ SERPHOUSE_API_KEY = "V38CNn4HXpLtynJQyOeoUensTEYoFy8PBUxKpDqAW1pawT1vfJ2BWtPQ98h
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,27 +60,26 @@ 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)}"
@@ -87,15 +103,10 @@ MAX_NUM_IMAGES = int(os.getenv("MAX_NUM_IMAGES", "5"))
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)..."
@@ -105,9 +116,6 @@ def analyze_csv_file(path: str) -> str:
105
 
106
 
107
  def analyze_txt_file(path: str) -> str:
108
- """
109
- TXT ํŒŒ์ผ ์ „๋ฌธ ์ฝ๊ธฐ. ๋„ˆ๋ฌด ๊ธธ๋ฉด ์ผ๋ถ€๋งŒ ํ‘œ์‹œ.
110
- """
111
  try:
112
  with open(path, "r", encoding="utf-8") as f:
113
  text = f.read()
@@ -119,25 +127,19 @@ def analyze_txt_file(path: str) -> str:
119
 
120
 
121
  def pdf_to_markdown(pdf_path: str) -> str:
122
- """
123
- PDF โ†’ Markdown. ํŽ˜์ด์ง€๋ณ„๋กœ ๊ฐ„๋‹จํžˆ ํ…์ŠคํŠธ ์ถ”์ถœ.
124
- """
125
  text_chunks = []
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:
@@ -181,14 +183,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 +207,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):
@@ -232,9 +224,7 @@ 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 +235,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
 
@@ -275,7 +263,6 @@ def process_interleaved_images(message: dict) -> list[dict]:
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,7 +272,6 @@ 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
@@ -295,66 +281,50 @@ def process_interleaved_images(message: dict) -> list[dict]:
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
 
@@ -369,14 +339,11 @@ def process_history(history: list[dict]) -> list[dict]:
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,10 +352,8 @@ 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
 
@@ -407,60 +372,54 @@ 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,7 +428,6 @@ 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,
@@ -477,11 +435,9 @@ def run(
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
@@ -493,9 +449,6 @@ def run(
493
 
494
 
495
 
496
- ##################################################
497
- # ์˜ˆ์‹œ๋“ค (ํ•œ๊ธ€ํ™” ๋ฒ„์ „)
498
- ##################################################
499
  examples = [
500
 
501
  [
@@ -601,12 +554,6 @@ examples = [
601
  ]
602
 
603
 
604
-
605
-
606
-
607
- ##############################################################################
608
- # Custom CSS similar to second example (colorful background, panel, etc.)
609
- ##############################################################################
610
  css = """
611
  body {
612
  background: linear-gradient(135deg, #667eea, #764ba2);
@@ -668,11 +615,6 @@ title_html = """
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
 
@@ -686,10 +628,11 @@ with gr.Blocks(css=css, title="Vidraft-Gemma-3-27B") as demo:
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("---")
@@ -710,9 +653,8 @@ with gr.Blocks(css=css, title="Vidraft-Gemma-3-27B") as demo:
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,
@@ -731,7 +673,7 @@ with gr.Blocks(css=css, title="Vidraft-Gemma-3-27B") as demo:
731
  system_prompt_box,
732
  max_tokens_slider,
733
  web_search_checkbox,
734
- web_search_text,
735
  ],
736
  stop_btn=False,
737
  title="Vidraft-Gemma-3-27B",
@@ -745,10 +687,9 @@ 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
 
 
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
 
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
  "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", {})
71
  organic = results.get("results", {}).get("organic", [])
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)}"
 
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()
111
  if len(df_str) > MAX_CONTENT_CHARS:
112
  df_str = df_str[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
 
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
 
128
 
129
  def pdf_to_markdown(pdf_path: str) -> str:
 
 
 
130
  text_chunks = []
131
  try:
132
  with open(pdf_path, "rb") as f:
133
  reader = PyPDF2.PdfReader(f)
 
134
  max_pages = min(5, len(reader.pages))
135
  for page_num in range(max_pages):
136
  page = reader.pages[page_num]
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")
 
143
  if len(reader.pages) > max_pages:
144
  text_chunks.append(f"\n...(Showing {max_pages} of {len(reader.pages)} pages)...")
145
  except Exception as e:
 
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"):
 
207
  gr.Warning(f"You can upload up to {MAX_NUM_IMAGES} images.")
208
  return False
209
 
 
210
  if "<image>" in message["text"]:
 
211
  image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
212
  image_tag_count = message["text"].count("<image>")
213
  if image_tag_count != len(image_files):
 
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):
 
235
  pil_image = Image.fromarray(image)
236
  timestamp = round(i / fps, 2)
237
  frames.append((pil_image, timestamp))
 
 
238
  if len(frames) >= 5:
239
  break
240
 
 
263
  content = []
264
  image_index = 0
265
 
 
266
  image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
267
 
268
  for part in parts:
 
272
  elif part.strip():
273
  content.append({"type": "text", "text": part.strip()})
274
  else:
 
275
  if isinstance(part, str) and part != "<image>":
276
  content.append({"type": "text", "text": part})
277
  return content
 
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
 
 
286
  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"]:
296
  return [{"type": "text", "text": message["text"]}]
297
 
 
298
  video_files = [f for f in message["files"] if is_video_file(f)]
299
  image_files = [f for f in message["files"] if is_image_file(f)]
300
  csv_files = [f for f in message["files"] if f.lower().endswith(".csv")]
301
  txt_files = [f for f in message["files"] if f.lower().endswith(".txt")]
302
  pdf_files = [f for f in message["files"] if f.lower().endswith(".pdf")]
303
 
 
304
  content_list = [{"type": "text", "text": message["text"]}]
305
 
 
306
  for csv_path in csv_files:
307
  csv_analysis = analyze_csv_file(csv_path)
308
  content_list.append({"type": "text", "text": csv_analysis})
309
 
 
310
  for txt_path in txt_files:
311
  txt_analysis = analyze_txt_file(txt_path)
312
  content_list.append({"type": "text", "text": txt_analysis})
313
 
 
314
  for pdf_path in pdf_files:
315
  pdf_markdown = pdf_to_markdown(pdf_path)
316
  content_list.append({"type": "text", "text": pdf_markdown})
317
 
 
318
  if video_files:
319
  content_list += process_video(video_files[0])
320
  return content_list
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:
 
328
  for img_path in image_files:
329
  content_list.append({"type": "image", "url": img_path})
330
 
 
339
  current_user_content: list[dict] = []
340
  for item in history:
341
  if item["role"] == "assistant":
 
342
  if current_user_content:
343
  messages.append({"role": "user", "content": current_user_content})
344
  current_user_content = []
 
345
  messages.append({"role": "assistant", "content": [{"type": "text", "text": item["content"]}]})
346
  else:
 
347
  content = item["content"]
348
  if isinstance(content, str):
349
  current_user_content.append({"type": "text", "text": content})
 
352
  if is_image_file(file_path):
353
  current_user_content.append({"type": "image", "url": file_path})
354
  else:
 
355
  current_user_content.append({"type": "text", "text": f"[File: {os.path.basename(file_path)}]"})
356
+
 
357
  if current_user_content:
358
  messages.append({"role": "user", "content": current_user_content})
359
 
 
372
  use_web_search: bool = False,
373
  web_search_query: str = "",
374
  ) -> Iterator[str]:
375
+
 
 
 
 
 
 
376
  if not validate_media_constraints(message, history):
377
  yield ""
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",
397
+ "content": [{"type": "text", "text": system_search_content}]
398
+ }
399
+ else:
400
+ history_system_msg = {
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,
 
428
  return_tensors="pt",
429
  ).to(device=model.device, dtype=torch.bfloat16)
430
 
 
431
  streamer = TextIteratorStreamer(processor, timeout=30.0, skip_prompt=True, skip_special_tokens=True)
432
  gen_kwargs = dict(
433
  inputs,
 
435
  max_new_tokens=max_new_tokens,
436
  )
437
 
 
438
  t = Thread(target=model.generate, kwargs=gen_kwargs)
439
  t.start()
440
 
 
441
  output = ""
442
  for new_text in streamer:
443
  output += new_text
 
449
 
450
 
451
 
 
 
 
452
  examples = [
453
 
454
  [
 
554
  ]
555
 
556
 
 
 
 
 
 
 
557
  css = """
558
  body {
559
  background: linear-gradient(135deg, #667eea, #764ba2);
 
615
  </p>
616
  """
617
 
 
 
 
 
 
618
  with gr.Blocks(css=css, title="Vidraft-Gemma-3-27B") as demo:
619
  gr.Markdown(title_html)
620
 
 
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",
635
+ placeholder="No direct input needed"
636
  )
637
 
638
  gr.Markdown("---")
 
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
  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",
 
687
  with gr.Row(elem_id="examples_row"):
688
  with gr.Column(scale=12, elem_id="examples_container"):
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