streamlit / src /app.py
kbg05204's picture
Upload src/app.py with huggingface_hub
119a176 verified
import os, json, re, io
import streamlit as st
from openai import OpenAI
from PIL import Image
from datetime import datetime
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 1. ์ดˆ๊ธฐ ์„ค์ • ๋ฐ CSS ๋กœ๋“œ
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def local_css(file_name):
current_dir = os.path.dirname(os.path.abspath(__file__))
css_path = os.path.join(current_dir, file_name)
if os.path.exists(css_path):
with open(css_path, encoding="utf-8") as f:
st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True)
st.set_page_config(page_title="MeetingAI | KT Enterprise", page_icon="๐ŸŽ™๏ธ", layout="wide")
local_css("style.css")
# ๊ฒฝ๋กœ ๋ฐ ํด๋ผ์ด์–ธํŠธ ์„ค์ •
current_dir = os.path.dirname(os.path.abspath(__file__))
img_path = os.path.join(current_dir, "kt.png")
client = OpenAI()
# ์„ธ์…˜ ์ƒํƒœ ์ดˆ๊ธฐํ™”
if "analysis_result" not in st.session_state:
st.session_state.analysis_result = None
if "transcript" not in st.session_state:
st.session_state.transcript = ""
# ์šฐ์„ ์ˆœ์œ„ ๋ผ๋ฒจ ์„ค์ •
PRIORITY_LABEL = {"high": "๐Ÿ”ด ๋†’์Œ", "medium": "๐ŸŸก ๋ณดํ†ต", "low": "๐ŸŸข ๋‚ฎ์Œ"}
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 2. ๋ฐฑ์—”๋“œ ํ•ต์‹ฌ ํ•จ์ˆ˜ (๋ฏผ๊ฐ์ •๋ณด ๊ฐ์ง€, ๋ถ„์„)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def detect_sensitive(text):
SENSITIVE_PATTERNS = [
(r"๊ธฐ๋ฐ€|๋น„๋ฐ€|๋Œ€์™ธ๋น„|๋‚ด๋ถ€.*๋ณด์•ˆ|๋ณด์•ˆ.*์œ ์ง€|confidential", "๊ธฐ๋ฐ€/๋Œ€์™ธ๋น„ ํ‘œํ˜„"),
(r"๊ธ‰์—ฌ|์—ฐ๋ด‰|์ž„๊ธˆ|salary", "๊ธ‰์—ฌยท์ธ์‚ฌ ์ •๋ณด"),
(r"๊ฐœ์ธ์ •๋ณด|์ฃผ๋ฏผ|์ƒ๋…„์›”์ผ", "๊ฐœ์ธ์ •๋ณด"),
(r"\d{3}-\d{4}-\d{4}", "์ „ํ™”๋ฒˆํ˜ธ ํŒจํ„ด"),
(r"\d{6}-\d{7}", "์ฃผ๋ฏผ๋ฒˆํ˜ธ ํŒจํ„ด"),
]
return list({label for pat, label in SENSITIVE_PATTERNS if re.search(pat, text, re.IGNORECASE)})
sys_role = """
## ์—ญํ• 
๋‹น์‹ ์€ KT Enterprise IT๊ธฐ์ˆ ํ˜์‹ ํŒ€ DX Consulting์˜ AI ํšŒ์˜ ๋น„์„œ์ž…๋‹ˆ๋‹ค.
ํšŒ์˜ ๋‚ด์šฉ์„ ์ •ํ™•ํ•˜๊ณ  ์‹ ์†ํ•˜๊ฒŒ ๋ถ„์„ยท์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ์‘๋‹ต์€ ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
## ํšŒ์˜๋ก ์ •๋ฆฌ ๊ทœ์น™
1. ํšŒ์˜ ๊ฐœ์š” โ€” ์ œ๋ชฉ / ๋ชฉ์  / ์ฃผ์š” ์•ˆ๊ฑด
2. ๊ฒฐ์ •๋œ ์‚ฌํ•ญ โ€” ํ•ต์‹ฌ ๊ฒฐ์ •๋งŒ ๊ฐ„๊ฒฐํ•˜๊ฒŒ
3. ๋‹ด๋‹น ์—…๋ฌด(Action Items) โ€” [๋‹ด๋‹น์ž]:[์—…๋ฌด]/๊ธฐํ•œ/์šฐ์„ ์ˆœ์œ„
4. ์ผ์ • ์š”์•ฝ โ€” ๋งˆ๊ฐ ๊ธฐํ•œ ์‹œ๊ฐ„์ˆœ ์ •๋ฆฌ
"""
def analyze_meeting(text):
today = datetime.now().strftime("%Y๋…„ %m์›” %d์ผ")
prompt = f"""
๋‹ค์Œ์€ ์˜ค๋Š˜({today}) ์ง„ํ–‰๋œ ํšŒ์˜ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค. ์•„๋ž˜ JSON ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•˜์„ธ์š”. ๋ฐ˜๋“œ์‹œ JSON๋งŒ ์ถœ๋ ฅํ•˜์„ธ์š”.
ํšŒ์˜ ๋‚ด์šฉ:
{text}
{{
"meeting_title": "ํšŒ์˜ ์ œ๋ชฉ",
"purpose": "ํšŒ์˜ ๋ชฉ์ ",
"decisions": ["๊ฒฐ์ • ์‚ฌํ•ญ ๋ฆฌ์ŠคํŠธ"],
"tasks": [
{{"person":"๋‹ด๋‹น์ž","task":"์—…๋ฌด ๋‚ด์šฉ","deadline":"๊ธฐํ•œ","priority":"high|medium|low"}}
],
"agenda_items": ["์•ˆ๊ฑด ๋ฆฌ์ŠคํŠธ"],
"summary": "์ „์ฒด ์š”์•ฝ",
"keywords": ["ํ‚ค์›Œ๋“œ1", "ํ‚ค์›Œ๋“œ2"]
}}
"""
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "system", "content": sys_role}, {"role": "user", "content": prompt}],
temperature=0.2,
)
raw = re.sub(r"^```json\s*|```\s*$", "", resp.choices[0].message.content.strip())
return json.loads(raw)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 3. UI ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ (ํ—ค๋” ๋ฐ ์ž…๋ ฅ๋ถ€)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ์ƒ๋‹จ ํ—ค๋” ์˜์—ญ (ํฌ๊ธฐ ํ™•๋Œ€ ๋ฒ„์ „)
st.markdown("""
<div style="display:flex;align-items:center;gap:20px;padding:25px 30px;
background:linear-gradient(135deg,#16213E 0%,#0F3460 100%);
border-bottom:4px solid #E3000B;border-radius:15px;margin-bottom:25px;">
<div style="font-family:'Rajdhani',sans-serif;font-size:3.5rem;font-weight:800;
color:#E3000B;letter-spacing:1px;line-height:1;">KT</div>
<div class="title-container">
<div class="main-title" style="font-size:2.2rem; font-weight:700; color:#FFFFFF;">Meeting AI ํšŒ์˜ ๋ถ„์„ ์–ด์‹œ์Šคํ„ดํŠธ</div>
<div class="sub-title" style="font-size:1.0rem; color:#E0E0E0; margin-top:5px;">IT๊ธฐ์ˆ ํ˜์‹ ํŒ€ DX Consulting ยท Intelligent Meeting Solution</div>
</div>
</div>
""", unsafe_allow_html=True)
col1, col2 = st.columns([1, 2], gap="large")
with col1:
try:
st.image(Image.open(img_path), caption="KT DX Assistant", use_container_width=True)
except:
st.info("์ด๋ฏธ์ง€(kt.png)๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
with col2:
st.subheader("ํšŒ์˜๋ก์„ ์—…๋กœ๋“œํ•˜์‹œ๋ฉด ์š”์•ฝํ•ด๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.")
with st.container(border=True):
uploaded_file = st.file_uploader("๐Ÿ“„ ํšŒ์˜ ๋‚ด์šฉ ํ…์ŠคํŠธ ํŒŒ์ผ ์—…๋กœ๋“œ", type=["txt"])
st.write("**์š”์ฒญ ๋ฐฉ์‹ ์„ ํƒ**")
input_mode = st.radio("์š”์ฒญ ๋ฐฉ์‹", ["๐ŸŽ™๏ธ ์‹ค์‹œ๊ฐ„ ๋…น์Œ", "๐Ÿ“ ์Œ์„ฑ ํŒŒ์ผ ์—…๋กœ๋“œ", "๐Ÿ“ ํ…์ŠคํŠธ ์ž…๋ ฅ"], horizontal=True, label_visibility="collapsed")
audio_value = None
uploaded_audio_file = None
text_input = ""
if input_mode == "๐ŸŽ™๏ธ ์‹ค์‹œ๊ฐ„ ๋…น์Œ":
audio_value = st.audio_input("์Œ์„ฑ์œผ๋กœ ๋‚ด์šฉ์„ ๋งํ•ด์ฃผ์„ธ์š”.")
elif input_mode == "๐Ÿ“ ์Œ์„ฑ ํŒŒ์ผ ์—…๋กœ๋“œ":
uploaded_audio_file = st.file_uploader("์Œ์„ฑ ํŒŒ์ผ ์—…๋กœ๋“œ", type=["mp3", "wav", "m4a"])
else:
text_input = st.text_area("๋‚ด์šฉ์„ ์ง์ ‘ ์ž…๋ ฅํ•˜์„ธ์š”.", height=150)
submit = st.button("๐Ÿš€ ํšŒ์˜ ๋ถ„์„ ์‹œ์ž‘", use_container_width=True)
if submit:
meeting_text = ""
if uploaded_file:
meeting_text = uploaded_file.read().decode("utf-8")
with st.spinner("AI๊ฐ€ ๋‚ด์šฉ์„ ๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค..."):
try:
user_req = ""
if audio_value:
user_req = client.audio.transcriptions.create(model="whisper-1", file=("audio.wav", audio_value, "audio/wav")).text
elif uploaded_audio_file:
user_req = client.audio.transcriptions.create(model="whisper-1", file=(uploaded_audio_file.name, uploaded_audio_file, uploaded_audio_file.type)).text
else:
user_req = text_input
final_content = (meeting_text + "\n" + user_req).strip()
if not final_content:
st.warning("๋ถ„์„ํ•  ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค.")
else:
st.session_state.transcript = final_content
st.session_state.analysis_result = analyze_meeting(final_content)
st.rerun()
except Exception as e:
st.error(f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 4. ๊ฒฐ๊ณผ ์ถœ๋ ฅ ์˜์—ญ (Tabs ํ™œ์šฉ)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if st.session_state.analysis_result:
res = st.session_state.analysis_result
hits = detect_sensitive(st.session_state.transcript)
if hits:
st.error(f"๐Ÿ”’ ๋ฏผ๊ฐ์ •๋ณด ๊ฐ์ง€: {', '.join(hits)} ํ•ญ๋ชฉ ์ฃผ์˜")
tab1, tab2, tab3 = st.tabs(["๐Ÿ“‹ ํšŒ์˜ ์š”์•ฝ", "โœ… ํ•  ์ผ & ์šฐ์„ ์ˆœ์œ„", "๐Ÿ“„ ์›๋ฌธ ๋ณด๊ธฐ"])
with tab1:
st.markdown(f"### ๐Ÿ“Œ {res.get('meeting_title', 'ํšŒ์˜ ๊ฒฐ๊ณผ')}")
st.info(f"**ํšŒ์˜ ๋ชฉ์ :** {res.get('purpose', '๋‚ด์šฉ ์—†์Œ')}")
c1, c2 = st.columns(2)
with c1:
st.subheader("โœ… ๊ฒฐ์ • ์‚ฌํ•ญ")
for d in res.get('decisions', []): st.write(f"- {d}")
with c2:
st.subheader("๐Ÿ“Œ ์ฃผ์š” ์•ˆ๊ฑด")
for a in res.get('agenda_items', []): st.write(f"- {a}")
st.divider()
st.subheader("๐Ÿ’ฌ ์ „์ฒด ์š”์•ฝ")
st.write(res.get('summary', '์š”์•ฝ ๋‚ด์šฉ ์—†์Œ'))
with tab2:
tasks = res.get('tasks', [])
if not tasks:
st.write("๋ฐฐ์ •๋œ ํ•  ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.")
else:
for t in tasks:
p = t.get('priority', 'low').lower()
label = PRIORITY_LABEL.get(p, "๐ŸŸข ๋‚ฎ์Œ")
deadline = f" (๊ธฐํ•œ: {t['deadline']})" if t.get('deadline') and t['deadline'] != "null" else ""
st.markdown(f"**{label} [{t.get('person', '๋ฏธ์ง€์ •')}]** : {t.get('task')}{deadline}")
with tab3:
st.markdown("#### ๐Ÿ”‘ ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ")
st.write(", ".join([f"#{k}" for k in res.get('keywords', [])]))
st.divider()
st.text_area("์ „์‚ฌ ์›๋ฌธ", st.session_state.transcript, height=300)