llm_analysis / src /streamlit_app.py
jonghhhh's picture
Update src/streamlit_app.py
0cd8d7e verified
# app.py
import os, io, time, json, re, requests
from io import BytesIO # ํŒŒ์ผ์ด ์—†์–ด๋„, ๋ฉ”๋ชจ๋ฆฌ ์† ๋ฐ์ดํ„ฐ(๋ฐ”์ดํŠธ์—ด) ๋ฅผ ํŒŒ์ผ์ฒ˜๋Ÿผ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ. ์›น์—์„œ ๋ฐ›์€ ์ด๋ฏธ์ง€/ํŒŒ์ผ์„ ์ €์žฅํ•˜์ง€ ์•Š๊ณ  ๋ฐ”๋กœ ์‚ฌ์šฉ
from PIL import Image
import streamlit as st
from google import genai
st.set_page_config(page_title="๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ LLM ๋ฐ๋ชจ (Gemma 3 27B)", page_icon="๐Ÿค–", layout="wide")
# ===== Sidebar: ์„ค์ • =====
st.sidebar.title("๐Ÿ”ง ์„ค์ •")
api_key = st.sidebar.text_input("GOOGLE_API_KEY", value=os.getenv("GOOGLE_API_KEY", ""), type="password")
model_name = st.sidebar.selectbox("๋ชจ๋ธ", ["gemma-3-27b-it"], index=0)
rate_delay = st.sidebar.number_input("ํ˜ธ์ถœ๊ฐ„ ๋Œ€๊ธฐ(์ดˆ)", value=1.0, step=0.5, min_value=0.0)
if not api_key:
st.warning("์‚ฌ์ด๋“œ๋ฐ”์— GOOGLE_API_KEY๋ฅผ ์ž…๋ ฅํ•˜๊ฑฐ๋‚˜ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์„ค์ •ํ•ด ์ฃผ์„ธ์š”.")
st.stop()
client = genai.Client(api_key=api_key)
# ===== ์œ ํ‹ธ =====
RETRY_LIMIT = 3
def load_image(source: str) -> Image.Image:
"""URL/๋กœ์ปฌ ๋ชจ๋‘ ์ง€์› + ํˆฌ๋ช… ๋ฐฐ๊ฒฝ ๋ณด์ •."""
session = requests.Session()
session.headers.update({"User-Agent": "Mozilla/5.0"})
last_err = None
for _ in range(RETRY_LIMIT):
try:
if source.startswith(("http://", "https://")):
resp = session.get(source, timeout=10)
resp.raise_for_status()
img = Image.open(BytesIO(resp.content))
else:
img = Image.open(source)
if img.mode in ("RGBA", "LA", "P"):
bg = Image.new("RGB", img.size, (255, 255, 255))
img = img.convert("RGBA")
bg.paste(img, mask=img.split()[-1] if len(img.split()) > 3 else None)
img = bg
else:
img = img.convert("RGB")
return img
except Exception as e:
last_err = e
time.sleep(0.5)
raise RuntimeError(f"์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ: {last_err}")
RESET_PROMPT = "์ด์ „ ๋Œ€ํ™”๋Š” ๋ฌด์‹œํ•˜๊ณ , ์•„๋ž˜ ์ง€์‹œ์—๋งŒ ์‘๋‹ตํ•˜์„ธ์š”.\n\n"
def try_parse_json(raw_text: str):
"""์‘๋‹ต์—์„œ JSON ์ถ”์ถœ ์‹œ๋„."""
pattern = r'```(?:json)?\s*(\{[\s\S]*?\})\s*```|(\{[\s\S]*?\})'
for g1, g2 in re.findall(pattern, raw_text):
cand = (g1 or g2).strip()
try:
return json.loads(cand)
except json.JSONDecodeError:
pass
cleaned = re.sub(r'```json|```', '', raw_text).strip()
try:
return json.loads(cleaned)
except json.JSONDecodeError:
return None
def infer_text(article: str, prompt: str):
contents = [RESET_PROMPT + prompt.strip() + "\n\n" + article.strip()]
resp = client.models.generate_content(model=model_name, contents=contents)
time.sleep(rate_delay)
text = (resp.text or "").strip()
parsed = try_parse_json(text)
return parsed if parsed is not None else {"text": text}
def infer_image(image: Image.Image, prompt: str):
contents = [RESET_PROMPT + prompt.strip(), image]
resp = client.models.generate_content(model=model_name, contents=contents)
time.sleep(rate_delay)
text = (resp.text or "").strip()
parsed = try_parse_json(text)
return parsed if parsed is not None else {"text": text}
# ===== ๊ธฐ๋ณธ ํ”„๋กฌํ”„ํŠธ(๊ฐ„๊ฒฐ ๋ฒ„์ „) =====
TEXT_PROMPT = (
"๋‹น์‹ ์€ ๊ธฐ์‚ฌ ์ •๋ณด์› ๋ถ„์„ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ๋‹ค์Œ ๊ธฐ์‚ฌ์—์„œ ์ •๋ณด์›์„ ์ถ”์ถœํ•˜๊ณ , "
"์ •๋ณด์›๋ณ„ ๋ฌ˜์‚ฌ ํ”„๋ ˆ์ž„์„ ๊ธ์ •/์ค‘๋ฆฝ/๋ถ€์ •์œผ๋กœ ํŒ์ •ํ•ด JSON์œผ๋กœ๋งŒ ์ถœ๋ ฅํ•˜์„ธ์š”.\n"
'์˜ˆ์‹œ: {"sources": ["์ •๋ณด์›A","์ •๋ณด์›B"], "frames": ["์ค‘๋ฆฝ","๋ถ€์ •"]}'
)
IMAGE_PROMPT = (
"๋‹น์‹ ์€ ๋ณด๋„์‚ฌ์ง„ ๋ถ„์„ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ์ œ๊ณต๋œ ์‚ฌ์ง„์—์„œ Donald Trump ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•˜๊ณ  "
"๊ฐ์ •(emotion: positive/negative/neutral)๊ณผ ์—ญ๋™์„ฑ(dynamism: high/medium/low)์„ ํ‰๊ฐ€ํ•ด "
'JSON์œผ๋กœ๋งŒ ์ถœ๋ ฅํ•˜์„ธ์š”. Trump๊ฐ€ ์—†์œผ๋ฉด {"trump_present": false}๋งŒ ๋ฐ˜ํ™˜ํ•˜์„ธ์š”.'
)
# ===== UI =====
st.title("๐Ÿค– ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ LLM ๋ฐ๋ชจ (Gemma 3 27B)")
tab_text, tab_img = st.tabs(["๐Ÿ“ ํ…์ŠคํŠธ ๋ถ„์„", "๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€ ๋ถ„์„"])
with tab_text:
st.subheader("๊ธฐ์‚ฌ ํ…์ŠคํŠธ โ†’ ์ •๋ณด์› & ํ”„๋ ˆ์ž„ ํŒ์ •")
article = st.text_area(
"๊ธฐ์‚ฌ ๋ณธ๋ฌธ", height=180,
value="Donald Trump์™€ Nancy Pelosi๊ฐ€ ํšŒ์˜์žฅ์—์„œ ๊ฒฉ๋ ฌํžˆ ๋…ผ์Ÿ์„ ๋ฒŒ์˜€๋‹ค..."
)
user_prompt = st.text_area("ํ”„๋กฌํ”„ํŠธ(์˜ต์…˜)", value=TEXT_PROMPT, height=120)
if st.button("ํ…์ŠคํŠธ ๋ถ„์„ ์‹คํ–‰", type="primary", use_container_width=True):
if not article.strip():
st.error("๊ธฐ์‚ฌ ๋ณธ๋ฌธ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.")
else:
with st.spinner("๋ถ„์„ ์ค‘..."):
try:
result = infer_text(article, user_prompt or TEXT_PROMPT)
st.success("์™„๋ฃŒ")
st.json(result)
st.download_button("๊ฒฐ๊ณผ JSON ๋‹ค์šด๋กœ๋“œ", data=json.dumps(result, ensure_ascii=False, indent=2),
file_name="text_result.json", mime="application/json")
except Exception as e:
st.error(f"์˜ค๋ฅ˜: {e}")
with tab_img:
st.subheader("๋ณด๋„์‚ฌ์ง„ โ†’ ์ธ๋ฌผ ์กด์žฌยท๊ฐ์ •ยท์—ญ๋™์„ฑ ๋ถ„์„")
col1, col2 = st.columns(2)
with col1:
img_url = st.text_input("์ด๋ฏธ์ง€ URL")
with col2:
file = st.file_uploader("์ด๋ฏธ์ง€ ์—…๋กœ๋“œ", type=["jpg","jpeg","png","webp","gif","bmp"])
img_prompt = st.text_area("ํ”„๋กฌํ”„ํŠธ(์˜ต์…˜)", value=IMAGE_PROMPT, height=120)
if st.button("์ด๋ฏธ์ง€ ๋ถ„์„ ์‹คํ–‰", type="primary", use_container_width=True):
try:
if file is not None:
image = Image.open(io.BytesIO(file.read())).convert("RGB")
elif img_url.strip():
image = load_image(img_url.strip())
else:
st.error("์ด๋ฏธ์ง€ URL์„ ์ž…๋ ฅํ•˜๊ฑฐ๋‚˜ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•ด ์ฃผ์„ธ์š”.")
st.stop()
st.image(image, caption="์ž…๋ ฅ ์ด๋ฏธ์ง€", use_container_width=True)
with st.spinner("๋ถ„์„ ์ค‘..."):
result = infer_image(image, img_prompt or IMAGE_PROMPT)
st.success("์™„๋ฃŒ")
st.json(result)
st.download_button("๊ฒฐ๊ณผ JSON ๋‹ค์šด๋กœ๋“œ", data=json.dumps(result, ensure_ascii=False, indent=2),
file_name="image_result.json", mime="application/json")
except Exception as e:
st.error(f"์˜ค๋ฅ˜: {e}")