ArunK-2003/KapNotes
0
1import os2import json3import re4import html5import streamlit as st6import plotly.graph_objects as go7from google.cloud import storage8from google.oauth2 import service_account9from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer10from scipy.ndimage import gaussian_filter1d11from datetime import timedelta, datetime12 13gcp_credentials = os.getenv('GCP_CREDENTIALS')14credentials_dict = json.loads(gcp_credentials)15creds = service_account.Credentials.from_service_account_info(credentials_dict)16client = storage.Client(credentials=creds)17bucket_name = "kapnotes"18bucket = client.bucket(bucket_name)19st.set_page_config(page_title="Kap Notes", layout="wide")20 21def get_client_names():22 blobs = list(bucket.list_blobs(prefix=""))23 client_names = set()24 for blob in blobs:25 client_name = blob.name.split("/")[0]26 client_names.add(client_name)27 return sorted(client_names)28 29def validate_data(client_name, date, meeting):30 summary_blob_name = f"{client_name}/{date}/{meeting}/summary.txt"31 transcription_blob_name = f"{client_name}/{date}/{meeting}/transcription.txt"32 audio_blob_name = f"{client_name}/{date}/{meeting}/audio.wav"33 summary_blob = bucket.blob(summary_blob_name)34 transcription_blob = bucket.blob(transcription_blob_name)35 audio_blob = bucket.blob(audio_blob_name)36 return summary_blob.exists() and transcription_blob.exists() and audio_blob.exists()37 38def get_meetings_for_date(client_name, date):39 prefix = f"{client_name}/{date}/"40 blobs = list(bucket.list_blobs(prefix=prefix))41 meetings = set()42 for blob in blobs:43 parts = blob.name.split("/")44 if len(parts) > 2 and parts[2]:45 meetings.add(parts[2])46 return sorted(meetings)47 48def get_dates_for_client(client_name):49 prefix = f"{client_name}/"50 blobs = list(bucket.list_blobs(prefix=prefix))51 dates = set()52 for blob in blobs:53 parts = blob.name.split("/")54 if len(parts) > 1 and parts[1]:55 dates.add(parts[1])56 return sorted(dates)57 58def login():59 st.markdown("""60 <style>61 .stApp {62 background: linear-gradient(125deg,rgb(253, 250, 220) 0%,rgb(214, 245, 255) 50%, #F8F8FF 100%);63 background-size: 200% 200%;64 animation: gradientMove 10s ease infinite;65 }66 @keyframes gradientMove {67 0% { background-position: 0% 50%; }68 50% { background-position: 100% 50%; }69 100% { background-position: 0% 50%; }70 }71 h1 {72 color: #4A4A4A;73 font-size: 3.5rem;74 font-weight: 900;75 text-align: center;76 margin-bottom: 3rem;77 letter-spacing: 2px;78 }79 .stButton > button {80 width: 100%;81 background: linear-gradient(45deg, #2563eb 0%, #3b82f6 100%);82 color: white;83 border: none;84 border-radius: 16px;85 padding: 1rem 1.5rem;86 font-size: 1.3rem;87 font-weight: bold;88 cursor: pointer;89 margin-top: 2rem;90 box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4),91 inset 0 -4px 8px rgba(0, 0, 0, 0.2),92 inset 0 4px 8px rgba(255, 255, 255, 0.2);93 transition: all 0.3s ease;94 }95 .stButton > button:hover {96 transform: translateY(-3px) scale(1.03);97 box-shadow: 0 12px 24px rgba(0, 0, 0, 0.5),98 inset 0 -4px 8px rgba(0, 0, 0, 0.2),99 inset 0 4px 8px rgba(255, 255, 255, 0.2);100 background: linear-gradient(45deg, #1d4ed8 0%, #2563eb 100%);101 }102 </style>103 """, unsafe_allow_html=True)104 105 if 'password' not in st.session_state:106 st.session_state.password = ""107 108 st.markdown("<h1>KAP NOTES</h1>", unsafe_allow_html=True)109 110 client_names = get_client_names()111 client_name = st.selectbox("Select Client", client_names)112 113 if client_name:114 available_dates = get_dates_for_client(client_name)115 selected_date = st.selectbox(f"Available Dates for {client_name}", available_dates)116 117 if selected_date:118 available_meetings = get_meetings_for_date(client_name, selected_date)119 selected_meeting = st.selectbox(f"Available Meetings for {selected_date}", available_meetings)120 password = st.text_input("Enter Password", type="password", value=st.session_state.password)121 sign_in_button = st.button("Sign In", key="sign_in")122 if sign_in_button:123 if password == "kapnotes12345":124 if validate_data(client_name, selected_date, selected_meeting):125 st.session_state.client_name = client_name126 st.session_state.date = selected_date127 st.session_state.meeting = selected_meeting128 st.session_state.logged_in = True129 st.session_state.password = password130 st.rerun()131 else:132 st.error(f"No records available for {client_name} on {selected_date}. Please select another option.")133 elif not password:134 st.error("Please enter password.")135 else: 136 st.error("Incorrect Password. Please try again.")137 st.session_state.password = password138 139 140if 'logged_in' not in st.session_state:141 st.session_state.logged_in = False142 143if not st.session_state.logged_in:144 login()145else:146 client_name = st.session_state.client_name147 date = st.session_state.date148 meeting = st.session_state.meeting149 password= st.session_state.password150 151 if st.sidebar.button("Back"):152 st.session_state.logged_in = False153 st.rerun() 154 155 st.sidebar.markdown(f'''156 <div class="client-name-container">157 <div class="client-name">{client_name}</div>158 </div>159 ''', unsafe_allow_html=True)160 161 css = '''162 <style>163 [data-testid="stExpander"] div:has(>.streamlit-expanderContent) {164 max-height: 400px;165 overflow-y: scroll;166 }167 [data-testid="stSidebar"] {168 min-width: 400px; 169 }170 [data-testid="stSidebar"] > div:first-child {171 padding-top: 10px;172 }173 .main {174 margin-top: 0 !important;175 }176 .summary-box, .keypoints-box, .action-items-box {177 padding: 20px;178 border-radius: 15px;179 box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2), 0px 6px 15px rgba(0, 0, 0, 0.15);180 margin-top: 20px;181 margin-bottom: 30px;182 line-height: 1.8;183 font-size: 16px;184 color: #333;185 transition: transform 0.3s ease, box-shadow 0.3s ease;186 }187 .summary-box:hover, .keypoints-box:hover, .action-items-box:hover {188 transform: translateY(-0.1px) scale(1.05);189 box-shadow: 0px 12px 25px rgba(0, 0, 0, 0.25), 0px 18px 35px rgba(0, 0, 0, 0.2);190 }191 .summary-box {192 background: linear-gradient(145deg, #F1EAFF, #E5D4FF);193 }194 .keypoints-box {195 background: linear-gradient(145deg, #F1EAFF, #E5D4FF);196 }197 .action-items-box {198 background: linear-gradient(145deg, #F1EAFF, #E5D4FF);199 }200 .summary-box, .keypoints-box, .action-items-box {201 border: 1px solid rgba(0, 0, 0, 0.1);202 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2), 0 6px 18px rgba(0, 0, 0, 0.15);203 }204 .summary-box {205 background-color: #FFF8DC;206 }207 button:hover {208 background-color: white;209 color: black;210 border-color: black;211 }212 br {213 margin-top: 8px;214 }215 .audio-player-container {216 background: linear-gradient(135deg, #FF91A4, #FF4E00);217 padding: 25px;218 border-radius: 20px;219 box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.2), 0px 6px 25px rgba(0, 0, 0, 0.15);220 margin-top: 30px;221 margin-bottom: 40px;222 text-align: center;223 font-size: 18px;224 transition: all 0.7s ease-in-out, transform 0.3s ease;225 }226 .audio-player-container:hover {227 transform: translateY(-10px) scale(1.03);228 box-shadow: 0px 8px 25px rgba(0, 0, 0, 0.3), 0px 12px 35px rgba(0, 0, 0, 0.25);229 }230 .audio-player {231 width: 80%;232 height: 50px;233 border-radius: 15px;234 background-color: #FFF8DC;235 border: 1px solid rgba(0, 0, 0, 0.15);236 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2), 0 6px 18px rgba(0, 0, 0, 0.15);237 transition: all 0.3s ease, transform 0.3s ease;238 }239 .audio-player:hover {240 background-color: #FF7F50;241 transform: scale(1.1);242 }243 .audio-player-container h4 {244 color: #333;245 font-weight: 700;246 margin-bottom: 15px;247 font-size: 22px;248 }249 .audio-player-container .play-button {250 background-color: #FF4E00;251 border: none;252 padding: 10px 20px;253 color: white;254 font-weight: 600;255 border-radius: 30px;256 cursor: pointer;257 transition: all 0.5s ease;258 }259 .audio-player-container .play-button:hover {260 background-color: #FF91A4;261 box-shadow: 0px 4px 20px rgba(255, 145, 164, 0.5);262 }263 .audio-player-container .play-button:focus {264 outline: none;265 }266 .comment-box {267 background: linear-gradient(145deg, #f4f9fb, #dce5f5);268 border: 1px solid #cfd9e6;269 border-radius: 10px;270 padding: 15px;271 margin-bottom: 20px;272 box-shadow: 2px 2px 12px rgba(0, 0, 0, 0.1), -2px -2px 12px rgba(255, 255, 255, 0.8);273 font-family: 'Arial', sans-serif;274 transition: transform 0.3s ease, box-shadow 0.3s ease;275 }276 .comment-box:hover {277 transform: scale(1.02) translateY(-2px);278 box-shadow: 4px 4px 20px rgba(0, 0, 0, 0.2), -4px -4px 20px rgba(255, 255, 255, 0.4);279 }280 .comment-header {281 display: flex;282 justify-content: space-between;283 margin-bottom: 10px;284 font-weight: bold;285 color: #3e3e3e;286 }287 .comment-text {288 color: #555;289 font-size: 14px;290 line-height: 1.6;291 }292 .comment-header span {293 color: #007bff;294 font-size: 12px;295 }296 .form-container {297 margin-bottom: 30px;298 }299 .stTextInput, .stTextArea {300 border-radius: 5px;301 border: 1px solid #ccc;302 padding: 10px;303 font-size: 14px;304 margin-bottom: 15px;305 width: 100%;306 }307 .stFormSubmitButton {308 color: black;309 padding: 10px 20px;310 border-radius: 5px;311 font-size: 14px;312 cursor: pointer;313 }314 315 .client-name-container {316 padding: 20px;317 border-radius: 10px;318 background: linear-gradient(145deg, #ff7e5f, #feb47b);319 box-shadow: 5px 5px 15px rgba(0, 0, 0, 0.1), -5px -5px 15px rgba(255, 255, 255, 0.3);320 transition: transform 0.3s ease, box-shadow 0.3s ease;321 }322 323 .client-name-container:hover {324 transform: translateY(-5px) scale(1.05);325 box-shadow: 8px 8px 20px rgba(0, 0, 0, 0.15), -8px -8px 20px rgba(255, 255, 255, 0.4);326 }327 328 .client-name {329 font-size: 2.5rem;330 font-weight: 700;331 color: #fff;332 text-align: center;333 margin: 0;334 }335 336 .form-container {337 display: flex;338 flex-direction: column;339 align-items: center;340 }341 342 .stButton>button {343 background-color: black;344 color: white;345 width: 150px;346 height: 40px;347 border: none;348 cursor: pointer;349 font-weight: bold;350 transition: all 0.3s;351 }352 .stButton>button:hover {353 background-color: white;354 color: black;355 }356 </style>357 '''358 st.markdown(css, unsafe_allow_html=True)359 360 summary_blob_name = f"{client_name}/{date}/{meeting}/summary.txt"361 transcription_blob_name = f"{client_name}/{date}/{meeting}/transcription.txt" 362 audio_blob_name = f"{client_name}/{date}/{meeting}/audio.wav"363 364 bucket = client.bucket(bucket_name)365 366 summary_blob = bucket.blob(summary_blob_name)367 summary_content = summary_blob.download_as_text()368 369 audio_blob = bucket.blob(audio_blob_name)370 audio_url = audio_blob.generate_signed_url(expiration=timedelta(hours=1), method='GET')371 372 summary_match = re.search(r"Summary:\s*(.*?)(?=\nKey Points:)", summary_content, re.DOTALL)373 summary = summary_match.group(1).strip() if summary_match else "Summary not found."374 375 key_points_match = re.search(r"Key Points:\s*(.*?)(?=\nAction Items:)", summary_content, re.DOTALL)376 key_points = re.findall(r"- (.*?)\n", key_points_match.group(1)) if key_points_match else ["Key points not found."]377 378 action_items_match = re.search(r"Action Items:\s*(.*)", summary_content, re.DOTALL)379 if action_items_match:380 action_items = re.findall(r"- (.*?)(?=\n- |$)", action_items_match.group(1), re.DOTALL)381 else:382 action_items = ["Action items not found."]383 384 transcription_blob = bucket.blob(transcription_blob_name)385 with transcription_blob.open("r") as file:386 meeting_data = json.load(file)387 388 speaker_data = {}389 total_talktime = 0390 391 for entry in meeting_data:392 speaker = entry["speaker"]393 duration = entry["end"] - entry["start"]394 text = entry["text"]395 total_talktime += duration396 397 if speaker not in speaker_data:398 speaker_data[speaker] = {"talktime": 0, "text": "", "words": 0}399 400 speaker_data[speaker]["talktime"] += duration401 speaker_data[speaker]["text"] += " " + text402 speaker_data[speaker]["words"] += len(text.split())403 404 for speaker, data in speaker_data.items():405 data["word_per_minute"] = round((data["words"] / data["talktime"] * 60), 2)406 data["talktime_percentage"] = round((data["talktime"] / total_talktime * 100), 2)407 408 combined_text = " ".join(data["text"] for data in speaker_data.values())409 410 analyzer = SentimentIntensityAnalyzer()411 sentences = combined_text.split('.')412 sentiment_polarity = [analyzer.polarity_scores(sentence)["compound"] for sentence in sentences if sentence.strip()]413 smoothed_polarity = gaussian_filter1d(sentiment_polarity, sigma=2)414 415 st.title("Kap Notes - Unveiling the story behind your meeting")416 417 st.markdown(f"### Summary\n<div class='summary-box'>{summary}</div>", unsafe_allow_html=True)418 419 st.markdown("### Meeting Highlights")420 st.markdown(421 f"<div class='keypoints-box'>" + "<br>".join(f"• {point}" for point in key_points) + "</div>",422 unsafe_allow_html=True423 )424 425 st.markdown("### Actionable Items")426 st.markdown(427 f"<div class='action-items-box'>" + "<br>".join(f"• {item}" for item in action_items) + "</div>",428 unsafe_allow_html=True429 )430 431 st.markdown("### Comments")432 433 if 'comments' not in st.session_state:434 st.session_state.comments = []435 436 def add_comment(comment):437 st.session_state.comments.append({"name": "", "comment": comment, "date": datetime.now().strftime("%d, %b %Y")})438 439 if 'name' not in st.session_state:440 st.session_state.name = ""441 if 'comment' not in st.session_state:442 st.session_state.comment = ""443 444 for comment in st.session_state.comments:445 st.markdown(446 f"""447 <div class="comment-box">448 <div class="comment-header">449 <span>Admin</span>450 <span>{comment['date']}</span>451 </div>452 <div class="comment-text">453 {comment['comment']}454 </div>455 </div>456 """, unsafe_allow_html=True)457 458 with st.form(key="comment_form"):459 st.markdown('<div class="form-container">', unsafe_allow_html=True)460 comment_input = st.text_area("Your Comment", height=100, value=st.session_state.comment)461 submit_button = st.form_submit_button("Submit")462 st.markdown('</div>', unsafe_allow_html=True)463 464 if submit_button:465 if not comment_input:466 st.error("Enter your comment") 467 else:468 add_comment(comment_input)469 st.session_state.comment = "" 470 st.rerun() 471 472 with st.sidebar:473 474 speaker_names = list(speaker_data.keys())475 talk_time_percentages = [data["talktime_percentage"] for data in speaker_data.values()]476 477 color_palette = ["#A3BFF1", "#F4A7B9", "#C4F1D2", "#D6A7F2", "#FFD5A6", "#9BE1E6", "#F4A3C0", "#C1E7B4", "#F1D0FF", "#F9E9A6"]478 speaker_colors = {speaker: color_palette[i % len(color_palette)] for i, speaker in enumerate(speaker_names)} 479 480 st.markdown(f"""481 <div class="audio-player-container">482 <h4>Listen to the Meeting Audio</h4>483 <audio class="audio-player" controls>484 <source src="{audio_url}" type="audio/wav">485 Your browser does not support the audio element.486 </audio>487 </div>488 """, unsafe_allow_html=True)489 490 st.title("Chat Conversation")491 492 with st.expander("Click to view the chat conversation", expanded=False):493 chat_conversation = ""494 for index, entry in enumerate(meeting_data):495 speaker = entry["speaker"]496 text = entry["text"]497 talk_time = entry["end"] - entry["start"]498 speaker_color = speaker_colors[speaker]499 chat_conversation += f"""500 <div style="margin-bottom: 20px; background-color: {speaker_color}; padding: 15px; 501 border-radius: 10px; box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);">502 <div style="display: flex; justify-content: space-between; align-items: center;">503 <b style="color: black;">{speaker}</b>504 <span style="color: black;">{talk_time:.2f} mins</span>505 </div>506 <div style="margin-top: 10px; text-align: justify; line-height: 1.6; color: black;">507 {text}508 </div>509 </div>510 """511 st.markdown(chat_conversation, unsafe_allow_html=True)512 513 fig = go.Figure(data=[go.Pie(labels=speaker_names, values=talk_time_percentages, marker=dict(colors=list(speaker_colors.values())), hole=0.3)])514 fig.update_layout(515 title="Speaker Analytics",516 showlegend=True,517 legend=dict(518 orientation="h",519 yanchor="top",520 y=-0.2,521 xanchor="center",522 x=0.5523 )524 )525 st.plotly_chart(fig)526 527 st.markdown("### Sentiment Analysis of the Meeting")528 529 fig = go.Figure()530 fig.add_trace(go.Scatter(x=list(range(len(smoothed_polarity))), y=smoothed_polarity, mode='lines', name='Sentiment', line=dict(color='blue')))531 fig.update_layout(532 xaxis=dict(title="Time (in seconds)"),533 yaxis=dict(title="Sentiment Score", range=[-1, 1]),534 )535 st.plotly_chart(fig)