hyerong commited on
Commit
0fe8faa
ยท
verified ยท
1 Parent(s): 30fb914

Upload src/app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/app.py +63 -265
src/app.py CHANGED
@@ -22,9 +22,7 @@ current_dir = os.path.dirname(os.path.abspath(__file__))
22
  img_path = os.path.join(current_dir, "kt.png")
23
  client = OpenAI()
24
 
25
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
26
- # 2. ์„ธ์…˜ ์ƒํƒœ ๋ฐ ์ƒ์ˆ˜ ์„ค์ •
27
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
28
  if "analysis_result" not in st.session_state:
29
  st.session_state.analysis_result = None
30
  if "transcript" not in st.session_state:
@@ -34,10 +32,9 @@ if "transcript" not in st.session_state:
34
  PRIORITY_LABEL = {"high": "๐Ÿ”ด ๋†’์Œ", "medium": "๐ŸŸก ๋ณดํ†ต", "low": "๐ŸŸข ๋‚ฎ์Œ"}
35
 
36
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
37
- # 3. ๋ฐฑ์—”๋“œ ํ•ต์‹ฌ ํ•จ์ˆ˜ (STT, ๋ฏผ๊ฐ์ •๋ณด, ๋ถ„์„)
38
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
39
 
40
- # ๋ฏผ๊ฐ์ •๋ณด ๊ฐ์ง€ ํ•จ์ˆ˜
41
  def detect_sensitive(text):
42
  SENSITIVE_PATTERNS = [
43
  (r"๊ธฐ๋ฐ€|๋น„๋ฐ€|๋Œ€์™ธ๋น„|๋‚ด๋ถ€.*๋ณด์•ˆ|๋ณด์•ˆ.*์œ ์ง€|confidential", "๊ธฐ๋ฐ€/๋Œ€์™ธ๋น„ ํ‘œํ˜„"),
@@ -48,7 +45,6 @@ def detect_sensitive(text):
48
  ]
49
  return list({label for pat, label in SENSITIVE_PATTERNS if re.search(pat, text, re.IGNORECASE)})
50
 
51
- # AI ๋น„์„œ ์—ญํ•  ์ •์˜
52
  sys_role = """
53
  ## ์—ญํ• 
54
  ๋‹น์‹ ์€ KT Enterprise IT๊ธฐ์ˆ ํ˜์‹ ํŒ€ DX Consulting์˜ AI ํšŒ์˜ ๋น„์„œ์ž…๋‹ˆ๋‹ค.
@@ -59,313 +55,117 @@ sys_role = """
59
  2. ๊ฒฐ์ •๋œ ์‚ฌํ•ญ โ€” ํ•ต์‹ฌ ๊ฒฐ์ •๋งŒ ๊ฐ„๊ฒฐํ•˜๊ฒŒ
60
  3. ๋‹ด๋‹น ์—…๋ฌด(Action Items) โ€” [๋‹ด๋‹น์ž]:[์—…๋ฌด]/๊ธฐํ•œ/์šฐ์„ ์ˆœ์œ„
61
  4. ์ผ์ • ์š”์•ฝ โ€” ๋งˆ๊ฐ ๊ธฐํ•œ ์‹œ๊ฐ„์ˆœ ์ •๋ฆฌ
62
-
63
- ## ์ฃผ์˜์‚ฌํ•ญ
64
- - ํšŒ์˜๋ก์— ์—†๋Š” ๋‚ด์šฉ์€ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
65
- - ๋ถˆ๋ช…ํ™•ํ•œ ๋‚ด์šฉ์€ 'ํ™•์ธ ํ•„์š”'๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.
66
  """
67
 
68
- # ํšŒ์˜ ๋ถ„์„ ํ•จ์ˆ˜
69
  def analyze_meeting(text):
70
  today = datetime.now().strftime("%Y๋…„ %m์›” %d์ผ")
71
  prompt = f"""
72
- ๋‹ค์Œ์€ ์˜ค๋Š˜({today}) ์ง„ํ–‰๋œ ํšŒ์˜ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค.
73
- ์•„๋ž˜ JSON ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•˜์„ธ์š”. ๋ฐ˜๋“œ์‹œ JSON๋งŒ ์ถœ๋ ฅํ•˜์„ธ์š”.
74
 
75
  ํšŒ์˜ ๋‚ด์šฉ:
76
  {text}
77
 
