diff --git a/.gitignore b/.gitignore index 0f8643d..515a067 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # Custom ignore local/ data/ +ssl/server.crt +ssl/server.key ### Python template # Byte-compiled / optimized / DLL files diff --git a/Dockerfile b/Dockerfile index 187535e..4b8fb17 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,11 +13,11 @@ COPY app /app COPY ./requirements.txt /requirements.txt # Pip install the dependencies -RUN pip install --upgrade pip +RUN pip --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --trusted-host pypi.org install --upgrade pip # For CPU only, you can use pip install git+https://github.com/MiscellaneousStuff/whisper.git # in place of openai-whisper. # Also, --extra-index-url https://download.pytorch.org/whl/cpu might be needed if you are using a CPU only machine -RUN pip install --no-cache-dir -r /requirements.txt +RUN pip install --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --trusted-host pypi.org --no-cache-dir -r /requirements.txt # Set the working directory to /app WORKDIR /app @@ -29,4 +29,8 @@ EXPOSE 8501 VOLUME /data # Run the app -CMD streamlit run /app/01_🏠_Home.py +#CMD streamlit run /app/01_🏠_Home.py + +# Run the app on https +COPY ssl /app/ssl +CMD streamlit run /app/01_🏠_Home.py --server.sslCertFile '/app/ssl/server.crt' --server.sslKeyFile '/app/ssl/server.key' diff --git a/README.md b/README.md index 6ac9b9c..ba4164e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Streamlit UI for OpenAI's Whisper +# Streamlit UI for OpenAI's Whisper (mic support) This is a simple [Streamlit UI](https://streamlit.io/) for [OpenAI's Whisper speech-to-text model](https://openai.com/blog/whisper/). It let's you download and transcribe media from YouTube videos, playlists, or local files. @@ -25,12 +25,23 @@ conda env create -f environment.yml Note: If you're using a CPU-only machine, your runtime can be sped-up by using quantization implemented by [@MicellaneousStuff](https://github.com/MiscellaneousStuff) by swapping out `pip install openai-whisper` from `requirements.txt` and replacing it with their fork `pip install git+https://github.com/MiscellaneousStuff/whisper.git` (See related discussion here - https://github.com/hayabhay/whisper-ui/issues/20) +## Setup SSL termination configuration + +This `mic_support` branch supports "mic input". Due to browser media access issue, you should use SSL termination configuration in a reverse proxy or load balancer, or you can use streamlit [HTTPS support](https://docs.streamlit.io/library/advanced-features/https-support). streamlit 1.20.0 or later supports HTTPS. "mic input" data will be treated as "uploaded". + +As the easiest way, you can create a self-signed-cert as follows: + +``` +cd ssl +create-self-signed-cert.sh +``` + ## Usage Once you're set up, you can run the app with: ``` -streamlit run app/01_🏠_Home.py +streamlit run app/01_🏠_Home.py --server.sslCertFile 'ssl/server.crt' --server.sslKeyFile 'ssl/server.key' ``` This will open a new tab in your browser with the app. You can then select a YouTube URL or local file & click "Run Whisper" to run the model on the selected media. @@ -43,7 +54,7 @@ Alternatively, you can run the app containerized with Docker via the included do docker compose up ``` -Then open up a new tab and navigate to [http://localhost:8501/](http://localhost:8501/) +Then open up a new tab and navigate to [https://localhost:8501/](https://localhost:8501/) NOTE: For existing users, this will break the database since absolute paths of files are saved. A future fix will be added to fix this. diff --git "a/app/01_\360\237\217\240_Home.py" "b/app/01_\360\237\217\240_Home.py" index a6ca088..9406587 100644 --- "a/app/01_\360\237\217\240_Home.py" +++ "b/app/01_\360\237\217\240_Home.py" @@ -5,6 +5,13 @@ from config import get_page_config, get_whisper_settings, save_whisper_settings from core import MediaManager +# for self-signed-cert, not verified but un-safe. +import ssl +ssl._create_default_https_context = ssl._create_unverified_context + +from streamlit.runtime.uploaded_file_manager import UploadedFile, UploadedFileRec +from audiorecorder import audiorecorder + st.set_page_config(**get_page_config()) @@ -40,7 +47,7 @@ def get_formatted_date(date_str: str) -> str: # --------- with st.sidebar.expander("➕   Add Media", expanded=False): # # Render media type selection on the sidebar & the form - source_type = st.radio("Media Source", ["YouTube", "Upload"], label_visibility="collapsed") + source_type = st.radio("Media Source", ["YouTube", "Upload", "Mic"], label_visibility="collapsed") with st.form("input_form"): if source_type == "YouTube": youtube_url = st.text_input("Youtube video or playlist URL") @@ -48,6 +55,29 @@ def get_formatted_date(date_str: str) -> str: input_files = st.file_uploader( "Add one or more files", type=["mp4", "avi", "mov", "mkv", "mp3", "wav"], accept_multiple_files=True ) + elif source_type == "Mic": + audio = audiorecorder("Click to record", "Recording...") + if len(audio) > 0: + # To play audio in frontend: + #st.audio(audio.tobytes()) + + # To save audio to a file: + #wav_file = open("audio.mp3", "wb") + #wav_file.write(audio.tobytes()) + #wav_file.close() + + # emulate "uploaded file" + now = datetime.now() + d = f'{now:%Y%m%d%H%M%S}' + + filerec = UploadedFileRec(0, + f"mic_input_{d}.mp3", + "mp3", + audio.tobytes()) + input_files = UploadedFile(filerec) + else: + input_files = None + task_options = ["transcribe", "translate"] task = st.selectbox( "Task", options=task_options, index=task_options.index(st.session_state.whisper_params["task"]) @@ -66,6 +96,11 @@ def get_formatted_date(date_str: str) -> str: source = input_files else: st.error("Please upload files") + elif source_type == "Mic": + if input_files: + source = input_files + else: + st.error("Please input your voice via mic") # Lowercase the source type source_type = source_type.lower() @@ -156,7 +191,7 @@ def get_formatted_date(date_str: str) -> str: # Add a meta caption st.write(f"#### {media['source_name']}") - source_type = "YouTube" if media["source_type"] == "youtube" else "upload" + source_type = "YouTube" if media["source_type"] == "youtube" else media["source_type"] st.markdown( f""" Source: {source_type}
@@ -179,7 +214,7 @@ def get_formatted_date(date_str: str) -> str: # Render the media if media["source_type"] == "youtube": st.video(media["source_link"]) - elif media["source_type"] == "upload": + elif media["source_type"] in ["upload", "mic"]: st.audio(media["filepath"]) st.write("---") @@ -207,6 +242,12 @@ def get_formatted_date(date_str: str) -> str: # Add a meta caption source_type = "YouTube" if media["source_type"] == "youtube" else "uploaded" + if media["source_type"] == "youtube": + source_type = "YouTube" + elif media["source_type"] == "upload": + source_type = "uploaded" + else: + source_tpye = media["source_type"] st.markdown( f""" Source: {media['source_name']} ({source_type})
@@ -253,14 +294,27 @@ def get_formatted_date(date_str: str) -> str: if media["source_type"] == "youtube": st.sidebar.audio(media["filepath"], start_time=st.session_state.selected_media_offset) st.sidebar.video(media["source_link"]) - elif media["source_type"] == "upload": + elif media["source_type"] in ["upload", "mic"]: st.sidebar.audio(media["filepath"], start_time=st.session_state.selected_media_offset) st.write(f'## {media["source_name"]}') + with st.expander("🔽   Downloads"): + fpath = str(Path(media["filepath"]).parent) + fname = str(Path(media["filepath"]).parent.name) + for ext in (".srt",".vtt",".json",".txt",".tsv"): + with open(fpath + '/transcript' + ext) as f: + st.download_button('Download ' + ext, f, file_name=fname+ext); + with st.expander("📝   Metadata"): # Add a meta caption source_type = "YouTube" if media["source_type"] == "youtube" else "uploaded" + if media["source_type"] == "youtube": + source_type = "YouTube" + elif media["source_type"] == "upload": + source_type = "uploaded" + else: + source_tpye = media["source_type"] st.markdown( f""" Source: {media['source_name']} ({source_type})
diff --git a/app/core.py b/app/core.py index 6314fbd..d6db276 100644 --- a/app/core.py +++ b/app/core.py @@ -117,7 +117,7 @@ def _create(self, source_list: List[Union[str, Any]], source_type: str, whisper_ save_dir = self.media_dir / f"""{source_dirname}-{datetime.now().strftime("%Y-%m-%d %H-%M-%S")}""" # Download the audio file yc.streams.get_by_itag(140).download(save_dir, filename=save_filename) - elif source_type == "upload": + elif source_type in ["upload", "mic"]: # Parse the file name from the source tokens = source.name.split(".") source_name = ".".join(tokens[:-1]) @@ -162,7 +162,7 @@ def add(self, source: Union[str, Any], source_type: str, **whisper_args): source_list = list(Playlist(source)) else: source_list = [source] - elif source_type == "upload": + elif source_type in ["upload", "mic"]: source_list = source if type(source) == list else [source] # Download & save the audio files and add them to the database diff --git "a/app/pages/02_\342\232\231\357\270\217_Settings.py" "b/app/pages/02_\342\232\231\357\270\217_Settings.py" index 9e39b79..6d4c7f2 100644 --- "a/app/pages/02_\342\232\231\357\270\217_Settings.py" +++ "b/app/pages/02_\342\232\231\357\270\217_Settings.py" @@ -14,7 +14,7 @@ # -------------- st.write("### ⚙️ Whisper Settings") with st.form("whisper_settings_form"): - model_options = ["tiny", "base", "small", "medium", "large"] + model_options = ["tiny", "base", "small", "medium", "large", "large-v2"] selected_model = model_options.index(st.session_state.whisper_params["whisper_model"]) whisper_model = st.selectbox("Model", options=model_options, index=selected_model) temperature = st.number_input( diff --git a/docker-compose.yml b/docker-compose.yml index cac6058..2f3946c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,5 +11,6 @@ services: # TODO: Fix this to relative paths from some user-defined directory - ./data:/data restart: unless-stopped + init: true # https://qiita.com/sun33/items/d62744f5746f6815f3c8 ports: - 8501:8501 diff --git a/environment.yml b/environment.yml index c3bf90b..5be667a 100644 --- a/environment.yml +++ b/environment.yml @@ -75,7 +75,8 @@ dependencies: - six==1.16.0 - smmap==5.0.0 - sqlalchemy==2.0.2 - - streamlit==1.17.0 + - streamlit==1.20.0 + - streamlit-audiorecorder==0.0.2 - tokenizers==0.13.2 - toml==0.10.2 - toolz==0.12.0 diff --git a/requirements.txt b/requirements.txt index 9b16a64..50882ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,8 @@ SQLAlchemy==2.0.3 # https://github.com/sqlalchemy/sqlalchemy # Frontend # -------------------- -streamlit==1.18.1 # https://github.com/streamlit/streamlit +streamlit==1.20.0 # https://github.com/streamlit/streamlit + +# Audio Recorder on Frontend +# -------------------- +streamlit-audiorecorder==0.0.2 # https://github.com/theevann/streamlit-audiorecorder diff --git a/ssl/create-self-signed-cert.sh b/ssl/create-self-signed-cert.sh new file mode 100755 index 0000000..1f72f80 --- /dev/null +++ b/ssl/create-self-signed-cert.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +#openssl genrsa 2048 > server.key +#openssl req -new -key server.key > server.csr +#openssl x509 -days 3650 -req -sha256 -signkey server.key < server.csr > server.crt +#openssl x509 -text < server.crt + +# https://qiita.com/miyuki_samitani/items/b19aa5ac3b3c6e312bd5 +# https://qiita.com/sanyamarseille/items/46fc6ff5a0aca12e1946 + +# https://www.karakaram.com/creating-self-signed-certificate/ +openssl req -x509 -sha256 -nodes -days 3650 -newkey rsa:2048 -subj /CN=localhost -keyout server.key -out server.crt