| import os, json, re, io |
| import streamlit as st |
| from openai import OpenAI |
| from PIL import Image |
| from datetime import datetime |
|
|
| |
| |
| |
| 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": "๐ข ๋ฎ์"} |
|
|
| |
| |
| |
|
|
| 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) |
|
|
| |
| |
| |
|
|
| |
| 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}") |
|
|
| |
| |
| |
| 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) |
|
|