78
  {{
79
  "meeting_title": "ํšŒ์˜ ์ œ๋ชฉ",
80
- "purpose": "ํšŒ์˜ ๋ชฉ์  (2-3๋ฌธ์žฅ)",
81
- "decisions": ["๊ฒฐ์ • ์‚ฌํ•ญ 1", "๊ฒฐ์ • ์‚ฌํ•ญ 2"],
82
  "tasks": [
83
- {{"person":"๋‹ด๋‹น์ž","task":"์—…๋ฌด ๋‚ด์šฉ","deadline":"๊ธฐํ•œ ๋˜๋Š” null","priority":"high|medium|low"}}
84
  ],
85
- "agenda_items": ["์•ˆ๊ฑด 1", "์•ˆ๊ฑด 2"],
86
- "next_meeting": "๋‹ค์Œ ํšŒ์˜ ์ผ์ • ๋˜๋Š” null",
87
- "summary": "์ „์ฒด ์š”์•ฝ (4-5๋ฌธ์žฅ)",
88
- "keywords": ["ํ‚ค์›Œ๋“œ1", "ํ‚ค์›Œ๋“œ2", "ํ‚ค์›Œ๋“œ3"]
89
  }}
90
  """
91
  resp = client.chat.completions.create(
92
  model="gpt-4o",
93
- messages=[{"role": "system", "content": sys_role},
94
- {"role": "user", "content": prompt}],
95
  temperature=0.2,
96
  )
97
  raw = re.sub(r"^```json\s*|```\s*$", "", resp.choices[0].message.content.strip())
98
  return json.loads(raw)
99
 
100
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
101
- # 4. UI ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ
102
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
103
- # [์ˆ˜์ •๋œ ํ—ค๋” ์˜์—ญ] - ํ…์ŠคํŠธ ์‚ฌ์ด์ฆˆ๋ฅผ ๋Œ€ํญ ํ‚ค์šฐ๊ณ  ํด๋ž˜์Šค๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
 
104
  st.markdown("""
105
  <div style="display:flex;align-items:center;gap:20px;padding:25px 30px;
