seawolf2357's picture
Update app.py
f3a07fd verified
raw
history blame
26 kB
#!/usr/bin/env python
import os
import re
import tempfile
from collections.abc import Iterator
from threading import Thread
import json # โ† JSON ๋ณ€ํ™˜์„ ์œ„ํ•ด ์ถ”๊ฐ€
import requests # SERPHouse web search
import cv2
import gradio as gr
import spaces
import torch
from loguru import logger
from PIL import Image
from transformers import AutoProcessor, Gemma3ForConditionalGeneration, TextIteratorStreamer
# CSV/TXT ๋ถ„์„
import pandas as pd
# PDF ํ…์ŠคํŠธ ์ถ”์ถœ
import PyPDF2
##############################################################################
# SERPHouse API key from environment variable (์‚ฌ์šฉ์ž๊ฐ€ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ง€์ •ํ•ด์•ผ ํ•จ)
##############################################################################
SERPHOUSE_API_KEY = os.getenv("SERPHOUSE_API_KEY", "")
##############################################################################
# ๊ฐ„๋‹จํ•œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ ํ•จ์ˆ˜ (์‚ฌ์šฉ์ž ํ”„๋กฌํ”„ํŠธ -> ํ‚ค์›Œ๋“œ)
##############################################################################
def extract_keywords(text: str, top_k: int = 5) -> str:
"""
๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ:
1) ํ…์ŠคํŠธ๋ฅผ ์†Œ๋ฌธ์ž๋กœ
2) ์•ŒํŒŒ๋ฒณ/์ˆซ์ž/๊ณต๋ฐฑ ์ œ์™ธ ๋ฌธ์ž ์ œ๊ฑฐ
3) ๊ณต๋ฐฑ ํ† ํฐ ๋ถ„๋ฆฌ
4) ์•ž ํ† ํฐ n๊ฐœ ์ถ”์ถœ
"""
text = text.lower()
text = re.sub(r"[^a-z0-9\s]", "", text)
tokens = text.split()
key_tokens = tokens[:top_k]
return " ".join(key_tokens)
##############################################################################
# SERPHouse Live endpoint ํ˜ธ์ถœ
# - ์ƒ์œ„ 20๊ฐœ ๊ฒฐ๊ณผ ๋ชจ๋‘ "์ „์ฒด item"์„ system msg์— ๋‹ด์•„(=JSON ๊ทธ๋Œ€๋กœ) LLM์ด ์ฐธ์กฐ
##############################################################################
def do_web_search(query: str) -> str:
"""
SERPHouse ๋ผ์ด๋ธŒ ๊ฒ€์ƒ‰ ํ˜ธ์ถœ, ์ƒ์œ„ 20๊ฐœ 'organic' ๊ฒฐ๊ณผ ์ „์ฒด๋ฅผ JSON ํ˜•ํƒœ๋กœ ๋ฌถ์–ด์„œ ๋ฐ˜ํ™˜.
"""
try:
url = "https://api.serphouse.com/serp/live"
params = {
"q": query,
"domain": "google.com",
"lang": "en",
"device": "desktop",
"serp_type": "web",
"num_result": "20", # ์ƒ์œ„ 20๊ฐœ ๊ฒฐ๊ณผ
"api_token": SERPHOUSE_API_KEY,
}
resp = requests.get(url, params=params, timeout=30)
resp.raise_for_status() # 4xx/5xx ์—๋Ÿฌ ์‹œ ์˜ˆ์™ธ
data = resp.json()
results = data.get("results", {})
organic = results.get("results", {}).get("organic", [])
if not organic:
return "No web search results found."
# ๊ฐ item์„ JSON(์ „์ฒด ํ•„๋“œ)์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅ
summary_lines = []
for idx, item in enumerate(organic[:20], start=1):
# item ์ „์ฒด๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜
item_json = json.dumps(item, ensure_ascii=False, indent=2)
summary_lines.append(f"Result {idx}:\n{item_json}\n")
return "\n".join(summary_lines)
except Exception as e:
logger.error(f"Web search failed: {e}")
return f"Web search failed: {str(e)}"
##############################################################################
# ์ƒ์ˆ˜ ์„ค์ •
##############################################################################
MAX_CONTENT_CHARS = 4000 # ๋„ˆ๋ฌด ํฐ ํŒŒ์ผ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ์ตœ๋Œ€ 4000์ž๋งŒ ํ‘œ์‹œ
model_id = os.getenv("MODEL_ID", "google/gemma-3-27b-it")
processor = AutoProcessor.from_pretrained(model_id, padding_side="left")
model = Gemma3ForConditionalGeneration.from_pretrained(
model_id,
device_map="auto",
torch_dtype=torch.bfloat16,
attn_implementation="eager"
)
MAX_NUM_IMAGES = int(os.getenv("MAX_NUM_IMAGES", "5"))
##############################################################################
# CSV, TXT, PDF ๋ถ„์„ ํ•จ์ˆ˜
##############################################################################
def analyze_csv_file(path: str) -> str:
"""
CSV ํŒŒ์ผ์„ ์ „์ฒด ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜. ๋„ˆ๋ฌด ๊ธธ ๊ฒฝ์šฐ ์ผ๋ถ€๋งŒ ํ‘œ์‹œ.
"""
try:
df = pd.read_csv(path)
# ์ตœ๋Œ€ 50ํ–‰, 10์—ด๊นŒ์ง€๋งŒ ํ‘œ์‹œ
if df.shape[0] > 50 or df.shape[1] > 10:
df = df.iloc[:50, :10]
df_str = df.to_string()
if len(df_str) > MAX_CONTENT_CHARS:
df_str = df_str[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
return f"**[CSV File: {os.path.basename(path)}]**\n\n{df_str}"
except Exception as e:
return f"Failed to read CSV ({os.path.basename(path)}): {str(e)}"
def analyze_txt_file(path: str) -> str:
"""
TXT ํŒŒ์ผ ์ „๋ฌธ ์ฝ๊ธฐ. ๋„ˆ๋ฌด ๊ธธ๋ฉด ์ผ๋ถ€๋งŒ ํ‘œ์‹œ.
"""
try:
with open(path, "r", encoding="utf-8") as f:
text = f.read()
if len(text) > MAX_CONTENT_CHARS:
text = text[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
return f"**[TXT File: {os.path.basename(path)}]**\n\n{text}"
except Exception as e:
return f"Failed to read TXT ({os.path.basename(path)}): {str(e)}"
def pdf_to_markdown(pdf_path: str) -> str:
"""
PDF โ†’ Markdown. ํŽ˜์ด์ง€๋ณ„๋กœ ๊ฐ„๋‹จํžˆ ํ…์ŠคํŠธ ์ถ”์ถœ.
"""
text_chunks = []
try:
with open(pdf_path, "rb") as f:
reader = PyPDF2.PdfReader(f)
max_pages = min(5, len(reader.pages))
for page_num in range(max_pages):
page = reader.pages[page_num]
page_text = page.extract_text() or ""
page_text = page_text.strip()
if page_text:
if len(page_text) > MAX_CONTENT_CHARS // max_pages:
page_text = page_text[:MAX_CONTENT_CHARS // max_pages] + "...(truncated)"
text_chunks.append(f"## Page {page_num+1}\n\n{page_text}\n")
if len(reader.pages) > max_pages:
text_chunks.append(f"\n...(Showing {max_pages} of {len(reader.pages)} pages)...")
except Exception as e:
return f"Failed to read PDF ({os.path.basename(pdf_path)}): {str(e)}"
full_text = "\n".join(text_chunks)
if len(full_text) > MAX_CONTENT_CHARS:
full_text = full_text[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
return f"**[PDF File: {os.path.basename(pdf_path)}]**\n\n{full_text}"
##############################################################################
# ์ด๋ฏธ์ง€/๋น„๋””์˜ค ์—…๋กœ๋“œ ์ œํ•œ ๊ฒ€์‚ฌ
##############################################################################
def count_files_in_new_message(paths: list[str]) -> tuple[int, int]:
image_count = 0
video_count = 0
for path in paths:
if path.endswith(".mp4"):
video_count += 1
elif re.search(r"\.(png|jpg|jpeg|gif|webp)$", path, re.IGNORECASE):
image_count += 1
return image_count, video_count
def count_files_in_history(history: list[dict]) -> tuple[int, int]:
image_count = 0
video_count = 0
for item in history:
if item["role"] != "user" or isinstance(item["content"], str):
continue
if isinstance(item["content"], list) and len(item["content"]) > 0:
file_path = item["content"][0]
if isinstance(file_path, str):
if file_path.endswith(".mp4"):
video_count += 1
elif re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE):
image_count += 1
return image_count, video_count
def validate_media_constraints(message: dict, history: list[dict]) -> bool:
"""
- ๋น„๋””์˜ค 1๊ฐœ ์ดˆ๊ณผ ๋ถˆ๊ฐ€
- ๋น„๋””์˜ค์™€ ์ด๋ฏธ์ง€ ํ˜ผํ•ฉ ๋ถˆ๊ฐ€
- ์ด๋ฏธ์ง€ ๊ฐœ์ˆ˜(MAX_NUM_IMAGES) ์ดˆ๊ณผ ๋ถˆ๊ฐ€
- <image> ํƒœ๊ทธ๊ฐ€ ์žˆ์œผ๋ฉด ํƒœ๊ทธ ์ˆ˜์™€ ์‹ค์ œ ์ด๋ฏธ์ง€ ์ˆ˜ ์ผ์น˜
- CSV, TXT, PDF ๋“ฑ์€ ์—ฌ๊ธฐ์„œ ์ œํ•œํ•˜์ง€ ์•Š์Œ
"""
media_files = []
for f in message["files"]:
if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE) or f.endswith(".mp4"):
media_files.append(f)
new_image_count, new_video_count = count_files_in_new_message(media_files)
history_image_count, history_video_count = count_files_in_history(history)
image_count = history_image_count + new_image_count
video_count = history_video_count + new_video_count
if video_count > 1:
gr.Warning("Only one video is supported.")
return False
if video_count == 1:
if image_count > 0:
gr.Warning("Mixing images and videos is not allowed.")
return False
if "<image>" in message["text"]:
gr.Warning("Using <image> tags with video files is not supported.")
return False
if video_count == 0 and image_count > MAX_NUM_IMAGES:
gr.Warning(f"You can upload up to {MAX_NUM_IMAGES} images.")
return False
if "<image>" in message["text"]:
image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
image_tag_count = message["text"].count("<image>")
if image_tag_count != len(image_files):
gr.Warning("The number of <image> tags in the text does not match the number of image files.")
return False
return True
##############################################################################
# ๋น„๋””์˜ค ์ฒ˜๋ฆฌ
##############################################################################
def downsample_video(video_path: str) -> list[tuple[Image.Image, float]]:
vidcap = cv2.VideoCapture(video_path)
fps = vidcap.get(cv2.CAP_PROP_FPS)
total_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
frame_interval = max(int(fps), int(total_frames / 10))
frames = []
for i in range(0, total_frames, frame_interval):
vidcap.set(cv2.CAP_PROP_POS_FRAMES, i)
success, image = vidcap.read()
if success:
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(image)
timestamp = round(i / fps, 2)
frames.append((pil_image, timestamp))
if len(frames) >= 5:
break
vidcap.release()
return frames
def process_video(video_path: str) -> list[dict]:
content = []
frames = downsample_video(video_path)
for frame in frames:
pil_image, timestamp = frame
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
pil_image.save(temp_file.name)
content.append({"type": "text", "text": f"Frame {timestamp}:"})
content.append({"type": "image", "url": temp_file.name})
logger.debug(f"{content=}")
return content
##############################################################################
# interleaved <image> ์ฒ˜๋ฆฌ
##############################################################################
def process_interleaved_images(message: dict) -> list[dict]:
parts = re.split(r"(<image>)", message["text"])
content = []
image_index = 0
image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
for part in parts:
if part == "<image>" and image_index < len(image_files):
content.append({"type": "image", "url": image_files[image_index]})
image_index += 1
elif part.strip():
content.append({"type": "text", "text": part.strip()})
else:
if isinstance(part, str) and part != "<image>":
content.append({"type": "text", "text": part})
return content
##############################################################################
# PDF + CSV + TXT + ์ด๋ฏธ์ง€/๋น„๋””์˜ค
##############################################################################
def is_image_file(file_path: str) -> bool:
return bool(re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE))
def is_video_file(file_path: str) -> bool:
return file_path.endswith(".mp4")
def is_document_file(file_path: str) -> bool:
return (
file_path.lower().endswith(".pdf")
or file_path.lower().endswith(".csv")
or file_path.lower().endswith(".txt")
)
def process_new_user_message(message: dict) -> list[dict]:
if not message["files"]:
return [{"type": "text", "text": message["text"]}]
video_files = [f for f in message["files"] if is_video_file(f)]
image_files = [f for f in message["files"] if is_image_file(f)]
csv_files = [f for f in message["files"] if f.lower().endswith(".csv")]
txt_files = [f for f in message["files"] if f.lower().endswith(".txt")]
pdf_files = [f for f in message["files"] if f.lower().endswith(".pdf")]
content_list = [{"type": "text", "text": message["text"]}]
for csv_path in csv_files:
csv_analysis = analyze_csv_file(csv_path)
content_list.append({"type": "text", "text": csv_analysis})
for txt_path in txt_files:
txt_analysis = analyze_txt_file(txt_path)
content_list.append({"type": "text", "text": txt_analysis})
for pdf_path in pdf_files:
pdf_markdown = pdf_to_markdown(pdf_path)
content_list.append({"type": "text", "text": pdf_markdown})
if video_files:
content_list += process_video(video_files[0])
return content_list
if "<image>" in message["text"] and image_files:
interleaved_content = process_interleaved_images({"text": message["text"], "files": image_files})
if content_list and content_list[0]["type"] == "text":
content_list = content_list[1:]
return interleaved_content + content_list
else:
for img_path in image_files:
content_list.append({"type": "image", "url": img_path})
return content_list
##############################################################################
# history -> LLM ๋ฉ”์‹œ์ง€ ๋ณ€ํ™˜
##############################################################################
def process_history(history: list[dict]) -> list[dict]:
messages = []
current_user_content: list[dict] = []
for item in history:
if item["role"] == "assistant":
if current_user_content:
messages.append({"role": "user", "content": current_user_content})
current_user_content = []
messages.append({"role": "assistant", "content": [{"type": "text", "text": item["content"]}]})
else:
content = item["content"]
if isinstance(content, str):
current_user_content.append({"type": "text", "text": content})
elif isinstance(content, list) and len(content) > 0:
file_path = content[0]
if is_image_file(file_path):
current_user_content.append({"type": "image", "url": file_path})
else:
current_user_content.append({"type": "text", "text": f"[File: {os.path.basename(file_path)}]"})
if current_user_content:
messages.append({"role": "user", "content": current_user_content})
return messages
##############################################################################
# ๋ฉ”์ธ ์ถ”๋ก  ํ•จ์ˆ˜ (web search ์ฒดํฌ ์‹œ ์ž๋™ ํ‚ค์›Œ๋“œ์ถ”์ถœ->๊ฒ€์ƒ‰->๊ฒฐ๊ณผ system msg ๋ฐ˜์˜)
##############################################################################
@spaces.GPU(duration=120)
def run(
message: dict,
history: list[dict],
system_prompt: str = "",
max_new_tokens: int = 512,
use_web_search: bool = False,
web_search_query: str = "",
) -> Iterator[str]:
if not validate_media_constraints(message, history):
yield ""
return
try:
# web_search๊ฐ€ True๋ฉด => message["text"]๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ‚ค์›Œ๋“œ ์ถ”์ถœํ•˜์—ฌ SERPHouse ํ˜ธ์ถœ
history_system_msg = None
if use_web_search:
user_text = message["text"]
# 1) ํ‚ค์›Œ๋“œ ์ถ”์ถœ
ws_query = extract_keywords(user_text, top_k=5)
logger.info(f"[Auto WebSearch Keyword] {ws_query!r}")
# 2) ์ƒ์œ„ 20๊ฐœ ๊ฒฐ๊ณผ (item ์ „์ฒด) ๊ฐ€์ ธ์˜ค๊ธฐ
ws_result = do_web_search(ws_query)
# 3) ์ด๋ฅผ system ๋ฉ”์‹œ์ง€๋กœ ์ถ”๊ฐ€
system_search_content = f"[Search top-20 Full Items Based on user prompt]\n{ws_result}\n"
if system_search_content.strip():
history_system_msg = {
"role": "system",
"content": [{"type": "text", "text": system_search_content}]
}
else:
history_system_msg = {
"role": "system",
"content": [{"type": "text", "text": "No web search results"}]
}
# ๊ธฐ์กด system prompt
messages = []
if system_prompt:
messages.append({"role": "system", "content": [{"type": "text", "text": system_prompt}]})
# web ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ system msg
if history_system_msg:
messages.append(history_system_msg)
# ์ด์ „ ๋Œ€ํ™”์ด๋ ฅ(assistant/user)
messages.extend(process_history(history))
# ์ƒˆ ์œ ์ € ๋ฉ”์‹œ์ง€ ๋ณ€ํ™˜
user_content = process_new_user_message(message)
for item in user_content:
if item["type"] == "text" and len(item["text"]) > MAX_CONTENT_CHARS:
item["text"] = item["text"][:MAX_CONTENT_CHARS] + "\n...(truncated)..."
messages.append({"role": "user", "content": user_content})
# LLM ์ž…๋ ฅ ์ƒ์„ฑ
inputs = processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors="pt",
).to(device=model.device, dtype=torch.bfloat16)
streamer = TextIteratorStreamer(processor, timeout=30.0, skip_prompt=True, skip_special_tokens=True)
gen_kwargs = dict(
inputs,
streamer=streamer,
max_new_tokens=max_new_tokens,
)
t = Thread(target=model.generate, kwargs=gen_kwargs)
t.start()
output = ""
for new_text in streamer:
output += new_text
yield output
except Exception as e:
logger.error(f"Error in run: {str(e)}")
yield f"์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
##############################################################################
# ์˜ˆ์‹œ๋“ค (ํ•œ๊ธ€ํ™”)
##############################################################################
examples = [
[
{
"text": "๋‘ PDF ํŒŒ์ผ ๋‚ด์šฉ์„ ๋น„๊ตํ•˜๋ผ.",
"files": ["assets/additional-examples/pdf.pdf"],
"files": [
"assets/additional-examples/before.pdf",
"assets/additional-examples/after.pdf",
],
}
],
[
{
"text": "CSV ํŒŒ์ผ ๋‚ด์šฉ์„ ์š”์•ฝ, ๋ถ„์„ํ•˜๋ผ",
"files": ["assets/additional-examples/sample-csv.csv"],
}
],
[
{
"text": "์ด ์˜์ƒ์˜ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•˜๋ผ",
"files": ["assets/additional-examples/tmp.mp4"],
}
],
[
{
"text": "ํ‘œ์ง€ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•˜๊ณ  ๊ธ€์ž๋ฅผ ์ฝ์–ด์ฃผ์„ธ์š”.",
"files": ["assets/additional-examples/maz.jpg"],
}
],
[
{
"text": "์ด๋ฏธ ์ด ์˜์–‘์ œ๋ฅผ <image> ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ์ด ์ œํ’ˆ <image>์„ ์ƒˆ๋กœ ์‚ฌ๋ ค ํ•ฉ๋‹ˆ๋‹ค. ํ•จ๊ป˜ ์„ญ์ทจํ•  ๋•Œ ์ฃผ์˜ํ•ด์•ผ ํ•  ์ ์ด ์žˆ์„๊นŒ์š”?",
"files": ["assets/additional-examples/pill1.png", "assets/additional-examples/pill2.png"],
}
],
[
{
"text": "์ด ์ ๋ถ„์„ ํ’€์–ด์ฃผ์„ธ์š”.",
"files": ["assets/additional-examples/4.png"],
}
],
[
{
"text": "์ด ํ‹ฐ์ผ“์€ ์–ธ์ œ ๋ฐœ๊ธ‰๋œ ๊ฒƒ์ด๊ณ , ๊ฐ€๊ฒฉ์€ ์–ผ๋งˆ์ธ๊ฐ€์š”?",
"files": ["assets/additional-examples/2.png"],
}
],
[
{
"text": "์ด๋ฏธ์ง€๋“ค์˜ ์ˆœ์„œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์งง์€ ์ด์•ผ๊ธฐ๋ฅผ ๋งŒ๋“ค์–ด ์ฃผ์„ธ์š”.",
"files": [
"assets/sample-images/09-1.png",
"assets/sample-images/09-2.png",
"assets/sample-images/09-3.png",
"assets/sample-images/09-4.png",
"assets/sample-images/09-5.png",
],
}
],
[
{
"text": "์ด๋ฏธ์ง€์˜ ์‹œ๊ฐ์  ์š”์†Œ์—์„œ ์˜๊ฐ์„ ๋ฐ›์•„ ์‹œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”.",
"files": ["assets/sample-images/06-1.png", "assets/sample-images/06-2.png"],
}
],
[
{
"text": "๋™์ผํ•œ ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„๋ฅผ ๊ทธ๋ฆฌ๋Š” matplotlib ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”.",
"files": ["assets/additional-examples/barchart.png"],
}
],
[
{
"text": "์ด ์„ธ๊ณ„์—์„œ ์‚ด๊ณ  ์žˆ์„ ์ƒ๋ฌผ๋“ค์„ ์ƒ์ƒํ•ด์„œ ๋ฌ˜์‚ฌํ•ด์ฃผ์„ธ์š”.",
"files": ["assets/sample-images/08.png"],
}
],
[
{
"text": "์ด๋ฏธ์ง€์— ์žˆ๋Š” ํ…์ŠคํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ์ฝ์–ด์„œ ๋งˆํฌ๋‹ค์šด ํ˜•ํƒœ๋กœ ์ ์–ด์ฃผ์„ธ์š”.",
"files": ["assets/additional-examples/3.png"],
}
],
[
{
"text": "์ด ํ‘œ์ง€ํŒ์—๋Š” ๋ฌด์Šจ ๋ฌธ๊ตฌ๊ฐ€ ์ ํ˜€ ์žˆ๋‚˜์š”?",
"files": ["assets/sample-images/02.png"],
}
],
[
{
"text": "๋‘ ์ด๋ฏธ์ง€๋ฅผ ๋น„๊ตํ•ด์„œ ๊ณตํ†ต์ ๊ณผ ์ฐจ์ด์ ์„ ๋งํ•ด์ฃผ์„ธ์š”.",
"files": ["assets/sample-images/03.png"],
}
],
]
##############################################################################
# Gradio UI (Blocks) ๊ตฌ์„ฑ
##############################################################################
css = """
body {
background: linear-gradient(135deg, #667eea, #764ba2);
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: #333;
margin: 0;
padding: 0;
}
.gradio-container {
background: rgba(255, 255, 255, 0.95);
border-radius: 15px;
padding: 30px 40px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
margin: 40px auto;
max-width: 1200px;
}
.gradio-container h1 {
color: #333;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
}
.fillable {
width: 95% !important;
max-width: unset !important;
}
#examples_container {
margin: auto;
width: 90%;
}
#examples_row {
justify-content: center;
}
.sidebar {
background: rgba(255, 255, 255, 0.98);
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
button, .btn {
background: linear-gradient(90deg, #ff8a00, #e52e71);
border: none;
color: #fff;
padding: 12px 24px;
text-transform: uppercase;
font-weight: bold;
letter-spacing: 1px;
border-radius: 5px;
cursor: pointer;
transition: transform 0.2s ease-in-out;
}
button:hover, .btn:hover {
transform: scale(1.05);
}
"""
title_html = """
<h1 align="center" style="margin-bottom: 0.2em;"> ๐Ÿค— Vidraft-Gemma-3-27B </h1>
<p align="center" style="font-size:1.1em; color:#555;">
Multimodal Chat Interface + Optional Web Search
</p>
"""
with gr.Blocks(css=css, title="Vidraft-Gemma-3-27B") as demo:
gr.Markdown(title_html)
with gr.Row():
# Left Sidebar
with gr.Column(scale=3, variant="panel"):
gr.Markdown("### Menu / Options")
with gr.Row():
web_search_checkbox = gr.Checkbox(
label="Web Search",
value=False,
info="Check to enable a SERPHouse web search (auto keywords) before the chat reply"
)
web_search_text = gr.Textbox(
lines=1,
label="(Unused) Web Search Query",
placeholder="No direct input needed"
)
gr.Markdown("---")
gr.Markdown("#### System Prompt")
system_prompt_box = gr.Textbox(
lines=3,
value=(
"You are a deeply thoughtful AI. Consider problems thoroughly and derive "
"correct solutions through systematic reasoning. Please answer in korean."
),
)
max_tokens_slider = gr.Slider(
label="Max New Tokens",
minimum=100,
maximum=8000,
step=50,
value=2000,
)
gr.Markdown("<br><br>") # spacing
# Main ChatInterface to the right
with gr.Column(scale=7):
chat = gr.ChatInterface(
fn=run,
type="messages",
chatbot=gr.Chatbot(type="messages", scale=1, allow_tags=["image"]),
textbox=gr.MultimodalTextbox(
file_types=[
".webp", ".png", ".jpg", ".jpeg", ".gif",
".mp4", ".csv", ".txt", ".pdf"
],
file_count="multiple",
autofocus=True
),
multimodal=True,
additional_inputs=[
system_prompt_box,
max_tokens_slider,
web_search_checkbox,
web_search_text, # ์‹ค์ œ๋กœ๋Š” auto search
],
stop_btn=False,
title="Vidraft-Gemma-3-27B",
examples=examples,
run_examples_on_click=False,
cache_examples=False,
css_paths=None,
delete_cache=(1800, 1800),
)
with gr.Row(elem_id="examples_row"):
with gr.Column(scale=12, elem_id="examples_container"):
gr.Markdown("### Example Inputs (click to load)")
gr.Examples(
examples=examples,
inputs=[], # ๋งํฌํ•  inputs๊ฐ€ ์—†์œผ๋ฏ€๋กœ ๋นˆ ๋ฆฌ์ŠคํŠธ
cache_examples=False
)
if __name__ == "__main__":
demo.launch()