aicopy / streamlit_app.py
parkkyujin's picture
Rename app.py to streamlit_app.py
18de67a verified
raw
history blame
18.1 kB
# ์•ˆ์ •์ ์ธ AI ์นดํ”ผ๋ผ์ดํ„ฐ - ์ž„๋ฒ ๋”ฉ ๊ธฐ๋ฐ˜ RAG ์‹œ์Šคํ…œ
# Hugging Face Spaces ํ™˜๊ฒฝ ์ตœ์ ํ™” ๋ฒ„์ „
import streamlit as st
import pandas as pd
import numpy as np
import pickle
import google.generativeai as genai
import time
import json
import os
from datetime import datetime
# ํ™˜๊ฒฝ ์„ค์ • (๊ถŒํ•œ ๋ฌธ์ œ ํ•ด๊ฒฐ)
os.environ['STREAMLIT_BROWSER_GATHER_USAGE_STATS'] = 'false'
os.environ['TRANSFORMERS_CACHE'] = '/tmp/transformers'
os.environ['SENTENCE_TRANSFORMERS_HOME'] = '/tmp/sentence_transformers'
# ํŽ˜์ด์ง€ ์„ค์ •
st.set_page_config(
page_title="AI ์นดํ”ผ๋ผ์ดํ„ฐ | RAG ๊ธฐ๋ฐ˜ ๊ด‘๊ณ  ์นดํ”ผ ์ƒ์„ฑ",
page_icon="โœจ",
layout="wide",
initial_sidebar_state="expanded"
)
# ์ œ๋ชฉ ๋ฐ ์„ค๋ช…
st.title("โœจ AI ์นดํ”ผ๋ผ์ดํ„ฐ")
st.markdown("### ๐ŸŽฏ 37,671๊ฐœ ์‹ค์ œ ๊ด‘๊ณ  ์นดํ”ผ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ RAG ์‹œ์Šคํ…œ")
st.markdown("---")
# ์‚ฌ์ด๋“œ๋ฐ” ์„ค์ •
st.sidebar.header("๐ŸŽ›๏ธ ์นดํ”ผ ์ƒ์„ฑ ์„ค์ •")
# API ํ‚ค ์ž…๋ ฅ (ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์šฐ์„  ์‚ฌ์šฉ)
default_api_key = os.getenv("GEMINI_API_KEY", "")
api_key = st.sidebar.text_input(
"๐Ÿ”‘ Gemini API ํ‚ค",
value=default_api_key,
type="password",
help="ํ™˜๊ฒฝ๋ณ€์ˆ˜์— GEMINI_API_KEY๋กœ ์„ค์ •ํ•˜๋ฉด ์ž๋™ ์ž…๋ ฅ๋ฉ๋‹ˆ๋‹ค"
)
if not api_key:
st.warning("โš ๏ธ Gemini API ํ‚ค๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”")
st.info("๐Ÿ’ก Settings โ†’ Repository secrets์—์„œ GEMINI_API_KEY๋ฅผ ์„ค์ •ํ•˜์„ธ์š”")
st.stop()
# ์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™” (์บ์‹ฑ) - ์ž„๋ฒ ๋”ฉ ํ•„์ˆ˜!
@st.cache_resource(show_spinner=False)
def load_system():
"""์‹œ์Šคํ…œ ์ปดํฌ๋„ŒํŠธ ๋กœ๋”ฉ - ์ž„๋ฒ ๋”ฉ ๊ธฐ๋ฐ˜ RAG ์‹œ์Šคํ…œ"""
progress_container = st.container()
with progress_container:
# ์ „์ฒด ์ง„ํ–‰๋ฅ 
total_progress = st.progress(0)
status_text = st.empty()
# 1๋‹จ๊ณ„: API ์„ค์ • (10%)
status_text.text("๐Ÿ”‘ Gemini API ์ดˆ๊ธฐํ™” ์ค‘...")
try:
genai.configure(api_key=api_key)
model = genai.GenerativeModel('gemini-2.0-flash')
total_progress.progress(10)
st.success("โœ… Gemini API ์„ค์ • ์™„๋ฃŒ")
except Exception as e:
st.error(f"โŒ Gemini API ์„ค์ • ์‹คํŒจ: {e}")
return None, None, None, None
# 2๋‹จ๊ณ„: ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ ๋กœ๋“œ (40%)
status_text.text("๐Ÿค– ํ•œ๊ตญ์–ด ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ ๋กœ๋”ฉ ์ค‘... (1-2๋ถ„ ์†Œ์š”)")
embedding_model = None
# ์•ˆ์ •์ ์ธ ๋ชจ๋ธ ๋กœ๋”ฉ ์ „๋žต
try:
# ๋จผ์ € ์บ์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
os.makedirs('/tmp/sentence_transformers', exist_ok=True)
os.makedirs('/tmp/transformers', exist_ok=True)
# sentence-transformers ์ž„ํฌํŠธ๋ฅผ ํ•จ์ˆ˜ ๋‚ด์—์„œ
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
# ํ•œ๊ตญ์–ด ๋ชจ๋ธ ๋กœ๋”ฉ ์‹œ๋„
embedding_model = SentenceTransformer('jhgan/ko-sbert-nli',
cache_folder='/tmp/sentence_transformers')
total_progress.progress(40)
st.success("โœ… ํ•œ๊ตญ์–ด ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ ๋กœ๋”ฉ ์™„๋ฃŒ")
except Exception as e:
st.error(f"โŒ ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ ๋กœ๋”ฉ ์‹คํŒจ: {e}")
st.error("๐Ÿšจ ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ ์—†์ด๋Š” RAG ์‹œ์Šคํ…œ์ด ์ž‘๋™ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค!")
return None, None, None, None
# 3๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ ๋กœ๋“œ (60%)
status_text.text("๐Ÿ“Š ์นดํ”ผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋กœ๋”ฉ ์ค‘...")
try:
df = pd.read_excel('๊ด‘๊ณ ์นดํ”ผ๋ฐ์ดํ„ฐ_๋ธŒ๋žœ๋“œ์ถ”์ถœ์™„๋ฃŒ.xlsx')
total_progress.progress(60)
st.success(f"โœ… ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์™„๋ฃŒ: {len(df):,}๊ฐœ ์นดํ”ผ")
except Exception as e:
st.error(f"โŒ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹คํŒจ: {e}")
return None, None, None, None
# 4๋‹จ๊ณ„: ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ ๋กœ๋“œ (90%) - ์ด๊ฒŒ ํ•ต์‹ฌ!
status_text.text("๐Ÿ” ๋ฒกํ„ฐ ์ž„๋ฒ ๋”ฉ ๋กœ๋”ฉ ์ค‘... (RAG ์‹œ์Šคํ…œ ํ•ต์‹ฌ)")
try:
with open('copy_embeddings.pkl', 'rb') as f:
embeddings_data = pickle.load(f)
embeddings = embeddings_data['embeddings']
total_progress.progress(90)
st.success(f"โœ… ์ž„๋ฒ ๋”ฉ ๋กœ๋”ฉ ์™„๋ฃŒ: {embeddings.shape[0]:,}๊ฐœ ร— {embeddings.shape[1]}์ฐจ์›")
except Exception as e:
st.error(f"โŒ ์ž„๋ฒ ๋”ฉ ๋กœ๋”ฉ ์‹คํŒจ: {e}")
st.error("๐Ÿšจ ์ž„๋ฒ ๋”ฉ ์—†์ด๋Š” ์˜๋ฏธ์  ๊ฒ€์ƒ‰์ด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค!")
return None, None, None, None
# 5๋‹จ๊ณ„: ์ตœ์ข… ๊ฒ€์ฆ (100%)
status_text.text("โœจ ์‹œ์Šคํ…œ ๊ฒ€์ฆ ์ค‘...")
if model and embedding_model and df is not None and embeddings is not None:
total_progress.progress(100)
status_text.text("๐ŸŽ‰ RAG ์‹œ์Šคํ…œ ๋กœ๋”ฉ ์™„๋ฃŒ!")
# ์„ฑ๊ณต ๋ฉ”์‹œ์ง€
success_col1, success_col2, success_col3 = st.columns(3)
with success_col1:
st.metric("์นดํ”ผ ๋ฐ์ดํ„ฐ", f"{len(df):,}๊ฐœ")
with success_col2:
st.metric("์ž„๋ฒ ๋”ฉ ์ฐจ์›", f"{embeddings.shape[1]}D")
with success_col3:
st.metric("๊ฒ€์ƒ‰ ์—”์ง„", "Korean SBERT")
# ์ง„ํ–‰๋ฅ  ๋ฐ” ์ œ๊ฑฐ
time.sleep(1)
total_progress.empty()
status_text.empty()
return model, embedding_model, df, embeddings
else:
st.error("โŒ ์‹œ์Šคํ…œ ๋กœ๋”ฉ ์‹คํŒจ: ํ•„์ˆ˜ ๊ตฌ์„ฑ์š”์†Œ ๋ˆ„๋ฝ")
return None, None, None, None
# ์‹œ์Šคํ…œ ๋กœ๋”ฉ
with st.spinner("๐Ÿš€ AI ์นดํ”ผ๋ผ์ดํ„ฐ ์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™” ์ค‘..."):
model, embedding_model, df, embeddings = load_system()
if model is None or embedding_model is None or df is None or embeddings is None:
st.error("โŒ ์‹œ์Šคํ…œ์„ ๋กœ๋”ฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๊ฑฐ๋‚˜ ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.")
st.stop()
# ์‚ฌ์ด๋“œ๋ฐ” ์„ค์ • (์‹œ์Šคํ…œ ๋กœ๋”ฉ ์„ฑ๊ณต ํ›„)
st.sidebar.success("๐ŸŽ‰ RAG ์‹œ์Šคํ…œ ์ค€๋น„ ์™„๋ฃŒ!")
# ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ
categories = ['์ „์ฒด'] + sorted(df['์นดํ…Œ๊ณ ๋ฆฌ'].unique().tolist())
selected_category = st.sidebar.selectbox(
"๐Ÿ“‚ ์นดํ…Œ๊ณ ๋ฆฌ",
categories,
help="ํŠน์ • ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ๊ฒ€์ƒ‰์„ ์ œํ•œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค"
)
# ํƒ€๊ฒŸ ๊ณ ๊ฐ ์„ค์ •
target_audience = st.sidebar.selectbox(
"๐ŸŽฏ ํƒ€๊ฒŸ ๊ณ ๊ฐ",
['20๋Œ€', '30๋Œ€', '์ผ๋ฐ˜', '10๋Œ€', '40๋Œ€', '50๋Œ€+', '๋‚จ์„ฑ', '์—ฌ์„ฑ', '์ง์žฅ์ธ', 'ํ•™์ƒ', '์ฃผ๋ถ€'],
help="ํƒ€๊ฒŸ ๊ณ ๊ฐ์— ๋งž๋Š” ํ†ค์•ค๋งค๋„ˆ๋กœ ์นดํ”ผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค"
)
# ๋ธŒ๋žœ๋“œ ํ†ค์•ค๋งค๋„ˆ
brand_tone = st.sidebar.selectbox(
"๐ŸŽจ ๋ธŒ๋žœ๋“œ ํ†ค",
['์„ธ๋ จ๋œ', '์นœ๊ทผํ•œ', '๊ณ ๊ธ‰์Šค๋Ÿฌ์šด', 'ํ™œ๊ธฐ์ฐฌ', '์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š”', '์ Š์€', '๋”ฐ๋œปํ•œ', '์ „๋ฌธ์ ์ธ'],
help="์›ํ•˜๋Š” ๋ธŒ๋žœ๋“œ ์ด๋ฏธ์ง€๋ฅผ ์„ ํƒํ•˜์„ธ์š”"
)
# ์ฐฝ์˜์„ฑ ์ˆ˜์ค€
creative_level = st.sidebar.select_slider(
"๐Ÿง  ์ฐฝ์˜์„ฑ ์ˆ˜์ค€",
options=['๋ณด์ˆ˜์ ', '๊ท ํ˜•', '์ฐฝ์˜์ '],
value='๊ท ํ˜•',
help="๋ณด์ˆ˜์ : ์•ˆ์ „ํ•œ ํ‘œํ˜„, ์ฐฝ์˜์ : ๋…์ฐฝ์  ํ‘œํ˜„"
)
# ๋ฉ”์ธ ์ž…๋ ฅ ์˜์—ญ
st.markdown("## ๐Ÿ’ญ ์–ด๋–ค ์นดํ”ผ๋ฅผ ๋งŒ๋“ค๊ณ  ์‹ถ์œผ์‹ ๊ฐ€์š”?")
# ์ž…๋ ฅ ๋ฐฉ์‹ ์„ ํƒ
input_method = st.radio(
"์ž…๋ ฅ ๋ฐฉ์‹ ์„ ํƒ:",
["์ง์ ‘ ์ž…๋ ฅ", "ํ…œํ”Œ๋ฆฟ ์„ ํƒ"],
horizontal=True
)
if input_method == "์ง์ ‘ ์ž…๋ ฅ":
user_request = st.text_area(
"์นดํ”ผ ์š”์ฒญ์„ ์ž์„ธํžˆ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”:",
placeholder="์˜ˆ: 30๋Œ€ ์ง์žฅ ์—ฌ์„ฑ์šฉ ํ”„๋ฆฌ๋ฏธ์—„ ์Šคํ‚จ์ผ€์–ด ์‹ ์ œํ’ˆ ๋Ÿฐ์นญ ์นดํ”ผ",
height=100
)
else:
# ํ…œํ”Œ๋ฆฟ ์„ ํƒ
templates = {
"์‹ ์ œํ’ˆ ๋Ÿฐ์นญ": "๋Œ€์ƒ {์นดํ…Œ๊ณ ๋ฆฌ} ์‹ ์ œํ’ˆ ๋Ÿฐ์นญ ์นดํ”ผ",
"ํ• ์ธ ์ด๋ฒคํŠธ": "{์นดํ…Œ๊ณ ๋ฆฌ} ํ• ์ธ ์ด๋ฒคํŠธ ํ”„๋กœ๋ชจ์…˜ ์นดํ”ผ",
"๋ธŒ๋žœ๋“œ ์Šฌ๋กœ๊ฑด": "{์นดํ…Œ๊ณ ๋ฆฌ} ๋ธŒ๋žœ๋“œ์˜ ๋Œ€ํ‘œ ์Šฌ๋กœ๊ฑด",
"์•ฑ/์„œ๋น„์Šค ๋ฆฌ๋‰ด์–ผ": "{์„œ๋น„์Šค๋ช…} ์ƒˆ ๋ฒ„์ „ ์ถœ์‹œ ์นดํ”ผ",
"์‹œ์ฆŒ ํ•œ์ •": "{์‹œ์ฆŒ} ํ•œ์ • {์นดํ…Œ๊ณ ๋ฆฌ} ํŠน๋ณ„ ์—๋””์…˜ ์นดํ”ผ"
}
selected_template = st.selectbox("ํ…œํ”Œ๋ฆฟ ์„ ํƒ:", list(templates.keys()))
col1, col2 = st.columns(2)
with col1:
template_category = st.text_input("์ œํ’ˆ/์„œ๋น„์Šค:", value="")
with col2:
if selected_template == "์•ฑ/์„œ๋น„์Šค ๋ฆฌ๋‰ด์–ผ":
service_name = st.text_input("์„œ๋น„์Šค๋ช…:", placeholder="์˜ˆ: ๋ฐฐ๋‹ฌ์•ฑ, ๊ธˆ์œต์•ฑ")
user_request = templates[selected_template].format(์„œ๋น„์Šค๋ช…=service_name)
elif selected_template == "์‹œ์ฆŒ ํ•œ์ •":
season = st.selectbox("์‹œ์ฆŒ:", ["๋ด„", "์—ฌ๋ฆ„", "๊ฐ€์„", "๊ฒจ์šธ", "ํฌ๋ฆฌ์Šค๋งˆ์Šค", "์‹ ๋…„"])
user_request = templates[selected_template].format(์‹œ์ฆŒ=season, ์นดํ…Œ๊ณ ๋ฆฌ=template_category)
else:
user_request = templates[selected_template].format(์นดํ…Œ๊ณ ๋ฆฌ=template_category)
st.text_area("์ƒ์„ฑ๋œ ์š”์ฒญ:", value=user_request, height=80, disabled=True)
# ๊ณ ๊ธ‰ ์˜ต์…˜
with st.expander("๐Ÿ”ง ๊ณ ๊ธ‰ ์˜ต์…˜"):
col1, col2 = st.columns(2)
with col1:
num_concepts = st.slider("์ƒ์„ฑํ•  ์ปจ์…‰ ์ˆ˜:", 1, 5, 3)
min_similarity = st.slider("์ตœ์†Œ ์œ ์‚ฌ๋„:", 0.0, 1.0, 0.3, 0.1)
with col2:
show_references = st.checkbox("์ฐธ๊ณ  ์นดํ”ผ ๋ณด๊ธฐ", value=True)
num_references = st.slider("์ฐธ๊ณ  ์นดํ”ผ ์ˆ˜:", 3, 10, 5)
# RAG ์นดํ”ผ ์ƒ์„ฑ ํ•จ์ˆ˜ (์ž„๋ฒ ๋”ฉ ๊ธฐ๋ฐ˜ ํ•„์ˆ˜!)
def generate_copy_with_rag(user_request, category, target, tone, creative, num_concepts):
"""RAG ๊ธฐ๋ฐ˜ ์นดํ”ผ ์ƒ์„ฑ - ์ž„๋ฒ ๋”ฉ ํ•„์ˆ˜ ์‚ฌ์šฉ"""
if not user_request.strip():
st.error("โŒ ์นดํ”ผ ์š”์ฒญ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”")
return None
# ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ
progress_bar = st.progress(0)
status_text = st.empty()
# 1๋‹จ๊ณ„: ์˜๋ฏธ์  ๊ฒ€์ƒ‰ (์ž„๋ฒ ๋”ฉ ๊ธฐ๋ฐ˜)
status_text.text("๐Ÿ” ์˜๋ฏธ์  ๊ฒ€์ƒ‰ ์ค‘... (RAG ํ•ต์‹ฌ ๊ธฐ๋Šฅ)")
progress_bar.progress(20)
try:
# ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ ์ƒ์„ฑ ๋ฐ ์ž„๋ฒ ๋”ฉ
search_query = f"{user_request} {target} ๊ด‘๊ณ  ์นดํ”ผ"
from sklearn.metrics.pairwise import cosine_similarity
query_embedding = embedding_model.encode([search_query])
# ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ๋ง
if category != '์ „์ฒด':
filtered_df = df[df['์นดํ…Œ๊ณ ๋ฆฌ'] == category]
else:
filtered_df = df
progress_bar.progress(40)
# ์œ ์‚ฌ๋„ ๊ณ„์‚ฐ (์ž„๋ฒ ๋”ฉ์˜ ํ•ต์‹ฌ!)
filtered_indices = filtered_df.index.tolist()
filtered_embeddings = embeddings[filtered_indices]
similarities = cosine_similarity(query_embedding, filtered_embeddings)[0]
# ์ƒ์œ„ ์ฐธ๊ณ  ์นดํ”ผ ์„ ๋ณ„
top_indices = np.argsort(similarities)[::-1][:num_references]
reference_copies = []
for idx in top_indices:
original_idx = filtered_indices[idx]
row = df.iloc[original_idx]
if similarities[idx] >= min_similarity:
reference_copies.append({
'copy': row['์นดํ”ผ ๋‚ด์šฉ'],
'brand': row['๋ธŒ๋žœ๋“œ'],
'similarity': similarities[idx]
})
progress_bar.progress(60)
if not reference_copies:
st.warning(f"โš ๏ธ ์œ ์‚ฌ๋„ {min_similarity} ์ด์ƒ์ธ ์ฐธ๊ณ  ์นดํ”ผ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์œ ์‚ฌ๋„๋ฅผ ๋‚ฎ์ถฐ๋ณด์„ธ์š”.")
progress_bar.empty()
status_text.empty()
return None
# 2๋‹จ๊ณ„: AI ์นดํ”ผ ์ƒ์„ฑ
status_text.text("๐Ÿค– AI ์นดํ”ผ ์ƒ์„ฑ ์ค‘...")
progress_bar.progress(80)
# ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
references_text = "\n".join([
f"{i}. \"{ref['copy']}\" - {ref['brand']} (์œ ์‚ฌ๋„: {ref['similarity']:.3f})"
for i, ref in enumerate(reference_copies, 1)
])
creativity_guidance = {
"๋ณด์ˆ˜์ ": "์•ˆ์ „ํ•˜๊ณ  ๊ฒ€์ฆ๋œ ํ‘œํ˜„์„ ์‚ฌ์šฉํ•˜์—ฌ",
"๊ท ํ˜•": "์ฐฝ์˜์ ์ด๋ฉด์„œ๋„ ์ ์ ˆํ•œ ์ˆ˜์ค€์—์„œ",
"์ฐฝ์˜์ ": "๋…์ฐฝ์ ์ด๊ณ  ํ˜์‹ ์ ์ธ ํ‘œํ˜„์œผ๋กœ"
}
prompt = f"""
๋‹น์‹ ์€ ํ•œ๊ตญ์˜ ์ „๋ฌธ ๊ด‘๊ณ  ์นดํ”ผ๋ผ์ดํ„ฐ์ž…๋‹ˆ๋‹ค.
**์š”์ฒญ์‚ฌํ•ญ:** {user_request}
**ํƒ€๊ฒŸ ๊ณ ๊ฐ:** {target}
**๋ธŒ๋žœ๋“œ ํ†ค:** {tone}
**์ฐฝ์˜์„ฑ ์ˆ˜์ค€:** {creative}
**์ฐธ๊ณ  ์นดํ”ผ๋“ค (์˜๋ฏธ์  ์œ ์‚ฌ๋„ ๊ธฐ๋ฐ˜ ์„ ๋ณ„):**
{references_text}
**์ž‘์„ฑ ๊ฐ€์ด๋“œ๋ผ์ธ:**
1. ์œ„ ์ฐธ๊ณ  ์นดํ”ผ๋“ค์˜ ์Šคํƒ€์ผ๊ณผ ํ†ค์„ ๋ถ„์„ํ•˜์—ฌ ์œ ์‚ฌํ•œ ๋А๋‚Œ์œผ๋กœ ์ž‘์„ฑ
2. {creativity_guidance[creative]} ์ƒˆ๋กœ์šด ์นดํ”ผ {num_concepts}๊ฐœ๋ฅผ ์ž‘์„ฑ
3. ๊ฐ ์นดํ”ผ๋Š” ํ•œ๊ตญ์–ด๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ๋งค๋ ฅ์ ์ด์–ด์•ผ ํ•จ
4. {target}์—๊ฒŒ ์–ดํ•„ํ•  ์ˆ˜ ์žˆ๋Š” ํ‘œํ˜„ ์‚ฌ์šฉ
5. {tone} ํ†ค์•ค๋งค๋„ˆ ์œ ์ง€
**์ถœ๋ ฅ ํ˜•์‹:**
1. [์นดํ”ผ1]
- ์„ค๋ช…: ์™œ ์ด ์นดํ”ผ๊ฐ€ ํšจ๊ณผ์ ์ธ์ง€ ๊ฐ„๋‹จํžˆ ์„ค๋ช…
2. [์นดํ”ผ2]
- ์„ค๋ช…: ์™œ ์ด ์นดํ”ผ๊ฐ€ ํšจ๊ณผ์ ์ธ์ง€ ๊ฐ„๋‹จํžˆ ์„ค๋ช…
3. [์นดํ”ผ3]
- ์„ค๋ช…: ์™œ ์ด ์นดํ”ผ๊ฐ€ ํšจ๊ณผ์ ์ธ์ง€ ๊ฐ„๋‹จํžˆ ์„ค๋ช…
**์ถ”์ฒœ ์นดํ”ผ:** ์œ„ ์ค‘ ๊ฐ€์žฅ ์ถ”์ฒœํ•˜๋Š” ์นดํ”ผ์™€ ์ด์œ 
"""
response = model.generate_content(prompt)
progress_bar.progress(100)
status_text.text("โœ… ์™„๋ฃŒ!")
time.sleep(0.5)
progress_bar.empty()
status_text.empty()
return {
'references': reference_copies,
'generated_content': response.text,
'search_info': {
'query': search_query,
'total_candidates': len(filtered_df),
'selected_references': len(reference_copies)
},
'settings': {
'category': category,
'target': target,
'tone': tone,
'creative': creative
}
}
except Exception as e:
st.error(f"โŒ ์นดํ”ผ ์ƒ์„ฑ ์‹คํŒจ: {e}")
progress_bar.empty()
status_text.empty()
return None
# ์ƒ์„ฑ ๋ฒ„ํŠผ
if st.button("๐Ÿš€ ์นดํ”ผ ์ƒ์„ฑํ•˜๊ธฐ", type="primary", use_container_width=True):
if not user_request or not user_request.strip():
st.error("โŒ ์นดํ”ผ ์š”์ฒญ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”")
else:
# RAG ์นดํ”ผ ์ƒ์„ฑ
result = generate_copy_with_rag(
user_request=user_request,
category=selected_category,
target=target_audience,
tone=brand_tone,
creative=creative_level,
num_concepts=num_concepts
)
if result:
# ๊ฒฐ๊ณผ ํ‘œ์‹œ
st.markdown("## ๐ŸŽ‰ ์ƒ์„ฑ๋œ ์นดํ”ผ")
st.markdown("---")
# ๊ฒ€์ƒ‰ ์ •๋ณด ํ‘œ์‹œ
st.info(f"๐Ÿ” **๊ฒ€์ƒ‰ ์ •๋ณด**: {result['search_info']['total_candidates']:,}๊ฐœ ํ›„๋ณด์—์„œ "
f"{result['search_info']['selected_references']}๊ฐœ ์ฐธ๊ณ  ์นดํ”ผ ์„ ๋ณ„")
# ์ฐธ๊ณ  ์นดํ”ผ ํ‘œ์‹œ
if show_references and result['references']:
with st.expander("๐Ÿ“š ์ฐธ๊ณ ํ•œ ์นดํ”ผ๋“ค (์˜๋ฏธ์  ์œ ์‚ฌ๋„ ๊ธฐ๋ฐ˜ ์„ ๋ณ„)"):
for i, ref in enumerate(result['references'], 1):
st.markdown(f"**{i}.** \"{ref['copy']}\"")
st.markdown(f" - ๋ธŒ๋žœ๋“œ: {ref['brand']}")
st.markdown(f" - ์œ ์‚ฌ๋„: {ref['similarity']:.3f}")
st.markdown("")
# ์ƒ์„ฑ๋œ ์นดํ”ผ ํ‘œ์‹œ
st.markdown("### โœจ AI๊ฐ€ ์ƒ์„ฑํ•œ ์นดํ”ผ:")
st.markdown(result['generated_content'])
# ๊ฒฐ๊ณผ ๋‹ค์šด๋กœ๋“œ
result_json = json.dumps({
'timestamp': datetime.now().isoformat(),
'request': user_request,
'settings': result['settings'],
'search_info': result['search_info'],
'generated_content': result['generated_content']
}, ensure_ascii=False, indent=2)
st.download_button(
label="๐Ÿ’พ ๊ฒฐ๊ณผ ๋‹ค์šด๋กœ๋“œ (JSON)",
data=result_json,
file_name=f"copy_result_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
mime="application/json"
)
# ์‹œ์Šคํ…œ ์ •๋ณด (์‚ฌ์ด๋“œ๋ฐ” ํ•˜๋‹จ)
st.sidebar.markdown("---")
st.sidebar.markdown("### ๐Ÿ“Š RAG ์‹œ์Šคํ…œ ์ •๋ณด")
if df is not None and embeddings is not None:
st.sidebar.markdown(f"**์นดํ”ผ ๋ฐ์ดํ„ฐ**: {len(df):,}๊ฐœ")
st.sidebar.markdown(f"**์นดํ…Œ๊ณ ๋ฆฌ**: {df['์นดํ…Œ๊ณ ๋ฆฌ'].nunique()}๊ฐœ")
st.sidebar.markdown(f"**๋ธŒ๋žœ๋“œ**: {df['๋ธŒ๋žœ๋“œ'].nunique()}๊ฐœ")
st.sidebar.markdown(f"**์ž„๋ฒ ๋”ฉ**: {embeddings.shape[1]}์ฐจ์›")
st.sidebar.markdown("**๊ฒ€์ƒ‰ ์—”์ง„**: Korean SBERT")
st.sidebar.markdown("**ํ˜ธ์ŠคํŒ…**: ๐Ÿค— Hugging Face")
# ์‚ฌ์šฉ๋ฒ• ๊ฐ€์ด๋“œ
with st.expander("๐Ÿ’ก RAG ์‹œ์Šคํ…œ ์‚ฌ์šฉ๋ฒ• ๊ฐ€์ด๋“œ"):
st.markdown("""
### ๐ŸŽฏ ํšจ๊ณผ์ ์ธ ์‚ฌ์šฉ๋ฒ•
**1. ๊ตฌ์ฒด์ ์ธ ์š”์ฒญํ•˜๊ธฐ:**
- โŒ "์นดํ”ผ ์จ์ค˜"
- โœ… "30๋Œ€ ์ง์žฅ ์—ฌ์„ฑ์šฉ ํ”„๋ฆฌ๋ฏธ์—„ ์Šคํ‚จ์ผ€์–ด ์‹ ์ œํ’ˆ ๋Ÿฐ์นญ ์นดํ”ผ"
**2. RAG ์‹œ์Šคํ…œ์˜ ์žฅ์ :**
- ๐Ÿง  **์˜๋ฏธ์  ๊ฒ€์ƒ‰**: ํ‚ค์›Œ๋“œ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์˜๋ฏธ๊นŒ์ง€ ์ดํ•ด
- ๐ŸŽฏ **๋ฌธ๋งฅ ๋งค์นญ**: ํƒ€๊ฒŸ๊ณผ ์ƒํ™ฉ์— ๋งž๋Š” ์นดํ”ผ ์ž๋™ ์„ ๋ณ„
- ๐Ÿ“Š **๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜**: 37,671๊ฐœ ์‹ค์ œ ์นดํ”ผ์—์„œ ํ•™์Šตํ•œ ํŒจํ„ด
**3. ์ฐฝ์˜์„ฑ ์กฐ์ ˆ:**
- **๋ณด์ˆ˜์ **: ์•ˆ์ „ํ•œ ํด๋ผ์ด์–ธํŠธ, ๊ฒ€์ฆ๋œ ์ ‘๊ทผ
- **๊ท ํ˜•**: ์ผ๋ฐ˜์ ์ธ ํ”„๋กœ์ ํŠธ (์ถ”์ฒœ!)
- **์ฐฝ์˜์ **: ํ˜์‹ ์  ๋ธŒ๋žœ๋“œ, ํŒŒ๊ฒฉ์  ์บ ํŽ˜์ธ
**4. ์ฐธ๊ณ  ์นดํ”ผ ํ™œ์šฉ:**
- ์ƒ์„ฑ๋œ ์นดํ”ผ์™€ ์ฐธ๊ณ  ์นดํ”ผ๋ฅผ ๋น„๊ต ๋ถ„์„
- ํŠธ๋ Œ๋“œ์™€ ํŒจํ„ด ํŒŒ์•… ๊ฐ€๋Šฅ
- ๊ฒฝ์Ÿ์‚ฌ ๋ถ„์„ ์ž๋ฃŒ๋กœ ํ™œ์šฉ
""")
# ํ‘ธํ„ฐ
st.markdown("---")
st.markdown(
"๐Ÿ’ก **AI ์นดํ”ผ๋ผ์ดํ„ฐ** | 37,671๊ฐœ ์‹ค์ œ ๊ด‘๊ณ  ์นดํ”ผ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ | "
"RAG(๊ฒ€์ƒ‰ ์ฆ๊ฐ• ์ƒ์„ฑ) ์‹œ์Šคํ…œ powered by Korean SBERT + Gemini AI"
)
# ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง (๊ฐœ๋ฐœ์ž์šฉ)
if os.getenv("DEBUG_MODE"):
st.sidebar.markdown("### ๐Ÿ”ง ๋””๋ฒ„๊ทธ ์ •๋ณด")
if 'embeddings' in locals():
st.sidebar.write(f"์ž„๋ฒ ๋”ฉ ๋ฉ”๋ชจ๋ฆฌ: {embeddings.nbytes / (1024*1024):.1f}MB")
st.sidebar.write(f"Streamlit ๋ฒ„์ „: {st.__version__}")