106
  background:linear-gradient(135deg,#16213E 0%,#0F3460 100%);
107
  border-bottom:4px solid #E3000B;border-radius:15px;margin-bottom:25px;">
108
- <!-- KT ๋กœ๊ณ  ํฌ๊ธฐ ํ™•๋Œ€ -->
109
  <div style="font-family:'Rajdhani',sans-serif;font-size:3.5rem;font-weight:800;
110
  color:#E3000B;letter-spacing:1px;line-height:1;">KT</div>
111
  <div class="title-container">
112
- <!-- ๋ฉ”์ธ ํƒ€์ดํ‹€ ํฌ๊ธฐ ํ™•๋Œ€ (style.css์™€ ์—ฐ๋™) -->
113
- <div class="main-title" style="font-size:2.2rem; font-weight:700; color:#FFFFFF;">
114
- Meeting AI ํšŒ์˜ ๋ถ„์„ ์–ด์‹œ์Šคํ„ดํŠธ
115
- </div>
116
- <!-- ์„œ๋ธŒ ํƒ€์ดํ‹€ ํฌ๊ธฐ ํ™•๋Œ€ -->
117
- <div class="sub-title" style="font-size:1.0rem; color:#E0E0E0; margin-top:5px;">
118
- IT๊ธฐ์ˆ ํ˜์‹ ํŒ€ DX Consulting ยท Intelligent Meeting Solution
119
- </div>
120
  </div>
121
  </div>
122
  """, unsafe_allow_html=True)
123
 
124
  col1, col2 = st.columns([1, 2], gap="large")
125
 
126
- # ์ขŒ์ธก: ์ด๋ฏธ์ง€ ๋ฐ ์ž…๋ ฅ ์•ˆ๋‚ด
127
  with col1:
128
  try:
129
  st.image(Image.open(img_path), caption="KT DX Assistant", use_container_width=True)
130
  except:
131
  st.info("์ด๋ฏธ์ง€(kt.png)๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
132
 
133
- # ์šฐ์ธก: ์ž…๋ ฅ ํผ
134
  with col2:
135
  st.subheader("ํšŒ์˜๋ก์„ ์—…๋กœ๋“œํ•˜์‹œ๋ฉด ์š”์•ฝํ•ด๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.")
136
-
137
- # โ”€โ”€ ํšŒ์˜๋ก ํŒŒ์ผ ์—…๋กœ๋“œ (ํ•ญ์ƒ ํ‘œ์‹œ) โ”€โ”€
138
- uploaded_file = st.file_uploader("๐Ÿ“„ ํšŒ์˜ ๋‚ด์šฉ ํ…์ŠคํŠธ ํŒŒ์ผ ์—…๋กœ๋“œ", type=["txt"])
139
-
140
- # โ”€โ”€ ์š”์ฒญ ๋ฐฉ์‹ ์„ ํƒ (form ๋ฐ– โ†’ ์„ ํƒ ์ฆ‰์‹œ ์นธ์ด ๋ฐ”๋€œ) โ”€โ”€
141
- st.write("**์š”์ฒญ ๋ฐฉ์‹ ์„ ํƒ**")
142
- input_mode = st.radio(
143
- "์š”์ฒญ ๋ฐฉ์‹",
144
- ["๐ŸŽ™๏ธ ์‹ค์‹œ๊ฐ„ ์Œ์„ฑ ๋…น์Œ", "๐Ÿ“ ์Œ์„ฑ ํŒŒ์ผ ์—…๋กœ๋“œ", "๐Ÿ“ ํ…์ŠคํŠธ ์ง์ ‘ ์ž…๋ ฅ"],
145
- horizontal=True,
146
- label_visibility="collapsed",
147
- )
148
-
149
- # โ”€โ”€ ์„ ํƒ๋œ ๋ฐฉ์‹์— ๋”ฐ๋ผ ์ž…๋ ฅ ์นธ ํ‘œ์‹œ โ”€โ”€
150
- audio_value = None
151
- uploaded_audio_file = None
152
- text_input = ""
153
-
154
- if input_mode == "๐ŸŽ™๏ธ ์‹ค์‹œ๊ฐ„ ์Œ์„ฑ ๋…น์Œ":
155
- audio_value = st.audio_input("๋ฌด์—‡์„ ๋„์™€๋“œ๋ฆด๊นŒ์š”?")
156
-
157
- elif input_mode == "๐Ÿ“ ์Œ์„ฑ ํŒŒ์ผ ์—…๋กœ๋“œ":
158
- uploaded_audio_file = st.file_uploader(
159
- "์š”์ฒญ ์Œ์„ฑ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜์„ธ์š”",
160
- type=["mp3", "wav", "m4a"],
161
- )
162
-
163
- else: # ํ…์ŠคํŠธ ์ง์ ‘ ์ž…๋ ฅ
164
- text_input = st.text_area("์š”์ฒญ ์‚ฌํ•ญ์„ ์ž…๋ ฅํ•˜์„ธ์š”", height=120,
165
- placeholder="์˜ˆ) ํšŒ์˜ ๋‚ด์šฉ์„ ์š”์•ฝํ•ด์ค˜ / ๊น€๋Œ€๋ฆฌ ํ• ์ผ๋งŒ ์ •๋ฆฌํ•ด์ค˜")
166
-
167
- # โ”€โ”€ ์‹คํ–‰ ๋ฒ„ํŠผ โ”€โ”€
168
- submit = st.button("์‹คํ–‰", use_container_width=True)
169
-
170
- # โ”€โ”€ ์‹คํ–‰ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ โ”€โ”€
171
- if submit:
172
- # ํšŒ์˜๋ก ํ…์ŠคํŠธ ํŒŒ์ผ ์ฝ๊ธฐ
173
- text = ""
174
- if uploaded_file is not None:
175
- text = uploaded_file.read().decode("utf-8")
176
-
177
- # ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ
178
- if audio_value is None and uploaded_audio_file is None and not text_input.strip():
179
- st.warning("์š”์ฒญ ๋ฐฉ์‹์„ ์„ ํƒํ•˜๊ณ  ๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
180
- st.stop()
181
-
182
- # ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ๊ตฌ์„ฑ
183
- system_content = sys_role
184
- if text:
185
- system_content += f"\n\n๋‹ค์Œ ํšŒ์˜ ๋‚ด์šฉ์„ ์ฐธ๊ณ ํ•˜์„ธ์š”:\n{text}"
186
- input_messages = [{"role": "system", "content": system_content}]
187
-
188
- # STT ๋˜๋Š” ํ…์ŠคํŠธ ์ฒ˜๋ฆฌ
189
- if audio_value is not None:
190
- transcript = client.audio.transcriptions.create(
191
- model="whisper-1",
192
- file=("audio.wav", audio_value, "audio/wav"),
193
- )
194
- user_text = transcript.text
195
-
196
- elif uploaded_audio_file is not None:
197
- transcript = client.audio.transcriptions.create(
198
- model="whisper-1",
199
- file=(uploaded_audio_file.name, uploaded_audio_file, uploaded_audio_file.type),
200
- )
201
- user_text = transcript.text
202
-
203
- else:
204
- user_text = text_input.strip()
205
-
206
- input_messages.append({"role": "user", "content": user_text})
207
- # with col2:
208
- # st.markdown('<div class="input-title">๐ŸŽ™ ํšŒ์˜ ๋‚ด์šฉ ์ž…๋ ฅ</div>', unsafe_allow_html=True)
209
- # with st.form("task_form"):
210
- # uploaded_file = st.file_uploader("ํ…์ŠคํŠธ ํŒŒ์ผ ์—…๋กœ๋“œ (.txt)", type=["txt"])
211
- # audio_value = st.audio_input("์Œ์„ฑ์œผ๋กœ ํšŒ์˜ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”")
212
- # submit = st.form_submit_button("๐Ÿ” ํšŒ์˜ ๋ถ„์„ ์‹œ์ž‘")
213
-
214
- # if submit:
215
- # final_text = ""
216
- # # 1. ํ…์ŠคํŠธ ํŒŒ์ผ ์ฒ˜๋ฆฌ
217
- # if uploaded_file:
218
- # final_text = uploaded_file.read().decode("utf-8")
219
 
220
- # # 2. ์Œ์„ฑ ํŒŒ์ผ STT ์ฒ˜๋ฆฌ
221
- # if audio_value:
222
- # with st.spinner("๐ŸŽ™๏ธ ์Œ์„ฑ์„ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ค‘..."):
223
- # transcript = client.audio.transcriptions.create(
224
- # model="whisper-1",
225
- # file=("audio.wav", audio_value, "audio/wav"),
226
- # language="ko"
227
- # ).text
228
- # final_text += "\n" + transcript
229
 
230
- # if not final_text.strip():
231
- # st.warning("โš ๏ธ ๋ถ„์„ํ•  ๋‚ด์šฉ(ํ…์ŠคํŠธ ๋˜๋Š” ์Œ์„ฑ)์ด ์—†์Šต๋‹ˆ๋‹ค.")
232
- # else:
233
- # with st.spinner("๐Ÿค– AI๊ฐ€ ํšŒ์˜๋ก์„ ๋ถ„์„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค..."):
234
- # try:
235
- # st.session_state.transcript = final_text
236
- # st.session_state.analysis_result = analyze_meeting(final_text)
237
- # except Exception as e:
238
- # st.error(f"๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
239
-
240
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
241
- # 5. ๊ฒฐ๊ณผ ์ถœ๋ ฅ ์˜์—ญ (Tabs ํ™œ์šฉ)
242
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
243
- if st.session_state.analysis_result:
244
- res = st.session_state.analysis_result
245
-
246
- # ๋ณด์•ˆ ๊ฒฝ๊ณ  ์ถœ๋ ฅ
247
- hits = detect_sensitive(st.session_state.transcript)
248
- if hits:
249
- st.error(f"๐Ÿ”’ ๋ฏผ๊ฐ์ •๋ณด ๊ฐ์ง€: {', '.join(hits)} ํ•ญ๋ชฉ์ด ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์™ธ๋ถ€ ์œ ์ถœ์— ์ฃผ์˜ํ•˜์„ธ์š”.")
250
-
251
- tab1, tab2, tab3 = st.tabs(["๐Ÿ“‹ ํšŒ์˜ ์š”์•ฝ", "โœ… ํ•  ์ผ & ์šฐ์„ ์ˆœ์œ„", "๐Ÿ“„ ์›๋ฌธ ๋ณด๊ธฐ"])
252
-
253
- with tab1:
254
- st.markdown(f"### ๐Ÿ“Œ {res.get('meeting_title', 'ํšŒ์˜ ๊ฒฐ๊ณผ')}")
255
- st.info(f"**ํšŒ์˜ ๋ชฉ์ :** {res.get('purpose', '๋‚ด์šฉ ์—†์Œ')}")
256
 
257
- c1, c2 = st.columns(2)
258
- with c1:
259
- st.subheader("โœ… ๊ฒฐ์ • ์‚ฌํ•ญ")
260
- for d in res.get('decisions', []):
261
- st.write(f"- {d}")
262
- with c2:
263
- st.subheader("๐Ÿ“Œ ์ฃผ์š” ์•ˆ๊ฑด")
264
- for a in res.get('agenda_items', []):
265
- st.write(f"- {a}")
266
-
267
- st.divider()
268
- st.subheader("๐Ÿ’ฌ ์ „์ฒด ์š”์•ฝ")
269
- st.write(res.get('summary', '์š”์•ฝ ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค.'))
270
-
271
- with tab2:
272
- tasks = res.get('tasks', [])
273
- if not tasks:
274
- st.write("๋ฐฐ์ •๋œ ํ•  ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.")
275
  else:
276
- for t in tasks:
277
- p = t.get('priority', 'low')
278
- label = PRIORITY_LABEL.get(p, "โšช ์ •๋ณด์—†์Œ")
279
- deadline = f" (๊ธฐํ•œ: {t['deadline']})" if t.get('deadline') else ""
280
- st.markdown(f"**{label} [{t.get('person')}]** : {t.get('task')}{deadline}")
281
 
282
- with tab3:
283
- st.markdown("#### ๐Ÿ”‘ ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ")
284
- st.write(", ".join(res.get('keywords', [])))
285
- st.divider()
286
- st.text_area("์ „์‚ฌ ์›๋ฌธ", st.session_state.transcript, height=300)
287
- submit = st.button("์‹คํ–‰", use_container_width=True)
288
-
289
- # โ”€โ”€ ์‹คํ–‰ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ โ”€โ”€
290
- if submit:
291
- # ํšŒ์˜๋ก ํ…์ŠคํŠธ ํŒŒ์ผ ์ฝ๊ธฐ
292
- text = ""
293
- if uploaded_file is not None:
294
- text = uploaded_file.read().decode("utf-8")
295
-
296
- # ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ
297
- if audio_value is None and uploaded_audio_file is None and not text_input.strip():
298
- st.warning("์š”์ฒญ ๋ฐฉ์‹์„ ์„ ํƒํ•˜๊ณ  ๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
299
- st.stop()
300
-
301
- # ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ๊ตฌ์„ฑ
302
- system_content = sys_role
303
- if text:
304
- system_content += f"\n\n๋‹ค์Œ ํšŒ์˜ ๋‚ด์šฉ์„ ์ฐธ๊ณ ํ•˜์„ธ์š”:\n{text}"
305
- input_messages = [{"role": "system", "content": system_content}]
306
-
307
- # STT ๋˜๋Š” ํ…์ŠคํŠธ ์ฒ˜๋ฆฌ
308
- if audio_value is not None:
309
- transcript = client.audio.transcriptions.create(
310
- model="whisper-1",
311
- file=("audio.wav", audio_value, "audio/wav"),
312
- )
313
- user_text = transcript.text
314
-
315
- elif uploaded_audio_file is not None:
316
- transcript = client.audio.transcriptions.create(
317
- model="whisper-1",
318
- file=(uploaded_audio_file.name, uploaded_audio_file, uploaded_audio_file.type),
319
- )
320
- user_text = transcript.text
321
-
322
- else:
323
- user_text = text_input.strip()
324
-
325
- input_messages.append({"role": "user", "content": user_text})
326
- # with col2:
327
- # st.markdown('<div class="input-title">๐ŸŽ™ ํšŒ์˜ ๋‚ด์šฉ ์ž…๋ ฅ</div>', unsafe_allow_html=True)
328
- # with st.form("task_form"):
329
- # uploaded_file = st.file_uploader("ํ…์ŠคํŠธ ํŒŒ์ผ ์—…๋กœ๋“œ (.txt)", type=["txt"])
330
- # audio_value = st.audio_input("์Œ์„ฑ์œผ๋กœ ํšŒ์˜ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”")
331
- # submit = st.form_submit_button("๐Ÿ” ํšŒ์˜ ๋ถ„์„ ์‹œ์ž‘")
332
 
333
- # if submit:
334
- # final_text = ""
335
- # # 1. ํ…์ŠคํŠธ ํŒŒ์ผ ์ฒ˜๋ฆฌ
336
- # if uploaded_file:
337
- # final_text = uploaded_file.read().decode("utf-8")
338
-
339
- # # 2. ์Œ์„ฑ ํŒŒ์ผ STT ์ฒ˜๋ฆฌ
340
- # if audio_value:
341
- # with st.spinner("๐ŸŽ™๏ธ ์Œ์„ฑ์„ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ค‘..."):
342
- # transcript = client.audio.transcriptions.create(
343
- # model="whisper-1",
344
- # file=("audio.wav", audio_value, "audio/wav"),
345
- # language="ko"
346
- # ).text
347
- # final_text += "\n" + transcript
348
-
349
- # if not final_text.strip():
350
- # st.warning("โš ๏ธ ๋ถ„์„ํ•  ๋‚ด์šฉ(ํ…์ŠคํŠธ ๋˜๋Š” ์Œ์„ฑ)์ด ์—†์Šต๋‹ˆ๋‹ค.")
351
- # else:
352
- # with st.spinner("๐Ÿค– AI๊ฐ€ ํšŒ์˜๋ก์„ ๋ถ„์„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค..."):
353
- # try:
354
- # st.session_state.transcript = final_text
355
- # st.session_state.analysis_result = analyze_meeting(final_text)
356
- # except Exception as e:
357
- # st.error(f"๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
358
 
359
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
360
- # 5. ๊ฒฐ๊ณผ ์ถœ๋ ฅ ์˜์—ญ (Tabs ํ™œ์šฉ)
361
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
362
  if st.session_state.analysis_result:
363
  res = st.session_state.analysis_result
364
-
365
- # ๋ณด์•ˆ ๊ฒฝ๊ณ  ์ถœ๋ ฅ
366
  hits = detect_sensitive(st.session_state.transcript)
367
  if hits:
368
- st.error(f"๐Ÿ”’ ๋ฏผ๊ฐ์ •๋ณด ๊ฐ์ง€: {', '.join(hits)} ํ•ญ๋ชฉ์ด ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์™ธ๋ถ€ ์œ ์ถœ์— ์ฃผ์˜ํ•˜์„ธ์š”.")
369
 
370
  tab1, tab2, tab3 = st.tabs(["๐Ÿ“‹ ํšŒ์˜ ์š”์•ฝ", "โœ… ํ•  ์ผ & ์šฐ์„ ์ˆœ์œ„", "๐Ÿ“„ ์›๋ฌธ ๋ณด๊ธฐ"])
371
 
@@ -376,16 +176,14 @@ if st.session_state.analysis_result:
376
  c1, c2 = st.columns(2)
377
  with c1:
378
  st.subheader("โœ… ๊ฒฐ์ • ์‚ฌํ•ญ")
379
- for d in res.get('decisions', []):
380
- st.write(f"- {d}")
381
  with c2:
382
  st.subheader("๐Ÿ“Œ ์ฃผ์š” ์•ˆ๊ฑด")
383
- for a in res.get('agenda_items', []):
384
- st.write(f"- {a}")
385
 
386
  st.divider()
387
  st.subheader("๐Ÿ’ฌ ์ „์ฒด ์š”์•ฝ")
388
- st.write(res.get('summary', '์š”์•ฝ ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค.'))
389
 
390
  with tab2:
391
  tasks = res.get('tasks', [])
@@ -393,13 +191,13 @@ if st.session_state.analysis_result:
393
  st.write("๋ฐฐ์ •๋œ ํ•  ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.")
394
  else:
395
  for t in tasks:
396
- p = t.get('priority', 'low')
397
- label = PRIORITY_LABEL.get(p, "โšช ์ •๋ณด์—†์Œ")
398
- deadline = f" (๊ธฐํ•œ: {t['deadline']})" if t.get('deadline') else ""
399
- st.markdown(f"**{label} [{t.get('person')}]** : {t.get('task')}{deadline}")
400
 
401
  with tab3:
402
  st.markdown("#### ๐Ÿ”‘ ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ")
403
- st.write(", ".join(res.get('keywords', [])))
404
  st.divider()
405
  st.text_area("์ „์‚ฌ ์›๋ฌธ", st.session_state.transcript, height=300)
 
22
  img_path = os.path.join(current_dir, "kt.png")
23
  client = OpenAI()
24
 
25
+ # ์„ธ์…˜ ์ƒํƒœ ์ดˆ๊ธฐํ™”
 
 
26
  if "analysis_result" not in st.session_state:
27
  st.session_state.analysis_result = None
28
  if "transcript" not in st.session_state:
 
32
  PRIORITY_LABEL = {"high": "๐Ÿ”ด ๋†’์Œ", "medium": "๐ŸŸก ๋ณดํ†ต", "low": "๐ŸŸข ๋‚ฎ์Œ"}
33
 
34
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
35
+ # 2. ๋ฐฑ์—”๋“œ ํ•ต์‹ฌ ํ•จ์ˆ˜ (๋ฏผ๊ฐ์ •๋ณด ๊ฐ์ง€, ๋ถ„์„)
36
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
37
 
 
38
  def detect_sensitive(text):
39
  SENSITIVE_PATTERNS = [
40
  (r"๊ธฐ๋ฐ€|๋น„๋ฐ€|๋Œ€์™ธ๋น„|๋‚ด๋ถ€.*๋ณด์•ˆ|๋ณด์•ˆ.*์œ ์ง€|confidential", "๊ธฐ๋ฐ€/๋Œ€์™ธ๋น„ ํ‘œํ˜„"),
 
45
  ]
46
  return list({label for pat, label in SENSITIVE_PATTERNS if re.search(pat, text, re.IGNORECASE)})
47
 
 
48
  sys_role = """
49
  ## ์—ญํ• 
50
  ๋‹น์‹ ์€ KT Enterprise IT๊ธฐ์ˆ ํ˜์‹ ํŒ€ DX Consulting์˜ AI ํšŒ์˜ ๋น„์„œ์ž…๋‹ˆ๋‹ค.
 
55
  2. ๊ฒฐ์ •๋œ ์‚ฌํ•ญ โ€” ํ•ต์‹ฌ ๊ฒฐ์ •๋งŒ ๊ฐ„๊ฒฐํ•˜๊ฒŒ
56
  3. ๋‹ด๋‹น ์—…๋ฌด(Action Items) โ€” [๋‹ด๋‹น์ž]:[์—…๋ฌด]/๊ธฐํ•œ/์šฐ์„ ์ˆœ์œ„
57
  4. ์ผ์ • ์š”์•ฝ โ€” ๋งˆ๊ฐ ๊ธฐํ•œ ์‹œ๊ฐ„์ˆœ ์ •๋ฆฌ
 
 
 
 
58
  """
59
 
 
60
  def analyze_meeting(text):
61
  today = datetime.now().strftime("%Y๋…„ %m์›” %d์ผ")
62
  prompt = f"""
63
+ ๋‹ค์Œ์€ ์˜ค๋Š˜({today}) ์ง„ํ–‰๋œ ํšŒ์˜ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค. ์•„๋ž˜ JSON ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•˜์„ธ์š”. ๋ฐ˜๋“œ์‹œ JSON๋งŒ ์ถœ๋ ฅํ•˜์„ธ์š”.
 
64
 
65
  ํšŒ์˜ ๋‚ด์šฉ:
66
  {text}
67
 
68
  {{
69
  "meeting_title": "ํšŒ์˜ ์ œ๋ชฉ",
70
+ "purpose": "ํšŒ์˜ ๋ชฉ์ ",
71
+ "decisions": ["๊ฒฐ์ • ์‚ฌํ•ญ ๋ฆฌ์ŠคํŠธ"],
72
  "tasks": [
73
+ {{"person":"๋‹ด๋‹น์ž","task":"์—…๋ฌด ๋‚ด์šฉ","deadline":"๊ธฐํ•œ","priority":"high|medium|low"}}
74
  ],
75
+ "agenda_items": ["์•ˆ๊ฑด ๋ฆฌ์ŠคํŠธ"],
76
+ "summary": "์ „์ฒด ์š”์•ฝ",
77
+ "keywords": ["ํ‚ค์›Œ๋“œ1", "ํ‚ค์›Œ๋“œ2"]
 
78
  }}
79
  """
80
  resp = client.chat.completions.create(
81
  model="gpt-4o",
82
+ messages=[{"role": "system", "content": sys_role}, {"role": "user", "content": prompt}],
 
83
  temperature=0.2,
84
  )
85
  raw = re.sub(r"^```json\s*|```\s*$", "", resp.choices[0].message.content.strip())
86
  return json.loads(raw)
87
 
88
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
89
+ # 3. UI ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ (ํ—ค๋” ๋ฐ ์ž…๋ ฅ๋ถ€)
90
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
91
+
92
+ # ์ƒ๋‹จ ํ—ค๋” ์˜์—ญ (ํฌ๊ธฐ ํ™•๋Œ€ ๋ฒ„์ „)
93
  st.markdown("""
94
  <div style="display:flex;align-items:center;gap:20px;padding:25px 30px;
95
  background:linear-gradient(135deg,#16213E 0%,#0F3460 100%);
96
  border-bottom:4px solid #E3000B;border-radius:15px;margin-bottom:25px;">
 
97
  <div style="font-family:'Rajdhani',sans-serif;font-size:3.5rem;font-weight:800;
98
  color:#E3000B;letter-spacing:1px;line-height:1;">KT</div>
99
  <div class="title-container">
100
+ <div class="main-title" style="font-size:2.2rem; font-weight:700; color:#FFFFFF;">Meeting AI ํšŒ์˜ ๋ถ„์„ ์–ด์‹œ์Šคํ„ดํŠธ</div>
101
+ <div class="sub-title" style="font-size:1.0rem; color:#E0E0E0; margin-top:5px;">IT๊ธฐ์ˆ ํ˜์‹ ํŒ€ DX Consulting ยท Intelligent Meeting Solution</div>
 
 
 
 
 
 
102
  </div>
103
  </div>
104
  """, unsafe_allow_html=True)
105
 
106
  col1, col2 = st.columns([1, 2], gap="large")
107
 
 
108
  with col1:
109
  try:
110
  st.image(Image.open(img_path), caption="KT DX Assistant", use_container_width=True)
111
  except:
112
  st.info("์ด๋ฏธ์ง€(kt.png)๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
113
 
 
114
  with col2:
115
  st.subheader("ํšŒ์˜๋ก์„ ์—…๋กœ๋“œํ•˜์‹œ๋ฉด ์š”์•ฝํ•ด๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.")
116
+ with st.container(border=True):
117
+ uploaded_file = st.file_uploader("๐Ÿ“„ ํšŒ์˜ ๋‚ด์šฉ ํ…์ŠคํŠธ ํŒŒ์ผ ์—…๋กœ๋“œ", type=["txt"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ st.write("**์š”์ฒญ ๋ฐฉ์‹ ์„ ํƒ**")
120
+ input_mode = st.radio("์š”์ฒญ ๋ฐฉ์‹", ["๐ŸŽ™๏ธ ์‹ค์‹œ๊ฐ„ ๋…น์Œ", "๐Ÿ“ ์Œ์„ฑ ํŒŒ์ผ ์—…๋กœ๋“œ", "๐Ÿ“ ํ…์ŠคํŠธ ์ž…๋ ฅ"], horizontal=True, label_visibility="collapsed")
 
 
 
 
 
 
 
121
 
122
+ audio_value = None
123
+ uploaded_audio_file = None
124
+ text_input = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
+ if input_mode == "๐ŸŽ™๏ธ ์‹ค์‹œ๊ฐ„ ๋…น์Œ":
127
+ audio_value = st.audio_input("์Œ์„ฑ์œผ๋กœ ๋‚ด์šฉ์„ ๋งํ•ด์ฃผ์„ธ์š”.")
128
+ elif input_mode == "๐Ÿ“ ์Œ์„ฑ ํŒŒ์ผ ์—…๋กœ๋“œ":
129
+ uploaded_audio_file = st.file_uploader("์Œ์„ฑ ํŒŒ์ผ ์—…๋กœ๋“œ", type=["mp3", "wav", "m4a"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  else:
131
+ text_input = st.text_area("๋‚ด์šฉ์„ ์ง์ ‘ ์ž…๋ ฅํ•˜์„ธ์š”.", height=150)
 
 
 
 
132
 
133
+ submit = st.button("๐Ÿš€ ํšŒ์˜ ๋ถ„์„ ์‹œ์ž‘", use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
+ if submit:
136
+ meeting_text = ""
137
+ if uploaded_file:
138
+ meeting_text = uploaded_file.read().decode("utf-8")
139
+
140
+ with st.spinner("AI๊ฐ€ ๋‚ด์šฉ์„ ๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค..."):
141
+ try:
142
+ user_req = ""
143
+ if audio_value:
144
+ user_req = client.audio.transcriptions.create(model="whisper-1", file=("audio.wav", audio_value, "audio/wav")).text
145
+ elif uploaded_audio_file:
146
+ user_req = client.audio.transcriptions.create(model="whisper-1", file=(uploaded_audio_file.name, uploaded_audio_file, uploaded_audio_file.type)).text
147
+ else:
148
+ user_req = text_input
149
+
150
+ final_content = (meeting_text + "\n" + user_req).strip()
151
+
152
+ if not final_content:
153
+ st.warning("๋ถ„์„ํ•  ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค.")
154
+ else:
155
+ st.session_state.transcript = final_content
156
+ st.session_state.analysis_result = analyze_meeting(final_content)
157
+ st.rerun()
158
+ except Exception as e:
159
+ st.error(f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
160
 
161
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
162
+ # 4. ๊ฒฐ๊ณผ ์ถœ๋ ฅ ์˜์—ญ (Tabs ํ™œ์šฉ)
163
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
164
  if st.session_state.analysis_result:
165
  res = st.session_state.analysis_result
 
 
166
  hits = detect_sensitive(st.session_state.transcript)
167
  if hits:
168
+ st.error(f"๐Ÿ”’ ๋ฏผ๊ฐ์ •๋ณด ๊ฐ์ง€: {', '.join(hits)} ํ•ญ๋ชฉ ์ฃผ์˜")
169
 
170
  tab1, tab2, tab3 = st.tabs(["๐Ÿ“‹ ํšŒ์˜ ์š”์•ฝ", "โœ… ํ•  ์ผ & ์šฐ์„ ์ˆœ์œ„", "๐Ÿ“„ ์›๋ฌธ ๋ณด๊ธฐ"])
171
 
 
176
  c1, c2 = st.columns(2)
177
  with c1:
178
  st.subheader("โœ… ๊ฒฐ์ • ์‚ฌํ•ญ")
179
+ for d in res.get('decisions', []): st.write(f"- {d}")
 
180
  with c2:
181
  st.subheader("๐Ÿ“Œ ์ฃผ์š” ์•ˆ๊ฑด")
182
+ for a in res.get('agenda_items', []): st.write(f"- {a}")
 
183
 
184
  st.divider()
185
  st.subheader("๐Ÿ’ฌ ์ „์ฒด ์š”์•ฝ")
186
+ st.write(res.get('summary', '์š”์•ฝ ๋‚ด์šฉ ์—†์Œ'))
187
 
188
  with tab2:
189
  tasks = res.get('tasks', [])
 
191
  st.write("๋ฐฐ์ •๋œ ํ•  ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.")
192
  else:
193
  for t in tasks:
194
+ p = t.get('priority', 'low').lower()
195
+ label = PRIORITY_LABEL.get(p, "๐ŸŸข ๋‚ฎ์Œ")
196
+ deadline = f" (๊ธฐํ•œ: {t['deadline']})" if t.get('deadline') and t['deadline'] != "null" else ""
197
+ st.markdown(f"**{label} [{t.get('person', '๋ฏธ์ง€์ •')}]** : {t.get('task')}{deadline}")
198
 
199
  with tab3:
200
  st.markdown("#### ๐Ÿ”‘ ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ")
201
+ st.write(", ".join([f"#{k}" for k in res.get('keywords', [])]))
202
  st.divider()
203
  st.text_area("์ „์‚ฌ ์›๋ฌธ", st.session_state.transcript, height=300)