Spaces:
Running
on
Zero
Running
on
Zero
#!/usr/bin/env python | |
import os | |
import re | |
import tempfile | |
from collections.abc import Iterator | |
from threading import Thread | |
import json | |
import requests | |
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) ํ๊ธ(๊ฐ-ํฃ), ์์ด(a-zA-Z), ์ซ์(0-9), ๊ณต๋ฐฑ๋ง ๋จ๊น | |
2) ๊ณต๋ฐฑ ๊ธฐ์ค ํ ํฐ ๋ถ๋ฆฌ | |
3) ์ต๋ top_k๊ฐ๋ง | |
""" | |
text = re.sub(r"[^a-zA-Z0-9๊ฐ-ํฃ\s]", "", text) | |
tokens = text.split() | |
key_tokens = tokens[:top_k] | |
return " ".join(key_tokens) | |
############################################################################## | |
# SerpHouse Live endpoint ํธ์ถ | |
# - ์์ 20๊ฐ ๊ฒฐ๊ณผ JSON์ LLM์ ๋๊ธธ ๋ link, snippet ๋ฑ ๋ชจ๋ ํฌํจ | |
############################################################################## | |
def do_web_search(query: str) -> str: | |
""" | |
์์ 20๊ฐ 'organic' ๊ฒฐ๊ณผ item ์ ์ฒด(์ ๋ชฉ, link, snippet ๋ฑ)๋ฅผ | |
JSON ๋ฌธ์์ด ํํ๋ก ๋ฐํ | |
""" | |
try: | |
url = "https://api.serphouse.com/serp/live" | |
# ๊ธฐ๋ณธ GET ๋ฐฉ์์ผ๋ก ํ๋ผ๋ฏธํฐ ๊ฐ์ํํ๊ณ ๊ฒฐ๊ณผ ์๋ฅผ 20๊ฐ๋ก ์ ํ | |
params = { | |
"q": query, | |
"domain": "google.com", | |
"serp_type": "web", # ๊ธฐ๋ณธ ์น ๊ฒ์ | |
"device": "desktop", | |
"lang": "en", | |
"num": "20" # ์ต๋ 20๊ฐ ๊ฒฐ๊ณผ๋ง ์์ฒญ | |
} | |
headers = { | |
"Authorization": f"Bearer {SERPHOUSE_API_KEY}" | |
} | |
logger.info(f"SerpHouse API ํธ์ถ ์ค... ๊ฒ์์ด: {query}") | |
logger.info(f"์์ฒญ URL: {url} - ํ๋ผ๋ฏธํฐ: {params}") | |
# GET ์์ฒญ ์ํ | |
response = requests.get(url, headers=headers, params=params, timeout=30) | |
response.raise_for_status() | |
logger.info(f"SerpHouse API ์๋ต ์ํ ์ฝ๋: {response.status_code}") | |
data = response.json() | |
# ๋ค์ํ ์๋ต ๊ตฌ์กฐ ์ฒ๋ฆฌ | |
results = data.get("results", {}) | |
organic = None | |
# ๊ฐ๋ฅํ ์๋ต ๊ตฌ์กฐ 1 | |
if isinstance(results, dict) and "organic" in results: | |
organic = results["organic"] | |
# ๊ฐ๋ฅํ ์๋ต ๊ตฌ์กฐ 2 (์ค์ฒฉ๋ results) | |
elif isinstance(results, dict) and "results" in results: | |
if isinstance(results["results"], dict) and "organic" in results["results"]: | |
organic = results["results"]["organic"] | |
# ๊ฐ๋ฅํ ์๋ต ๊ตฌ์กฐ 3 (์ต์์ organic) | |
elif "organic" in data: | |
organic = data["organic"] | |
if not organic: | |
logger.warning("์๋ต์์ organic ๊ฒฐ๊ณผ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.") | |
logger.debug(f"์๋ต ๊ตฌ์กฐ: {list(data.keys())}") | |
if isinstance(results, dict): | |
logger.debug(f"results ๊ตฌ์กฐ: {list(results.keys())}") | |
return "No web search results found or unexpected API response structure." | |
# ๊ฒฐ๊ณผ ์ ์ ํ ๋ฐ ์ปจํ ์คํธ ๊ธธ์ด ์ต์ ํ | |
max_results = min(20, len(organic)) | |
limited_organic = organic[:max_results] | |
# ๊ฒฐ๊ณผ ํ์ ๊ฐ์ํ - ์ ์ฒด JSON ๋์ ์ค์ ํ๋๋ง ํฌํจ | |
summary_lines = [] | |
for idx, item in enumerate(limited_organic, start=1): | |
title = item.get("title", "No title") | |
link = item.get("link", "#") | |
snippet = item.get("snippet", "No description") | |
# ๊ฐ์ํ๋ ํ์ | |
summary_lines.append( | |
f"Result {idx}:\n" | |
f"- Title: {title}\n" | |
f"- Link: {link}\n" | |
f"- Snippet: {snippet}\n" | |
) | |
logger.info(f"๊ฒ์ ๊ฒฐ๊ณผ {len(limited_organic)}๊ฐ ์ฒ๋ฆฌ ์๋ฃ") | |
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 | |
model_id = os.getenv("MODEL_ID", "VIDraft/Gemma3-R1945-27B") | |
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) | |
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: | |
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) | |
############################################################################## | |
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: | |
combined_system_msg = "" | |
# ๋ด๋ถ์ ์ผ๋ก๋ง ์ฌ์ฉ (UI์์๋ ๋ณด์ด์ง ์์) | |
if system_prompt.strip(): | |
combined_system_msg += f"[System Prompt]\n{system_prompt.strip()}\n\n" | |
if use_web_search: | |
user_text = message["text"] | |
ws_query = extract_keywords(user_text, top_k=5) | |
if ws_query.strip(): | |
logger.info(f"[Auto WebSearch Keyword] {ws_query!r}") | |
ws_result = do_web_search(ws_query) | |
combined_system_msg += f"[Search top-20 Full Items Based on user prompt]\n{ws_result}\n\n" | |
# >>> ์ถ๊ฐ๋ ์๋ด ๋ฌธ๊ตฌ (๊ฒ์ ๊ฒฐ๊ณผ์ link ๋ฑ ์ถ์ฒ๋ฅผ ํ์ฉ) | |
combined_system_msg += "[์ฐธ๊ณ : ์ ๊ฒ์๊ฒฐ๊ณผ ๋ด์ฉ๊ณผ link๋ฅผ ์ถ์ฒ๋ก ์ธ์ฉํ์ฌ ๋ต๋ณํด ์ฃผ์ธ์.]\n\n" | |
else: | |
combined_system_msg += "[No valid keywords found, skipping WebSearch]\n\n" | |
messages = [] | |
if combined_system_msg.strip(): | |
messages.append({ | |
"role": "system", | |
"content": [{"type": "text", "text": combined_system_msg.strip()}], | |
}) | |
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}) | |
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_gen_with_oom_catch, 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)}" | |
############################################################################## | |
# [์ถ๊ฐ] ๋ณ๋ ํจ์์์ model.generate(...)๋ฅผ ํธ์ถ, OOM ์บ์น | |
############################################################################## | |
def _model_gen_with_oom_catch(**kwargs): | |
""" | |
๋ณ๋ ์ค๋ ๋์์ OutOfMemoryError๋ฅผ ์ก์์ฃผ๊ธฐ ์ํด | |
""" | |
try: | |
model.generate(**kwargs) | |
except torch.cuda.OutOfMemoryError: | |
raise RuntimeError( | |
"[OutOfMemoryError] GPU ๋ฉ๋ชจ๋ฆฌ๊ฐ ๋ถ์กฑํฉ๋๋ค. " | |
"Max New Tokens์ ์ค์ด๊ฑฐ๋, ํ๋กฌํํธ ๊ธธ์ด๋ฅผ ์ค์ฌ์ฃผ์ธ์." | |
) | |
############################################################################## | |
# ์์๋ค (ํ๊ธํ) | |
############################################################################## | |
examples = [ | |
[ | |
{ | |
"text": "๋ 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": "๋์ผํ ๋ง๋ ๊ทธ๋ํ๋ฅผ ๊ทธ๋ฆฌ๋ matplotlib ์ฝ๋๋ฅผ ์์ฑํด์ฃผ์ธ์.", | |
"files": ["assets/additional-examples/barchart.png"], | |
} | |
], | |
[ | |
{ | |
"text": "์ด๋ฏธ์ง์ ์๋ ํ ์คํธ๋ฅผ ๊ทธ๋๋ก ์ฝ์ด์ ๋งํฌ๋ค์ด ํํ๋ก ์ ์ด์ฃผ์ธ์.", | |
"files": ["assets/additional-examples/3.png"], | |
} | |
], | |
[ | |
{ | |
"text": "์ด ํ์งํ์๋ ๋ฌด์จ ๋ฌธ๊ตฌ๊ฐ ์ ํ ์๋์?", | |
"files": ["assets/sample-images/02.png"], | |
} | |
], | |
[ | |
{ | |
"text": "๋ ์ด๋ฏธ์ง๋ฅผ ๋น๊ตํด์ ๊ณตํต์ ๊ณผ ์ฐจ์ด์ ์ ๋งํด์ฃผ์ธ์.", | |
"files": ["assets/sample-images/03.png"], | |
} | |
], | |
[ | |
{ | |
"text": "๋๋ ์น๊ทผํ๊ณ ๋ค์ ํ ์ดํด์ฌ ๋ง์ ์ฌ์์น๊ตฌ ์ญํ ์ด๋ค.", | |
} | |
], | |
[ | |
{ | |
"text": """์ธ๋ฅ์ ๋ง์ง๋ง ์ํ(Humanity's Last Exam) ๋ฌธ์ ๋ฅผ ํ์ดํ๋ผ('Deep Research' ๋ฒํผ ํด๋ฆญํ ๊ฒ) Which was the first statute in the modern State of Israel to explicitly introduce the concept of "good faith"? (Do not append "the" or the statute's year to the answer.)""", | |
} | |
], | |
[ | |
{ | |
"text": """์ธ๋ฅ์ ๋ง์ง๋ง ์ํ(Humanity's Last Exam) ๋ฌธ์ ๋ฅผ ํ์ดํ๋ผ. How does Guarani's nominal tense/aspect system interact with effected objects in sentences? | |
Answer Choices: | |
A. Effected objects cannot take nominal tense/aspect markers | |
B. Effected objects require the post-stative -kue | |
C. Effected objects must be marked with the destinative -rรฃ | |
D. Nominal tense/aspect is optional for effected objects | |
E. Effected objects use a special set of tense/aspect markers""", | |
} | |
], | |
] | |
############################################################################## | |
# Gradio UI (Blocks) ๊ตฌ์ฑ (์ข์ธก ์ฌ์ด๋ ๋ฉ๋ด ์์ด ์ ์ฒดํ๋ฉด ์ฑํ ) | |
############################################################################## | |
css = """ | |
/* 1) UI๋ฅผ ์ฒ์๋ถํฐ ๊ฐ์ฅ ๋๊ฒ (width 100%) ๊ณ ์ ํ์ฌ ํ์ */ | |
.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: 20px auto; /* ์์๋ ์ฌ๋ฐฑ๋ง ์ ์ง */ | |
width: 100% !important; | |
max-width: none !important; /* 1200px ์ ํ ์ ๊ฑฐ */ | |
} | |
.fillable { | |
width: 100% !important; | |
max-width: 100% !important; | |
} | |
/* 2) ๋ฐฐ๊ฒฝ์ ์ฐํ๊ณ ํฌ๋ช ํ ํ์คํ ํค ๊ทธ๋ผ๋์ธํธ๋ก ๋ณ๊ฒฝ */ | |
body { | |
background: linear-gradient( | |
135deg, | |
rgba(255, 229, 210, 0.6), | |
rgba(255, 240, 245, 0.6) | |
); | |
margin: 0; | |
padding: 0; | |
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; | |
color: #333; | |
} | |
/* ๋ฒํผ ์์๋ ๊ธฐ์กด์ ์ง์ ๋ถ์-์ฃผํฉ โ ํ์คํ ๊ณ์ด๋ก ์ฐํ๊ฒ */ | |
button, .btn { | |
background: linear-gradient( | |
90deg, | |
rgba(255, 210, 220, 0.7), | |
rgba(255, 190, 200, 0.7) | |
) !important; | |
border: none; | |
color: #333; /* ๊ธ์ ์ ๋ณด์ด๋๋ก ์ฝ๊ฐ ์งํ ๊ธ์จ */ | |
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.03); | |
} | |
#examples_container { | |
margin: auto; | |
width: 90%; | |
} | |
#examples_row { | |
justify-content: center; | |
} | |
""" | |
title_html = """ | |
<h1 align="center" style="margin-bottom: 0.2em; font-size: 1.6em;"> ๐ค Gemma3-uncensored-R27B </h1> | |
<p align="center" style="font-size:1.1em; color:#555;"> | |
โ Agentic AI Platform โ Reasoning & Uncensored โ Multimodal & VLM โ Deep-Research & RAG <br> | |
Operates on an โ 'NVIDIA A100 GPU' as an independent local server, enhancing security and preventing information leakage.<br> | |
@Based by 'MS Gemma-3-27b' / @Powered by 'MOUSE-II'(VIDRAFT) | |
</p> | |
""" | |
with gr.Blocks(css=css, title="Gemma3-uncensored-R27B") as demo: | |
gr.Markdown(title_html) | |
# ์น์์น ์ต์ ์ ํ๋ฉด์ ํ์ (ํ์ง๋ง ์์คํ ํ๋กฌํํธ, ํ ํฐ ์ฌ๋ผ์ด๋ ๋ฑ์ ๊ฐ์ถค) | |
web_search_checkbox = gr.Checkbox( | |
label="Deep Research", | |
value=False | |
) | |
# ๋ด๋ถ์ ์ผ๋ก ์ฐ์ด์ง๋ง ํ๋ฉด์๋ ๋ ธ์ถ๋์ง ์๋๋ก ์ค์ | |
system_prompt_box = gr.Textbox( | |
lines=3, | |
value="๋ฐ๋์ ํ๊ธ๋ก ๋ต๋ณํ๋ผ. You are a deep thinking AI, you may use extremely long chains of thought to deeply consider the problem and deliberate with yourself via systematic reasoning processes to help come to a correct solution prior to answering. Please answer in Korean.You have the ability to read English sources, but you **must always speak in Korean**.Even if the search results are in English, answer in Korean.", | |
visible=False # ํ๋ฉด์์ ๊ฐ์ถค | |
) | |
max_tokens_slider = gr.Slider( | |
label="Max New Tokens", | |
minimum=100, | |
maximum=8000, | |
step=50, | |
value=1000, | |
visible=False # ํ๋ฉด์์ ๊ฐ์ถค | |
) | |
web_search_text = gr.Textbox( | |
lines=1, | |
label="(Unused) Web Search Query", | |
placeholder="No direct input needed", | |
visible=False # ํ๋ฉด์์ ๊ฐ์ถค | |
) | |
# ์ฑํ ์ธํฐํ์ด์ค ๊ตฌ์ฑ | |
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, | |
], | |
stop_btn=False, | |
title='<a href="https://discord.gg/openfreeai" target="_blank">https://discord.gg/openfreeai</a>', | |
examples=examples, | |
run_examples_on_click=False, | |
cache_examples=False, | |
css_paths=None, | |
delete_cache=(1800, 1800), | |
) | |
# ์์ ์น์ - ์ด๋ฏธ ChatInterface์ examples๊ฐ ์ค์ ๋์ด ์์ผ๋ฏ๋ก ์ฌ๊ธฐ์๋ ์ค๋ช ๋ง ํ์ | |
with gr.Row(elem_id="examples_row"): | |
with gr.Column(scale=12, elem_id="examples_container"): | |
gr.Markdown("### Example Inputs (click to load)") | |
if __name__ == "__main__": | |
# ๋ก์ปฌ์์๋ง ์คํ ์ | |
demo.launch() | |