From 9c31b5cc6c10a7df17c0dbc7bae05ff4bb35d87a Mon Sep 17 00:00:00 2001 From: Chukwuma Nwaugha Date: Wed, 6 Nov 2024 16:39:03 +0000 Subject: [PATCH] Render waveform in a different context (#10) * add firestore_sdk ad session_manager * save user chats on firestore * pass down session_id for a deterministic workflow * handle conversion of chat object to/fro a dict * remove references to langchain * reuse a previously downloaded audiofile if it's processable * render audiocast metdata on share page * cleanup * temp remove audio_enchancement * sanitize audiocast transcript * add elevenlabs client * add __text_to_speech_elevenlabs; cleanup * use dry in text_to_speech * only lint on python versions 3.11 and 3.12 * add write permission to deploy job for marocchino/sticky-pull-request-comment * use eleven_multilingual_v2 model for improved stability, accuracy and quality * Refactor audiocast page to include waveform visualization * put waveform viz in an expander * cleanup * move download_waveform_video internal to render_waveform * allow toggling waveform visualizer * save waveform to gcs * reshuffle dependencies in requirements.txt * add pycairo to deps * add a server app * add header and process_time middlewares * make utils a shared folder * use root level services in app dir * write an improved init_project workflow * move streamlit related packages into app folder * convert app.src.utils into a module * install setup tools and create a setup file for utils_pkg * remove streamlit content from chat_utils * cleanup remnants * more cleanup * update package imports and initialization logic for utils_pkg * hit the backend in for chat workflow in generate_stream_response * add generate_audiocast endpoint handler * create a handler for get_audiocast by session_id * abstract waveform_utils; save wave_form on_generate audiocast * hit the server for generate_audiocast and get_audiocast * abstract audiocast inteface definitions * move audiocast_request to server * remove unused artifacts * save and update metadata.info * Remove audiocast page and add docstrings to session manager methods * revise github actions workflow files * add deploy_server.yml * rename deploy server workflow to reflect server deployment * upgrade to the latest version of pip * cleanup * remove the underscore in the service name * remove unused env variables * cleanup * add dockerfile for deploying the server * populate a dockerignore file on the fly * remove unused context from gcloudignore * localize the .*ignore files * move the dockerfile to root project pre-deploy step * specify corret path to streamlit index * Add debug output for Docker and gcloud ignore files; set no_traffic to false for deployment * add a gunicorn config file * rename server to api * rename all footprints of server to api * refactor: update workflow names and paths for cloudrun deployment * bump version to 1.0.1 and update project description * move chat_request to api utils * move api only utils into api folder * rename utils_pkg to shared_utils_pkg * add pre-commit config * refactor: clean up sidebar and improve metadata subscription handling * feat: add Streamlit configuration for browser and client settings * log the project_id in init_admin_sdk * remove reference_code from .gitignore * allow other ui elements to render before loading waveform video * remove subscription to metadata.info --- app/pages/audiocast.py | 3 +- app/src/utils/metadata_subscription.py | 43 ++++++++++++++----------- app/src/utils/render_audiocast.py | 6 ++-- app/src/utils/render_audiocast_utils.py | 26 ++++++++------- app/src/utils/render_waveform.py | 22 +++++++++---- app/uis/audioui.py | 2 +- 6 files changed, 60 insertions(+), 42 deletions(-) diff --git a/app/pages/audiocast.py b/app/pages/audiocast.py index e12d1b4..47ec1e9 100644 --- a/app/pages/audiocast.py +++ b/app/pages/audiocast.py @@ -32,7 +32,7 @@ async def render_audiocast_page(): if audiocast["created_at"]: st.markdown(f"> Created: {audiocast["created_at"]}") - share_url = render_audiocast_handler(session_id, audiocast) + share_url, finalize_task = render_audiocast_handler(session_id, audiocast) share_col, restart_row = st.columns(2, vertical_alignment="center") with share_col: @@ -42,6 +42,7 @@ async def render_audiocast_page(): if st.button("Create your Audiocast", use_container_width=True): navigate_to_home() + await finalize_task except Exception as e: st.error(f"Error loading audiocast: {str(e)}") else: diff --git a/app/src/utils/metadata_subscription.py b/app/src/utils/metadata_subscription.py index 333c54a..ea5a4d2 100644 --- a/app/src/utils/metadata_subscription.py +++ b/app/src/utils/metadata_subscription.py @@ -1,29 +1,34 @@ -from queue import Queue +import asyncio import streamlit as st from shared_utils_pkg.session_manager import SessionManager -def subscribe_to_audio_generation(session_id: str): - """Subscribe to audio generation metadata""" - q = Queue() +class SubscribeToAudioGeneration: + def __init__(self, session_id: str): + self.session_id = session_id + self.info: str | None = None - def handler(info: str | None): - if info: - q.put(info, block=False) + def create(self): + """Subscribe to audio generation metadata""" - db = SessionManager(session_id) - doc_watch = db.subscribe_to_metadata_info(handler) + def __handler(info: str): + self.info = info - with st.empty(): - while True: - try: - info = q.get(timeout=2) - if not info: - break - st.info(info) - except Exception: - break + db = SessionManager(self.session_id) + return db.subscribe_to_metadata_info(__handler) - return doc_watch + async def _update_ui(self, read_timeout=10): + """Update the UI with the current info attribute value.""" + timeout = 0 + container = st.empty() + + with container: + while read_timeout > timeout: + if self.info: + container.info(self.info) + await asyncio.sleep(1) + timeout += 1 + + container.empty() diff --git a/app/src/utils/render_audiocast.py b/app/src/utils/render_audiocast.py index 8696976..1b84e6d 100644 --- a/app/src/utils/render_audiocast.py +++ b/app/src/utils/render_audiocast.py @@ -1,5 +1,4 @@ import streamlit as st - from src.utils.custom_components import copy_button from src.utils.render_audiocast_utils import ( GenerateAudiocastDict, @@ -8,7 +7,7 @@ from src.utils.session_state import reset_session -def render_audiocast(session_id: str): +async def render_audiocast(session_id: str): """ Render the audiocast based on the user's preferences - Display current audiocast if available @@ -16,7 +15,7 @@ def render_audiocast(session_id: str): st.markdown("#### Your Audiocast") current_audiocast: GenerateAudiocastDict = st.session_state.current_audiocast - share_url = render_audiocast_handler(session_id, current_audiocast) + share_url, finalize_task = render_audiocast_handler(session_id, current_audiocast) share_col, restart_row = st.columns(2, vertical_alignment="center") @@ -28,3 +27,4 @@ def render_audiocast(session_id: str): if st.button("Generate New Audiocast", use_container_width=True): reset_session() st.rerun() + await finalize_task diff --git a/app/src/utils/render_audiocast_utils.py b/app/src/utils/render_audiocast_utils.py index 04b4f99..535b31d 100644 --- a/app/src/utils/render_audiocast_utils.py +++ b/app/src/utils/render_audiocast_utils.py @@ -1,11 +1,11 @@ +import asyncio import re from pathlib import Path from typing import cast import httpx import streamlit as st -from src.utils.metadata_subscription import subscribe_to_audio_generation -from src.utils.render_waveform import render_waveform +from src.utils.render_waveform import load_waveform_video, render_waveform from env_var import API_URL, APP_URL from shared_utils_pkg.audiocast_utils import GenerateAudioCastRequest, GenerateAudiocastDict @@ -33,8 +33,6 @@ async def generate_audiocast( summary: str, content_category: ContentCategory, ): - doc_watch = subscribe_to_audio_generation(session_id) - audiocast_req = GenerateAudioCastRequest( sessionId=session_id, summary=summary, @@ -46,8 +44,6 @@ async def generate_audiocast( timeout=None, ) response.raise_for_status() - doc_watch.unsubscribe() - return cast(GenerateAudiocastDict, response.json()) @@ -57,10 +53,11 @@ def render_audiocast_handler(session_id: str, audiocast: GenerateAudiocastDict): # Voice waveform with st.expander("Show Audio Waveform"): - try: - render_waveform(session_id, audiocast["url"], False) - except Exception as e: - st.error(f"Error rendering waveform: {str(e)}") + waveform_video_path: Path | str = st.session_state.get("waveform_video_path", False) + if waveform_video_path: + render_waveform(waveform_video_path) + else: + st.info("Loading waveform video...") # Transcript with st.expander("Show Transcript"): @@ -75,4 +72,11 @@ def render_audiocast_handler(session_id: str, audiocast: GenerateAudiocastDict): share_url = f"{APP_URL}/audiocast?session_id={session_id}" st.text_input("Share this audiocast:", share_url) - return share_url + async def finalize(): + try: + load_waveform_video(session_id, audiocast["url"]) + except Exception as e: + st.error(f"Error rendering waveform: {str(e)}") + + finalize_task = asyncio.create_task(finalize()) + return share_url, finalize_task diff --git a/app/src/utils/render_waveform.py b/app/src/utils/render_waveform.py index 0e23f79..5608cbb 100644 --- a/app/src/utils/render_waveform.py +++ b/app/src/utils/render_waveform.py @@ -1,4 +1,5 @@ import os +from pathlib import Path import streamlit as st from pydub import AudioSegment @@ -7,8 +8,8 @@ from shared_utils_pkg.waveform_utils import WaveformUtils -def render_waveform(session_id: str, audio_path: str, autoplay=False): - """Render waveform visualization from audio file.""" +def load_waveform_video(session_id: str, audio_path: str): + """Load waveform visualization from audio file.""" waveform_utils = WaveformUtils(session_id, audio_path) tmp_vid_path = waveform_utils.get_tmp_video_path() @@ -32,15 +33,22 @@ def render_waveform(session_id: str, audio_path: str, autoplay=False): video_path = waveform_utils.generate_waveform_video(tmp_vid_path) waveform_utils.save_waveform_video_to_gcs(str(video_path)) - with open(video_path, "rb") as video_file: - video_bytes = video_file.read() - st.video(video_bytes, autoplay=autoplay) - - download_waveform_video(str(video_path)) + if not st.session_state.get("waveform_video_path"): + st.session_state.waveform_video_path = video_path + st.rerun() except Exception as e: st.error(f"Error generating visualization: {str(e)}") +def render_waveform(video_path: Path | str, autoplay=False): + """Render waveform visualization from path""" + with open(video_path, "rb") as video_file: + video_bytes = video_file.read() + st.video(video_bytes, autoplay=autoplay) + + download_waveform_video(str(video_path)) + + def download_waveform_video(video_path: str): """Download video with waveform""" gen_video, _ = st.columns(2) diff --git a/app/uis/audioui.py b/app/uis/audioui.py index 254566e..fa0d580 100644 --- a/app/uis/audioui.py +++ b/app/uis/audioui.py @@ -19,4 +19,4 @@ async def audioui(session_id: str, uichat: DeltaGenerator): await use_audiocast_request(session_id, summary, content_category) else: st.info("Audiocast generation completed!") - render_audiocast(session_id) + await render_audiocast(session_id)