From c8a58d5fc855be46f62f993bd8c0a95068203ba5 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Mon, 6 Nov 2023 17:50:41 +0530 Subject: [PATCH 01/29] Add ability to use private key with 11labs --- daras_ai_v2/base.py | 5 + .../text_to_speech_settings_widgets.py | 100 +++++++++++++++--- gooey_ui/components.py | 25 +++++ recipes/TextToSpeech.py | 58 ++++++---- routers/root.py | 5 + 5 files changed, 157 insertions(+), 36 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index f5c485c37..0a2c00304 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -91,6 +91,8 @@ class BasePage: slug_versions: list[str] sane_defaults: dict = {} + private_fields: list = [] + RequestModel: typing.Type[BaseModel] ResponseModel: typing.Type[BaseModel] @@ -1130,6 +1132,9 @@ def is_current_user_admin(self) -> bool: def is_current_user_paying(self) -> bool: return bool(self.request and self.request.user and self.request.user.is_paying) + def is_current_user_owner(self) -> bool: + return bool(self.request and self.request.user and self.run_user == self.request.user) + def get_example_request_body( request_model: typing.Type[BaseModel], diff --git a/daras_ai_v2/text_to_speech_settings_widgets.py b/daras_ai_v2/text_to_speech_settings_widgets.py index 137cdb3c9..27a2c03eb 100644 --- a/daras_ai_v2/text_to_speech_settings_widgets.py +++ b/daras_ai_v2/text_to_speech_settings_widgets.py @@ -1,5 +1,7 @@ +import hashlib from enum import Enum +import requests import gooey_ui as st from google.cloud import texttospeech @@ -227,25 +229,66 @@ def text_to_speech_settings(page=None): case TextToSpeechProviders.ELEVEN_LABS.name: with col2: - if not ( - page - and (page.is_current_user_paying() or page.is_current_user_admin()) - ): - st.caption( + elevenlabs_use_custom_key = st.checkbox( + "Use custom API key + Voice ID", + key="__elevenlabs_use_custom_key", + value=bool(st.session_state.get("elevenlabs_api_key") or st.session_state.get("elevenlabs_voice_id")), + ) + if elevenlabs_use_custom_key: + st.session_state["elevenlabs_voice_name"] = None + elevenlabs_api_key = st.password_input( """ - Note: Please purchase Gooey.AI credits to use ElevenLabs voices - here. + ###### Your ElevenLabs API key + *Refer here + for how to obtain an API key from ElevenLabs.* + """, + key="elevenlabs_api_key", + ) + + selected_voice_id = st.session_state.get("elevenlabs_voice_id") + elevenlabs_voices = {selected_voice_id: selected_voice_id} if selected_voice_id else {} + + if elevenlabs_api_key: + try: + elevenlabs_voices = get_cached_elevenlabs_voices(st.session_state, elevenlabs_api_key) + except requests.exceptions.HTTPError as e: + st.error(f"Invalid ElevenLabs API key. Failed to fetch voices: {e}") + else: + if selected_voice_id not in elevenlabs_voices: + st.error(f"Selected ElevenLabs voice ID is not available in your account: {selected_voice_id}") + + st.selectbox( """ + ###### Voice ID (ElevenLabs) + *Enter an API key to list the available voices in your account.* + """, + key="elevenlabs_voice_id", + options=elevenlabs_voices.keys(), + format_func=elevenlabs_voices.__getitem__, ) + else: + if not ( + page + and (page.is_current_user_paying() or page.is_current_user_admin()) + ): + st.caption( + """ + Note: Please purchase Gooey.AI credits to use ElevenLabs voices + here.
+ Alternatively, you can use your own ElevenLabs API key by selecting the checkbox above. + """ + ) + else: + st.session_state.update(elevenlabs_api_key=None, elevenlabs_voice_id=None) + st.selectbox( + """ + ###### Voice Name (ElevenLabs) + """, + key="elevenlabs_voice_name", + format_func=str, + options=ELEVEN_LABS_VOICES.keys(), + ) - st.selectbox( - """ - ###### Voice name (ElevenLabs) - """, - key="elevenlabs_voice_name", - format_func=str, - options=ELEVEN_LABS_VOICES.keys(), - ) st.selectbox( """ ###### Voice Model @@ -303,6 +346,33 @@ def google_tts_voices() -> dict[str, str]: return {voice.name: _pretty_voice(voice) for voice in voices} +def get_cached_elevenlabs_voices(state, api_key) -> dict[str, str]: + api_key_hash = hashlib.sha256(api_key.encode("utf-8")).hexdigest()[:40] + state.setdefault("__elevenlabs_voices_cache", {}) + if api_key_hash not in state["__elevenlabs_voices_cache"]: + state["__elevenlabs_voices_cache"] = { + api_key_hash: fetch_elevenlabs_voices(api_key) + } + return state["__elevenlabs_voices_cache"][api_key_hash] + + +def fetch_elevenlabs_voices(api_key: str) -> dict[str, str]: + r = requests.get( + "https://api.elevenlabs.io/v1/voices", + headers={"Accept": "application/json", + "xi-api-key": api_key}, + ) + r.raise_for_status() + sorted_voices = sorted( + r.json()["voices"], + key=lambda v: (int(v["category"] == "premade"), v["name"]), + ) + return { + v["voice_id"]: f"{v['name']} ({v['voice_id']})" + for v in sorted_voices + } + + def _pretty_voice(voice) -> str: return f"{voice.name} ({voice.ssml_gender.name.capitalize()})" diff --git a/gooey_ui/components.py b/gooey_ui/components.py index 4c19343e9..10d128edf 100644 --- a/gooey_ui/components.py +++ b/gooey_ui/components.py @@ -607,6 +607,31 @@ def text_input( ) return value or "" +def password_input( + label: str, + value: str = "", + max_chars: str = None, + key: str = None, + help: str = None, + *, + placeholder: str = None, + disabled: bool = False, + label_visibility: LabelVisibility = "visible", + **props, +) -> str: + value = _input_widget( + input_type="password", + label=label, + value=value, + key=key, + help=help, + disabled=disabled, + label_visibility=label_visibility, + maxLength=max_chars, + placeholder=placeholder, + **props, + ) + return value or "" def slider( label: str, diff --git a/recipes/TextToSpeech.py b/recipes/TextToSpeech.py index de8178cee..3713da238 100644 --- a/recipes/TextToSpeech.py +++ b/recipes/TextToSpeech.py @@ -47,6 +47,7 @@ class TextToSpeechPage(BasePage): "elevenlabs_stability": 0.5, "elevenlabs_similarity_boost": 0.75, } + private_fields = ["elevenlabs_api_key"] class RequestModel(BaseModel): text_prompt: str @@ -65,10 +66,13 @@ class RequestModel(BaseModel): bark_history_prompt: str | None elevenlabs_voice_name: str | None + elevenlabs_api_key: str | None + elevenlabs_voice_id: str | None elevenlabs_model: str | None elevenlabs_stability: float | None elevenlabs_similarity_boost: float | None + class ResponseModel(BaseModel): audio_url: str @@ -239,26 +243,9 @@ def run(self, state: dict): ) case TextToSpeechProviders.ELEVEN_LABS: - assert ( - self.is_current_user_paying() or self.is_current_user_admin() - ), """ - Please purchase Gooey.AI credits to use ElevenLabs voices here. - """ - - # default to first in the mapping - default_voice_model = next(iter(ELEVEN_LABS_MODELS)) - default_voice_name = next(iter(ELEVEN_LABS_VOICES)) - - voice_model = state.get("elevenlabs_model", default_voice_model) - voice_name = state.get("elevenlabs_voice_name", default_voice_name) - - # validate voice_model / voice_name - if voice_model not in ELEVEN_LABS_MODELS: - raise ValueError(f"Invalid model: {voice_model}") - if voice_name not in ELEVEN_LABS_VOICES: - raise ValueError(f"Invalid voice_name: {voice_name}") - else: - voice_id = ELEVEN_LABS_VOICES[voice_name] + xi_api_key = self._get_elevenlabs_api_key(state) + voice_model = self._get_elevenlabs_voice_model(state) + voice_id = self._get_elevenlabs_voice_id(state) stability = state.get("elevenlabs_stability", 0.5) similarity_boost = state.get("elevenlabs_similarity_boost", 0.75) @@ -266,7 +253,7 @@ def run(self, state: dict): response = requests.post( f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}", headers={ - "xi-api-key": settings.ELEVEN_LABS_API_KEY, + "xi-api-key": xi_api_key, "Accept": "audio/mpeg", }, json={ @@ -285,6 +272,35 @@ def run(self, state: dict): "elevenlabs_gen.mp3", response.content ) + def _get_elevenlabs_voice_model(self, state: dict[str, str]): + default_voice_model = next(iter(ELEVEN_LABS_MODELS)) + voice_model = state.get("elevenlabs_voice_model", default_voice_model) + assert voice_model in ELEVEN_LABS_MODELS, f"Invalid model: {voice_model}" + return voice_model + + def _get_elevenlabs_voice_id(self, state: dict[str, str]): + if state.get("elevenlabs_voice_id"): + assert state.get("elevenlabs_api_key"), "ElevenLabs API key is required to use a custom voice_id" + return state["elevenlabs_voice_id"] + else: + # default to first in the mapping + default_voice_name = next(iter(ELEVEN_LABS_VOICES)) + voice_name = state.get("elevenlabs_voice_name", default_voice_name) + assert voice_name in ELEVEN_LABS_VOICES, f"Invalid voice_name: {voice_name}" + return ELEVEN_LABS_VOICES[voice_name] # voice_name -> voice_id + + def _get_elevenlabs_api_key(self, state: dict[str, str]): + # ElevenLabs is available for non-paying users with their own API key + if state.get("elevenlabs_api_key"): + return state["elevenlabs_api_key"] + else: + assert ( + self.is_current_user_paying() or self.is_current_user_admin() + ), """ + Please purchase Gooey.AI credits to use ElevenLabs voices here. + """ + return settings.ELEVEN_LABS_API_KEY + def related_workflows(self) -> list: from recipes.VideoBots import VideoBotsPage from recipes.LipsyncTTS import LipsyncTTSPage diff --git a/routers/root.py b/routers/root.py index cdbfac43b..dd98d4876 100644 --- a/routers/root.py +++ b/routers/root.py @@ -237,6 +237,11 @@ def st_page( state.update(db_state) for k, v in page.sane_defaults.items(): state.setdefault(k, v) + + # pop private fields if not the owner + if not page.is_current_user_owner(): + for k in page.private_fields: + state.pop(k, None) if state is None: raise HTTPException(status_code=404) From 14a1c33033309e5425fc3940ed85a10e1516d287 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Mon, 6 Nov 2023 17:53:22 +0530 Subject: [PATCH 02/29] Add 11labs custom key to LipsyncTTS and VideoBots --- recipes/LipsyncTTS.py | 3 +++ recipes/VideoBots.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/recipes/LipsyncTTS.py b/recipes/LipsyncTTS.py index b2d4c406e..058e60d06 100644 --- a/recipes/LipsyncTTS.py +++ b/recipes/LipsyncTTS.py @@ -23,6 +23,7 @@ class LipsyncTTSPage(LipsyncPage, TextToSpeechPage): "elevenlabs_stability": 0.5, "elevenlabs_similarity_boost": 0.75, } + private_fields = ["elevenlabs_api_key"] class RequestModel(BaseModel): input_face: str @@ -47,6 +48,8 @@ class RequestModel(BaseModel): bark_history_prompt: str | None elevenlabs_voice_name: str | None + elevenlabs_api_key: str | None + elevenlabs_voice_id: str | None elevenlabs_model: str | None elevenlabs_stability: float | None elevenlabs_similarity_boost: float | None diff --git a/recipes/VideoBots.py b/recipes/VideoBots.py index 9958e973a..8d573231f 100644 --- a/recipes/VideoBots.py +++ b/recipes/VideoBots.py @@ -190,6 +190,7 @@ class VideoBotsPage(BasePage): "use_url_shortener": False, "dense_weight": 1.0, } + private_fields = ["elevenlabs_api_key"] class RequestModel(BaseModel): input_prompt: str @@ -206,6 +207,8 @@ class RequestModel(BaseModel): google_pitch: float | None bark_history_prompt: str | None elevenlabs_voice_name: str | None + elevenlabs_api_key: str | None + elevenlabs_voice_id: str | None elevenlabs_model: str | None elevenlabs_stability: float | None elevenlabs_similarity_boost: float | None From 9a04169775e0288e4b1d0e58f4784e38af99f8f5 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Mon, 6 Nov 2023 18:17:46 +0530 Subject: [PATCH 03/29] format with black --- daras_ai_v2/base.py | 4 +- .../text_to_speech_settings_widgets.py | 40 +++++++++++++------ gooey_ui/components.py | 2 + recipes/TextToSpeech.py | 5 ++- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index 0a2c00304..cf3c2994e 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -1133,7 +1133,9 @@ def is_current_user_paying(self) -> bool: return bool(self.request and self.request.user and self.request.user.is_paying) def is_current_user_owner(self) -> bool: - return bool(self.request and self.request.user and self.run_user == self.request.user) + return bool( + self.request and self.request.user and self.run_user == self.request.user + ) def get_example_request_body( diff --git a/daras_ai_v2/text_to_speech_settings_widgets.py b/daras_ai_v2/text_to_speech_settings_widgets.py index 27a2c03eb..dd27d7110 100644 --- a/daras_ai_v2/text_to_speech_settings_widgets.py +++ b/daras_ai_v2/text_to_speech_settings_widgets.py @@ -232,7 +232,10 @@ def text_to_speech_settings(page=None): elevenlabs_use_custom_key = st.checkbox( "Use custom API key + Voice ID", key="__elevenlabs_use_custom_key", - value=bool(st.session_state.get("elevenlabs_api_key") or st.session_state.get("elevenlabs_voice_id")), + value=bool( + st.session_state.get("elevenlabs_api_key") + or st.session_state.get("elevenlabs_voice_id") + ), ) if elevenlabs_use_custom_key: st.session_state["elevenlabs_voice_name"] = None @@ -246,16 +249,26 @@ def text_to_speech_settings(page=None): ) selected_voice_id = st.session_state.get("elevenlabs_voice_id") - elevenlabs_voices = {selected_voice_id: selected_voice_id} if selected_voice_id else {} + elevenlabs_voices = ( + {selected_voice_id: selected_voice_id} + if selected_voice_id + else {} + ) if elevenlabs_api_key: try: - elevenlabs_voices = get_cached_elevenlabs_voices(st.session_state, elevenlabs_api_key) + elevenlabs_voices = get_cached_elevenlabs_voices( + st.session_state, elevenlabs_api_key + ) except requests.exceptions.HTTPError as e: - st.error(f"Invalid ElevenLabs API key. Failed to fetch voices: {e}") + st.error( + f"Invalid ElevenLabs API key. Failed to fetch voices: {e}" + ) else: if selected_voice_id not in elevenlabs_voices: - st.error(f"Selected ElevenLabs voice ID is not available in your account: {selected_voice_id}") + st.error( + f"Selected ElevenLabs voice ID is not available in your account: {selected_voice_id}" + ) st.selectbox( """ @@ -269,7 +282,10 @@ def text_to_speech_settings(page=None): else: if not ( page - and (page.is_current_user_paying() or page.is_current_user_admin()) + and ( + page.is_current_user_paying() + or page.is_current_user_admin() + ) ): st.caption( """ @@ -279,7 +295,9 @@ def text_to_speech_settings(page=None): """ ) else: - st.session_state.update(elevenlabs_api_key=None, elevenlabs_voice_id=None) + st.session_state.update( + elevenlabs_api_key=None, elevenlabs_voice_id=None + ) st.selectbox( """ ###### Voice Name (ElevenLabs) @@ -359,18 +377,14 @@ def get_cached_elevenlabs_voices(state, api_key) -> dict[str, str]: def fetch_elevenlabs_voices(api_key: str) -> dict[str, str]: r = requests.get( "https://api.elevenlabs.io/v1/voices", - headers={"Accept": "application/json", - "xi-api-key": api_key}, + headers={"Accept": "application/json", "xi-api-key": api_key}, ) r.raise_for_status() sorted_voices = sorted( r.json()["voices"], key=lambda v: (int(v["category"] == "premade"), v["name"]), ) - return { - v["voice_id"]: f"{v['name']} ({v['voice_id']})" - for v in sorted_voices - } + return {v["voice_id"]: f"{v['name']} ({v['voice_id']})" for v in sorted_voices} def _pretty_voice(voice) -> str: diff --git a/gooey_ui/components.py b/gooey_ui/components.py index 10d128edf..3981e61e1 100644 --- a/gooey_ui/components.py +++ b/gooey_ui/components.py @@ -607,6 +607,7 @@ def text_input( ) return value or "" + def password_input( label: str, value: str = "", @@ -633,6 +634,7 @@ def password_input( ) return value or "" + def slider( label: str, min_value: float = None, diff --git a/recipes/TextToSpeech.py b/recipes/TextToSpeech.py index 3713da238..d56866d49 100644 --- a/recipes/TextToSpeech.py +++ b/recipes/TextToSpeech.py @@ -72,7 +72,6 @@ class RequestModel(BaseModel): elevenlabs_stability: float | None elevenlabs_similarity_boost: float | None - class ResponseModel(BaseModel): audio_url: str @@ -280,7 +279,9 @@ def _get_elevenlabs_voice_model(self, state: dict[str, str]): def _get_elevenlabs_voice_id(self, state: dict[str, str]): if state.get("elevenlabs_voice_id"): - assert state.get("elevenlabs_api_key"), "ElevenLabs API key is required to use a custom voice_id" + assert state.get( + "elevenlabs_api_key" + ), "ElevenLabs API key is required to use a custom voice_id" return state["elevenlabs_voice_id"] else: # default to first in the mapping From 8dc0044cd3550a08f92d785e1068a56c22c42011 Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Tue, 7 Nov 2023 01:39:35 -0800 Subject: [PATCH 04/29] added ffmpeg supported audio types as accepts --- recipes/asr.py | 78 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/recipes/asr.py b/recipes/asr.py index 694f4bc95..120871f10 100644 --- a/recipes/asr.py +++ b/recipes/asr.py @@ -81,17 +81,79 @@ def render_form_v2(self): document_uploader( "##### Audio Files", accept=( - ".wav", - ".ogg", + ".aa", # Audio formats that ffmpeg can demux and decode (https://ffmpeg.org/ffmpeg-formats.html) + ".amr", + ".aac", # aac demuxer + ".m4p", + ".m4r", ".mp3", - ".aac", - ".opus", - ".oga", + ".asf", # asf demuxer + ".wma", + ".wmv", + ".xml", # imf demuxer + ".mxf", + ".flv", # flv, live_flv, kux demuxer + ".f4v", + ".f4p", + ".f4a", + ".f4b", + ".m3u8", # HLS demuxer + ".mov", # Demuxer for Quicktime File Format & ISO/IEC Base Media File Format (ISO/IEC 14496-12 or MPEG-4 Part 12, ISO/IEC 15444-12 or JPEG 2000 Part 12) ".mp4", - ".webm", - ".amr", - ".aac", ".m4a", + ".3gp", + ".3g2", + ".mj2", + ".psp", + ".m4b", + ".ism", + ".ismv", + ".isma", + ".f4v", + ".aax", # Audible AAX + ".aiff", # Audio Interchange File Format + ".aif", + ".aifc", + ".tun", # Muxer for audio of High Voltage Software’s Lego Racers game + ".pcm", + ".ts", # MPEG Transport Stream + ".tsv", + ".tsa", + ".m2t", + ".ogv", # Ogg + ".oga", + ".ogx", + ".ogg", + ".opus", + ".ac3", # Dolby Digital + ".adx", # CRI ADX + ".aptx", # aptX + ".aptxhd", + ".avs", # AVS2-P2/IEEE1857.4 video. + ".avs2", + ".cavs", # Chinese AVS (Audio Video Standard) video + ".drc", # BBC Dirac video. + ".vc2", + ".dnxhd", # Avid DNxHD video + ".dnxhr", + ".evc", # MPEG-5 Essential Video Coding (EVC) / EVC / MPEG-5 Part 1 EVC video. + ".tco", # ITU-T G.723.1 audio. + ".rco", + ".h264", # ITU-T H.264 / MPEG-4 Part 10 AVC video. + ".264", + ".hevc", # ITU-T H.265 / MPEG-H Part 2 HEVC video + ".h265", + ".265", + ".mp2", # MPEG-1 Audio Layer II + ".m2a", + ".mpa", + ".yuv", # raw video + ".rgb", + ".sbc", # SBC (low-complexity subband codec) audio + ".msbc", + ".thd", # Dolby TrueHD audio + ".webm", # WebM + ".wav", # Waveform Audio File Format ), ) col1, col2 = st.columns(2, responsive=False) From 79948875ea2ca2ed527fe9e4569e1d45975eea8e Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Tue, 7 Nov 2023 17:53:53 +0530 Subject: [PATCH 05/29] Use text_input for 11labs api key, add voice description to selectbox --- .../text_to_speech_settings_widgets.py | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/daras_ai_v2/text_to_speech_settings_widgets.py b/daras_ai_v2/text_to_speech_settings_widgets.py index dd27d7110..c24751c50 100644 --- a/daras_ai_v2/text_to_speech_settings_widgets.py +++ b/daras_ai_v2/text_to_speech_settings_widgets.py @@ -239,11 +239,12 @@ def text_to_speech_settings(page=None): ) if elevenlabs_use_custom_key: st.session_state["elevenlabs_voice_name"] = None - elevenlabs_api_key = st.password_input( + elevenlabs_api_key = st.text_input( """ ###### Your ElevenLabs API key - *Refer here - for how to obtain an API key from ElevenLabs.* + *Read this + to know how to obtain an API key from + ElevenLabs.* """, key="elevenlabs_api_key", ) @@ -264,16 +265,10 @@ def text_to_speech_settings(page=None): st.error( f"Invalid ElevenLabs API key. Failed to fetch voices: {e}" ) - else: - if selected_voice_id not in elevenlabs_voices: - st.error( - f"Selected ElevenLabs voice ID is not available in your account: {selected_voice_id}" - ) st.selectbox( """ ###### Voice ID (ElevenLabs) - *Enter an API key to list the available voices in your account.* """, key="elevenlabs_voice_id", options=elevenlabs_voices.keys(), @@ -294,18 +289,18 @@ def text_to_speech_settings(page=None): Alternatively, you can use your own ElevenLabs API key by selecting the checkbox above. """ ) - else: - st.session_state.update( - elevenlabs_api_key=None, elevenlabs_voice_id=None - ) - st.selectbox( - """ - ###### Voice Name (ElevenLabs) - """, - key="elevenlabs_voice_name", - format_func=str, - options=ELEVEN_LABS_VOICES.keys(), - ) + + st.session_state.update( + elevenlabs_api_key=None, elevenlabs_voice_id=None + ) + st.selectbox( + """ + ###### Voice Name (ElevenLabs) + """, + key="elevenlabs_voice_name", + format_func=str, + options=ELEVEN_LABS_VOICES.keys(), + ) st.selectbox( """ @@ -363,6 +358,8 @@ def google_tts_voices() -> dict[str, str]: voices.sort(key=_voice_sort_key) return {voice.name: _pretty_voice(voice) for voice in voices} +def _pretty_voice(voice) -> str: + return f"{voice.name} ({voice.ssml_gender.name.capitalize()})" def get_cached_elevenlabs_voices(state, api_key) -> dict[str, str]: api_key_hash = hashlib.sha256(api_key.encode("utf-8")).hexdigest()[:40] @@ -384,11 +381,8 @@ def fetch_elevenlabs_voices(api_key: str) -> dict[str, str]: r.json()["voices"], key=lambda v: (int(v["category"] == "premade"), v["name"]), ) - return {v["voice_id"]: f"{v['name']} ({v['voice_id']})" for v in sorted_voices} - - -def _pretty_voice(voice) -> str: - return f"{voice.name} ({voice.ssml_gender.name.capitalize()})" + describe_voice = lambda v: ", ".join(v["labels"].values()) + return {v["voice_id"]: f"{v['name']} - {describe_voice(v)}" for v in sorted_voices} _lang_code_sort = ["en-US", "en-IN", "en-GB", "en-AU"] From bb7e8c8394c8bec036f223674a01bf3c5aa729b2 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Tue, 7 Nov 2023 21:42:04 +0530 Subject: [PATCH 06/29] Add notes, pricing for 11labs with custom key and remove it from db fields --- daras_ai_v2/base.py | 10 +++- .../text_to_speech_settings_widgets.py | 2 +- recipes/LipsyncTTS.py | 1 - recipes/TextToSpeech.py | 50 +++++++++++++++---- recipes/VideoBots.py | 16 +++++- routers/root.py | 5 -- 6 files changed, 62 insertions(+), 22 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index cf3c2994e..75dc34483 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -148,15 +148,21 @@ def api_url(self, example_id=None, run_id=None, uid=None) -> furl: def endpoint(self) -> str: return f"/v2/{self.slug_versions[0]}/" - def render(self): + def before_render(self): + """ + Side-effects to apply before doing the actual render. + This shouldn't actually render anything to the page. + """ with sentry_sdk.configure_scope() as scope: scope.set_extra("base_url", self.app_url()) scope.set_transaction_name( "/" + self.slug_versions[0], source=TRANSACTION_SOURCE_ROUTE ) - example_id, run_id, uid = extract_query_params(gooey_get_query_params()) + def render(self): + self.before_render() + example_id, run_id, uid = extract_query_params(gooey_get_query_params()) if st.session_state.get(StateKeys.run_status): channel = f"gooey-outputs/{self.slug_versions[0]}/{uid}/{run_id}" output = realtime_pull([channel])[0] diff --git a/daras_ai_v2/text_to_speech_settings_widgets.py b/daras_ai_v2/text_to_speech_settings_widgets.py index c24751c50..8081eb6b4 100644 --- a/daras_ai_v2/text_to_speech_settings_widgets.py +++ b/daras_ai_v2/text_to_speech_settings_widgets.py @@ -25,7 +25,7 @@ class TextToSpeechProviders(Enum): GOOGLE_TTS = "Google Cloud Text-to-Speech" - ELEVEN_LABS = "Eleven Labs (Premium)" + ELEVEN_LABS = "Eleven Labs" UBERDUCK = "uberduck.ai" BARK = "Bark (suno-ai)" diff --git a/recipes/LipsyncTTS.py b/recipes/LipsyncTTS.py index 058e60d06..371a14fa7 100644 --- a/recipes/LipsyncTTS.py +++ b/recipes/LipsyncTTS.py @@ -23,7 +23,6 @@ class LipsyncTTSPage(LipsyncPage, TextToSpeechPage): "elevenlabs_stability": 0.5, "elevenlabs_similarity_boost": 0.75, } - private_fields = ["elevenlabs_api_key"] class RequestModel(BaseModel): input_face: str diff --git a/recipes/TextToSpeech.py b/recipes/TextToSpeech.py index d56866d49..0a479011a 100644 --- a/recipes/TextToSpeech.py +++ b/recipes/TextToSpeech.py @@ -47,7 +47,6 @@ class TextToSpeechPage(BasePage): "elevenlabs_stability": 0.5, "elevenlabs_similarity_boost": 0.75, } - private_fields = ["elevenlabs_api_key"] class RequestModel(BaseModel): text_prompt: str @@ -81,6 +80,16 @@ def fallback_preivew_image(self) -> str | None: def preview_description(self, state: dict) -> str: return "Input your text, pick a voice & a Text-to-Speech AI engine to create audio. Compare the best voice generators from Google, UberDuck.ai & more to add automated voices to your podcast, YouTube videos, website, or app." + def before_render(self): + super().before_render() + if st.session_state.get("tts_provider") == TextToSpeechProviders.ELEVEN_LABS.name: + if elevenlabs_api_key := st.session_state.get("elevenlabs_api_key"): + self.request.session["state"] = dict(elevenlabs_api_key=elevenlabs_api_key) + elif "elevenlabs_api_key" in self.request.session.get("state", {}): + st.session_state["elevenlabs_api_key"] = self.request.session["state"][ + "elevenlabs_api_key" + ] + def render_description(self): st.write( """ @@ -103,6 +112,12 @@ def render_form_v2(self): key="text_prompt", ) + def fields_to_save(self): + fields = super().fields_to_save() + if "elevenlabs_api_key" in fields: + fields.remove("elevenlabs_api_key") + return fields + def validate_form_v2(self): assert st.session_state["text_prompt"], "Text input cannot be empty" @@ -130,9 +145,13 @@ def render_output(self): st.div() def _get_eleven_labs_price(self, state: dict): - text = state.get("text_prompt", "") - # 0.079 credits / character ~ 4 credits / 10 words - return len(text) * 0.079 + _, is_user_provided_key = self._get_elevenlabs_api_key(state) + if is_user_provided_key: + return 0 + else: + text = state.get("text_prompt", "") + # 0.079 credits / character ~ 4 credits / 10 words + return len(text) * 0.079 def _get_tts_provider(self, state: dict): tts_provider = state.get("tts_provider", TextToSpeechProviders.UBERDUCK.name) @@ -142,9 +161,15 @@ def _get_tts_provider(self, state: dict): def additional_notes(self): tts_provider = st.session_state.get("tts_provider") if tts_provider == TextToSpeechProviders.ELEVEN_LABS.name: - return """ - *Eleven Labs cost ≈ 4 credits per 10 words* - """ + _, is_user_provided_key = self._get_elevenlabs_api_key(st.session_state) + if is_user_provided_key: + return """ + *Eleven Labs cost ≈ No additional credit charge given we'll use your API key* + """ + else: + return """ + *Eleven Labs cost ≈ 4 credits per 10 words* + """ else: return "" @@ -242,7 +267,7 @@ def run(self, state: dict): ) case TextToSpeechProviders.ELEVEN_LABS: - xi_api_key = self._get_elevenlabs_api_key(state) + xi_api_key, _ = self._get_elevenlabs_api_key(state) voice_model = self._get_elevenlabs_voice_model(state) voice_id = self._get_elevenlabs_voice_id(state) @@ -290,17 +315,20 @@ def _get_elevenlabs_voice_id(self, state: dict[str, str]): assert voice_name in ELEVEN_LABS_VOICES, f"Invalid voice_name: {voice_name}" return ELEVEN_LABS_VOICES[voice_name] # voice_name -> voice_id - def _get_elevenlabs_api_key(self, state: dict[str, str]): + def _get_elevenlabs_api_key(self, state: dict[str, str]) -> tuple[str, bool]: + """ + Returns the 11labs API key and whether it is a user-provided key or the default key + """ # ElevenLabs is available for non-paying users with their own API key if state.get("elevenlabs_api_key"): - return state["elevenlabs_api_key"] + return state["elevenlabs_api_key"], True else: assert ( self.is_current_user_paying() or self.is_current_user_admin() ), """ Please purchase Gooey.AI credits to use ElevenLabs voices here. """ - return settings.ELEVEN_LABS_API_KEY + return settings.ELEVEN_LABS_API_KEY, False def related_workflows(self) -> list: from recipes.VideoBots import VideoBotsPage diff --git a/recipes/VideoBots.py b/recipes/VideoBots.py index 8d573231f..eaf4bf7d1 100644 --- a/recipes/VideoBots.py +++ b/recipes/VideoBots.py @@ -190,7 +190,6 @@ class VideoBotsPage(BasePage): "use_url_shortener": False, "dense_weight": 1.0, } - private_fields = ["elevenlabs_api_key"] class RequestModel(BaseModel): input_prompt: str @@ -276,6 +275,16 @@ class ResponseModel(BaseModel): def preview_image(self, state: dict) -> str | None: return DEFAULT_COPILOT_META_IMG + def before_render(self): + super().before_render() + if st.session_state.get("tts_provider") == TextToSpeechProviders.ELEVEN_LABS.name: + if elevenlabs_api_key := st.session_state.get("elevenlabs_api_key"): + self.request.session["state"] = dict(elevenlabs_api_key=elevenlabs_api_key) + elif "elevenlabs_api_key" in self.request.session.get("state", {}): + st.session_state["elevenlabs_api_key"] = self.request.session["state"][ + "elevenlabs_api_key" + ] + def related_workflows(self): from recipes.LipsyncTTS import LipsyncTTSPage from recipes.CompareText2Img import CompareText2ImgPage @@ -403,7 +412,10 @@ def render_settings(self): lipsync_settings() def fields_to_save(self) -> [str]: - return super().fields_to_save() + ["landbot_url"] + fields = super().fields_to_save() + ["landbot_url"] + if "elevenlabs_api_key" in fields: + fields.remove("elevenlabs_api_key") + return fields def render_example(self, state: dict): input_prompt = state.get("input_prompt") diff --git a/routers/root.py b/routers/root.py index dd98d4876..cdbfac43b 100644 --- a/routers/root.py +++ b/routers/root.py @@ -237,11 +237,6 @@ def st_page( state.update(db_state) for k, v in page.sane_defaults.items(): state.setdefault(k, v) - - # pop private fields if not the owner - if not page.is_current_user_owner(): - for k in page.private_fields: - state.pop(k, None) if state is None: raise HTTPException(status_code=404) From 26d54afbc74c0f4a73208351d8edc46674d93507 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Tue, 7 Nov 2023 23:34:33 +0530 Subject: [PATCH 07/29] format with black --- daras_ai_v2/text_to_speech_settings_widgets.py | 2 ++ recipes/TextToSpeech.py | 9 +++++++-- recipes/VideoBots.py | 9 +++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/daras_ai_v2/text_to_speech_settings_widgets.py b/daras_ai_v2/text_to_speech_settings_widgets.py index 8081eb6b4..9efedece5 100644 --- a/daras_ai_v2/text_to_speech_settings_widgets.py +++ b/daras_ai_v2/text_to_speech_settings_widgets.py @@ -358,9 +358,11 @@ def google_tts_voices() -> dict[str, str]: voices.sort(key=_voice_sort_key) return {voice.name: _pretty_voice(voice) for voice in voices} + def _pretty_voice(voice) -> str: return f"{voice.name} ({voice.ssml_gender.name.capitalize()})" + def get_cached_elevenlabs_voices(state, api_key) -> dict[str, str]: api_key_hash = hashlib.sha256(api_key.encode("utf-8")).hexdigest()[:40] state.setdefault("__elevenlabs_voices_cache", {}) diff --git a/recipes/TextToSpeech.py b/recipes/TextToSpeech.py index 0a479011a..321de9764 100644 --- a/recipes/TextToSpeech.py +++ b/recipes/TextToSpeech.py @@ -82,9 +82,14 @@ def preview_description(self, state: dict) -> str: def before_render(self): super().before_render() - if st.session_state.get("tts_provider") == TextToSpeechProviders.ELEVEN_LABS.name: + if ( + st.session_state.get("tts_provider") + == TextToSpeechProviders.ELEVEN_LABS.name + ): if elevenlabs_api_key := st.session_state.get("elevenlabs_api_key"): - self.request.session["state"] = dict(elevenlabs_api_key=elevenlabs_api_key) + self.request.session["state"] = dict( + elevenlabs_api_key=elevenlabs_api_key + ) elif "elevenlabs_api_key" in self.request.session.get("state", {}): st.session_state["elevenlabs_api_key"] = self.request.session["state"][ "elevenlabs_api_key" diff --git a/recipes/VideoBots.py b/recipes/VideoBots.py index eaf4bf7d1..b9b7eb37a 100644 --- a/recipes/VideoBots.py +++ b/recipes/VideoBots.py @@ -277,9 +277,14 @@ def preview_image(self, state: dict) -> str | None: def before_render(self): super().before_render() - if st.session_state.get("tts_provider") == TextToSpeechProviders.ELEVEN_LABS.name: + if ( + st.session_state.get("tts_provider") + == TextToSpeechProviders.ELEVEN_LABS.name + ): if elevenlabs_api_key := st.session_state.get("elevenlabs_api_key"): - self.request.session["state"] = dict(elevenlabs_api_key=elevenlabs_api_key) + self.request.session["state"] = dict( + elevenlabs_api_key=elevenlabs_api_key + ) elif "elevenlabs_api_key" in self.request.session.get("state", {}): st.session_state["elevenlabs_api_key"] = self.request.session["state"][ "elevenlabs_api_key" From f98a2e691ec492471ebd5ea0b04dd8608ee839d4 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 8 Nov 2023 18:17:11 +0530 Subject: [PATCH 08/29] Refactoring and fixes for custom 11labs key in TTS and copilot --- daras_ai_v2/base.py | 8 ++- .../text_to_speech_settings_widgets.py | 4 -- recipes/TextToSpeech.py | 58 ++++++++++++------- recipes/VideoBots.py | 15 +---- 4 files changed, 44 insertions(+), 41 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index 75dc34483..dae5253d8 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -153,15 +153,17 @@ def before_render(self): Side-effects to apply before doing the actual render. This shouldn't actually render anything to the page. """ + pass + + def render(self): + self.before_render() + with sentry_sdk.configure_scope() as scope: scope.set_extra("base_url", self.app_url()) scope.set_transaction_name( "/" + self.slug_versions[0], source=TRANSACTION_SOURCE_ROUTE ) - def render(self): - self.before_render() - example_id, run_id, uid = extract_query_params(gooey_get_query_params()) if st.session_state.get(StateKeys.run_status): channel = f"gooey-outputs/{self.slug_versions[0]}/{uid}/{run_id}" diff --git a/daras_ai_v2/text_to_speech_settings_widgets.py b/daras_ai_v2/text_to_speech_settings_widgets.py index 9efedece5..047f49a5c 100644 --- a/daras_ai_v2/text_to_speech_settings_widgets.py +++ b/daras_ai_v2/text_to_speech_settings_widgets.py @@ -232,10 +232,6 @@ def text_to_speech_settings(page=None): elevenlabs_use_custom_key = st.checkbox( "Use custom API key + Voice ID", key="__elevenlabs_use_custom_key", - value=bool( - st.session_state.get("elevenlabs_api_key") - or st.session_state.get("elevenlabs_voice_id") - ), ) if elevenlabs_use_custom_key: st.session_state["elevenlabs_voice_name"] = None diff --git a/recipes/TextToSpeech.py b/recipes/TextToSpeech.py index 321de9764..497fa87ba 100644 --- a/recipes/TextToSpeech.py +++ b/recipes/TextToSpeech.py @@ -82,18 +82,12 @@ def preview_description(self, state: dict) -> str: def before_render(self): super().before_render() - if ( - st.session_state.get("tts_provider") - == TextToSpeechProviders.ELEVEN_LABS.name - ): - if elevenlabs_api_key := st.session_state.get("elevenlabs_api_key"): - self.request.session["state"] = dict( - elevenlabs_api_key=elevenlabs_api_key - ) - elif "elevenlabs_api_key" in self.request.session.get("state", {}): - st.session_state["elevenlabs_api_key"] = self.request.session["state"][ - "elevenlabs_api_key" - ] + + state = st.session_state + if self._get_tts_provider(state) == TextToSpeechProviders.ELEVEN_LABS: + if state.get("__elevenlabs_use_custom_key") is None: + state["__elevenlabs_use_custom_key"] = bool(state.get("elevenlabs_voice_id")) + self.save_or_restore_elevenlabs_api_key(state) def render_description(self): st.write( @@ -133,7 +127,7 @@ def get_raw_price(self, state: dict): tts_provider = self._get_tts_provider(state) match tts_provider: case TextToSpeechProviders.ELEVEN_LABS: - return self._get_eleven_labs_price(state) + return self._get_elevenlabs_price(state) case _: return super().get_raw_price(state) @@ -149,7 +143,7 @@ def render_output(self): else: st.div() - def _get_eleven_labs_price(self, state: dict): + def _get_elevenlabs_price(self, state: dict): _, is_user_provided_key = self._get_elevenlabs_api_key(state) if is_user_provided_key: return 0 @@ -272,7 +266,13 @@ def run(self, state: dict): ) case TextToSpeechProviders.ELEVEN_LABS: - xi_api_key, _ = self._get_elevenlabs_api_key(state) + xi_api_key, is_custom_key = self._get_elevenlabs_api_key(state) + assert ( + is_custom_key or self.is_current_user_paying() or self.is_current_user_admin() + ), """ + Please purchase Gooey.AI credits to use ElevenLabs voices here. + """ + voice_model = self._get_elevenlabs_voice_model(state) voice_id = self._get_elevenlabs_voice_id(state) @@ -303,7 +303,7 @@ def run(self, state: dict): def _get_elevenlabs_voice_model(self, state: dict[str, str]): default_voice_model = next(iter(ELEVEN_LABS_MODELS)) - voice_model = state.get("elevenlabs_voice_model", default_voice_model) + voice_model = state.get("elevenlabs_model", default_voice_model) assert voice_model in ELEVEN_LABS_MODELS, f"Invalid model: {voice_model}" return voice_model @@ -328,11 +328,6 @@ def _get_elevenlabs_api_key(self, state: dict[str, str]) -> tuple[str, bool]: if state.get("elevenlabs_api_key"): return state["elevenlabs_api_key"], True else: - assert ( - self.is_current_user_paying() or self.is_current_user_admin() - ), """ - Please purchase Gooey.AI credits to use ElevenLabs voices here. - """ return settings.ELEVEN_LABS_API_KEY, False def related_workflows(self) -> list: @@ -358,3 +353,24 @@ def render_example(self, state: dict): audio_url = state.get("audio_url") if audio_url: st.audio(audio_url) + + def save_or_restore_elevenlabs_api_key(self, state: dict): + # elevenlabs_api_key is in state, so save it to session + if (new_value := state.get("elevenlabs_api_key")) is not None: + self._save_to_cookie("elevenlabs_api_key", new_value) + elif state.get("__elevenlabs_use_custom_key") is True or state.get("elevenlabs_voice_id"): + state["elevenlabs_api_key"] = self._fetch_from_cookie("elevenlabs_api_key") + + def _save_to_cookie(self, key: str, value: str, store_name: str = "state") -> None: + if value is None: + # delete key + self.request.session.setdefault(store_name, {}) + self.request.session[store_name].pop(key, None) + elif self._fetch_from_cookie(key) != value: + # current value != new value, so update + self.request.session.setdefault(store_name, {}) + self.request.session[store_name][key] = value + + def _fetch_from_cookie(self, key: str, store_name: str = "state") -> str | None: + # returns None if value not found + return self.request.session.get(store_name, {}).get(key) diff --git a/recipes/VideoBots.py b/recipes/VideoBots.py index b9b7eb37a..5a93ce1c0 100644 --- a/recipes/VideoBots.py +++ b/recipes/VideoBots.py @@ -277,18 +277,7 @@ def preview_image(self, state: dict) -> str | None: def before_render(self): super().before_render() - if ( - st.session_state.get("tts_provider") - == TextToSpeechProviders.ELEVEN_LABS.name - ): - if elevenlabs_api_key := st.session_state.get("elevenlabs_api_key"): - self.request.session["state"] = dict( - elevenlabs_api_key=elevenlabs_api_key - ) - elif "elevenlabs_api_key" in self.request.session.get("state", {}): - st.session_state["elevenlabs_api_key"] = self.request.session["state"][ - "elevenlabs_api_key" - ] + TextToSpeechPage(request=self.request).before_render() def related_workflows(self): from recipes.LipsyncTTS import LipsyncTTSPage @@ -597,7 +586,7 @@ def additional_notes(self): case TextToSpeechProviders.ELEVEN_LABS.name: return f""" - *Base cost = {super().get_raw_price(st.session_state)} credits* - - *Additional Eleven Labs cost ≈ 4 credits per 10 words of the output* + - *Additional {TextToSpeechPage().additional_notes()}* """ case _: return "" From 00cb694e3b71bb60555ea4ed6cf3d4676fb89ecf Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 8 Nov 2023 20:17:57 +0530 Subject: [PATCH 09/29] move t&c out of sticky container --- daras_ai_v2/base.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index f8899b172..c02834778 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -479,10 +479,12 @@ def render_submit_button(self, key="--submit-1"): col2.node.props[ "className" ] += " d-flex justify-content-end align-items-center" + col1.node.props[ + "className" + ] += " d-flex flex-column justify-content-center" with col1: st.caption( - f"Run cost = [{self.get_price_roundoff(st.session_state)} credits]({self.get_credits_click_url()}) \\\n" - f"_By submitting, you agree to Gooey.AI's [terms](https://gooey.ai/terms) & [privacy policy](https://gooey.ai/privacy)._ ", + f"Run cost = [{self.get_price_roundoff(st.session_state)} credits]({self.get_credits_click_url()})" ) additional_notes = self.additional_notes() if additional_notes: @@ -632,6 +634,10 @@ def _render_input_col(self): st.text_input("Title", key=StateKeys.page_title) st.text_area("Notes", key=StateKeys.page_notes) submitted = self.render_submit_button() + st.caption( + "_By submitting, you agree to Gooey.AI's [terms](https://gooey.ai/terms) &" + "[privacy policy](https://gooey.ai/privacy)._" + ) return submitted def _render_output_col(self, submitted: bool): From b9f08a9610c72e86f7b5e163fa3c576101f88155 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 8 Nov 2023 20:41:47 +0530 Subject: [PATCH 10/29] run black formatter --- daras_ai_v2/base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index c02834778..be68ce72c 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -479,9 +479,7 @@ def render_submit_button(self, key="--submit-1"): col2.node.props[ "className" ] += " d-flex justify-content-end align-items-center" - col1.node.props[ - "className" - ] += " d-flex flex-column justify-content-center" + col1.node.props["className"] += " d-flex flex-column justify-content-center" with col1: st.caption( f"Run cost = [{self.get_price_roundoff(st.session_state)} credits]({self.get_credits_click_url()})" From 6dc3a008c48567d57347ff64160b5db7509e3143 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:20:39 +0530 Subject: [PATCH 11/29] Add cost note for presenting text in parens after showing run cost --- daras_ai_v2/base.py | 9 ++++++++- recipes/DeforumSD.py | 8 ++++---- recipes/Lipsync.py | 6 ++---- recipes/LipsyncTTS.py | 22 ++++++++++++++-------- recipes/TextToSpeech.py | 6 ++---- recipes/asr.py | 6 ++---- 6 files changed, 32 insertions(+), 25 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index be68ce72c..bb81ce045 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -481,8 +481,12 @@ def render_submit_button(self, key="--submit-1"): ] += " d-flex justify-content-end align-items-center" col1.node.props["className"] += " d-flex flex-column justify-content-center" with col1: + cost_note = self.get_cost_note() or "" + if cost_note: + cost_note = f"({cost_note.strip()})" st.caption( f"Run cost = [{self.get_price_roundoff(st.session_state)} credits]({self.get_credits_click_url()})" + f" {cost_note}" ) additional_notes = self.additional_notes() if additional_notes: @@ -633,7 +637,7 @@ def _render_input_col(self): st.text_area("Notes", key=StateKeys.page_notes) submitted = self.render_submit_button() st.caption( - "_By submitting, you agree to Gooey.AI's [terms](https://gooey.ai/terms) &" + "_By submitting, you agree to Gooey.AI's [terms](https://gooey.ai/terms) & " "[privacy policy](https://gooey.ai/privacy)._" ) return submitted @@ -1145,6 +1149,9 @@ def get_example_response_body( def additional_notes(self) -> str | None: pass + def get_cost_note(self) -> str | None: + pass + def is_current_user_admin(self) -> bool: if not self.request or not self.request.user: return False diff --git a/recipes/DeforumSD.py b/recipes/DeforumSD.py index c2ebd231c..b5ab1c327 100644 --- a/recipes/DeforumSD.py +++ b/recipes/DeforumSD.py @@ -229,11 +229,11 @@ def render_form_v2(self): """ ) + def get_cost_note(self) -> str | None: + return f"{CREDITS_PER_FRAME} / frame" + def additional_notes(self) -> str | None: - return f""" -*Cost ≈ {CREDITS_PER_FRAME} credits per frame* \\ -*Process Run Time ≈ 5 seconds per frame* - """ + return "Render Time ≈ 3s / frame" def get_raw_price(self, state: dict) -> float: max_frames = state.get("max_frames", 100) or 0 diff --git a/recipes/Lipsync.py b/recipes/Lipsync.py index 44e2af76f..68ac496a0 100644 --- a/recipes/Lipsync.py +++ b/recipes/Lipsync.py @@ -126,10 +126,8 @@ def render_usage_guide(self): def preview_description(self, state: dict) -> str: return "Create high-quality, realistic Lipsync animations from any audio file. Input a sample face gif/video + audio and we will automatically generate a lipsync animation that matches your audio." - def additional_notes(self) -> str | None: - return f""" - *Cost ≈ {CREDITS_PER_MB} credits per MB* - """ + def get_cost_note(self) -> str | None: + return f"{CREDITS_PER_MB} credits per MB" def get_raw_price(self, state: dict) -> float: total_bytes = 0 diff --git a/recipes/LipsyncTTS.py b/recipes/LipsyncTTS.py index 279dd8b15..17a02d15b 100644 --- a/recipes/LipsyncTTS.py +++ b/recipes/LipsyncTTS.py @@ -167,15 +167,21 @@ def get_raw_price(self, state: dict): else: return LipsyncPage.get_raw_price(self, state) + def get_cost_note(self): + return "Lipsync cost + TTS cost" + def additional_notes(self): - lipsync_notes = LipsyncPage.additional_notes(self) - if tts_notes := TextToSpeechPage.additional_notes(self): - notes = f""" - - *Lipsync* {lipsync_notes.strip()} - - *TTS* {tts_notes.strip()} - """ - else: - notes = lipsync_notes + cost_notes = { + "Lipsync": LipsyncPage.get_cost_note(self), + "TTS": TextToSpeechPage.get_cost_note(self), + } + notes = "\n".join([ + f"- *{k} cost: {v.strip()}*" if v else "" + for k, v in cost_notes.items() + ]) + + notes += LipsyncPage.additional_notes(self) or "" + notes += TextToSpeechPage.additional_notes(self) or "" return notes diff --git a/recipes/TextToSpeech.py b/recipes/TextToSpeech.py index de8178cee..e1753bf1d 100644 --- a/recipes/TextToSpeech.py +++ b/recipes/TextToSpeech.py @@ -136,12 +136,10 @@ def _get_tts_provider(self, state: dict): # TODO: validate tts_provider before lookup? return TextToSpeechProviders[tts_provider] - def additional_notes(self): + def get_cost_note(self): tts_provider = st.session_state.get("tts_provider") if tts_provider == TextToSpeechProviders.ELEVEN_LABS.name: - return """ - *Eleven Labs cost ≈ 4 credits per 10 words* - """ + return "Eleven Labs ≈ 4 credits per 10 words" else: return "" diff --git a/recipes/asr.py b/recipes/asr.py index a11f4c762..3798683fa 100644 --- a/recipes/asr.py +++ b/recipes/asr.py @@ -169,10 +169,8 @@ def run(self, state: dict): # Save the raw ASR text for details view state["output_text"] = asr_output - def additional_notes(self) -> str | None: - return """ -*Cost ≈ 1 credit for 12.5 words ≈ 0.08 credits per word* - """ + def get_cost_note(self) -> str | None: + return "1 credit for 12.5 words ≈ 0.08 per word" def get_raw_price(self, state: dict): texts = state.get("output_text", []) From f5322d8b298d0f48dc1d9347dee711f64516f596 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:24:55 +0530 Subject: [PATCH 12/29] fix linting --- recipes/LipsyncTTS.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/recipes/LipsyncTTS.py b/recipes/LipsyncTTS.py index 17a02d15b..56a185f11 100644 --- a/recipes/LipsyncTTS.py +++ b/recipes/LipsyncTTS.py @@ -175,10 +175,9 @@ def additional_notes(self): "Lipsync": LipsyncPage.get_cost_note(self), "TTS": TextToSpeechPage.get_cost_note(self), } - notes = "\n".join([ - f"- *{k} cost: {v.strip()}*" if v else "" - for k, v in cost_notes.items() - ]) + notes = "\n".join( + [f"- *{k} cost: {v.strip()}*" if v else "" for k, v in cost_notes.items()] + ) notes += LipsyncPage.additional_notes(self) or "" notes += TextToSpeechPage.additional_notes(self) or "" From 54f343fe07b8d1904b6162550a0b6ec29505d38c Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:29:21 +0530 Subject: [PATCH 13/29] fix linting --- recipes/TextToSpeech.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/recipes/TextToSpeech.py b/recipes/TextToSpeech.py index 497fa87ba..4ba336c60 100644 --- a/recipes/TextToSpeech.py +++ b/recipes/TextToSpeech.py @@ -86,7 +86,9 @@ def before_render(self): state = st.session_state if self._get_tts_provider(state) == TextToSpeechProviders.ELEVEN_LABS: if state.get("__elevenlabs_use_custom_key") is None: - state["__elevenlabs_use_custom_key"] = bool(state.get("elevenlabs_voice_id")) + state["__elevenlabs_use_custom_key"] = bool( + state.get("elevenlabs_voice_id") + ) self.save_or_restore_elevenlabs_api_key(state) def render_description(self): @@ -268,7 +270,9 @@ def run(self, state: dict): case TextToSpeechProviders.ELEVEN_LABS: xi_api_key, is_custom_key = self._get_elevenlabs_api_key(state) assert ( - is_custom_key or self.is_current_user_paying() or self.is_current_user_admin() + is_custom_key + or self.is_current_user_paying() + or self.is_current_user_admin() ), """ Please purchase Gooey.AI credits to use ElevenLabs voices here. """ @@ -358,7 +362,9 @@ def save_or_restore_elevenlabs_api_key(self, state: dict): # elevenlabs_api_key is in state, so save it to session if (new_value := state.get("elevenlabs_api_key")) is not None: self._save_to_cookie("elevenlabs_api_key", new_value) - elif state.get("__elevenlabs_use_custom_key") is True or state.get("elevenlabs_voice_id"): + elif state.get("__elevenlabs_use_custom_key") is True or state.get( + "elevenlabs_voice_id" + ): state["elevenlabs_api_key"] = self._fetch_from_cookie("elevenlabs_api_key") def _save_to_cookie(self, key: str, value: str, store_name: str = "state") -> None: From 7e821bf1a367db2e4965a32f996037c55720abcf Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Thu, 9 Nov 2023 18:16:35 +0530 Subject: [PATCH 14/29] Make zero-margin for caption in sticky submit section --- daras_ai_v2/base.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index bb81ce045..578c67e65 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -485,11 +485,15 @@ def render_submit_button(self, key="--submit-1"): if cost_note: cost_note = f"({cost_note.strip()})" st.caption( - f"Run cost = [{self.get_price_roundoff(st.session_state)} credits]({self.get_credits_click_url()})" - f" {cost_note}" + f""" +

+ Run cost = {self.get_price_roundoff(st.session_state)} credits + {cost_note} +

+ """, + unsafe_allow_html=True, ) - additional_notes = self.additional_notes() - if additional_notes: + if additional_notes := self.additional_notes(): st.caption(additional_notes) with col2: submitted = st.button( From e349e5477f8d1160d44e5faa54b43772f0577bc1 Mon Sep 17 00:00:00 2001 From: Alexander Metzger Date: Thu, 9 Nov 2023 12:13:56 -0800 Subject: [PATCH 15/29] simplify accept --- recipes/asr.py | 76 +------------------------------------------------- 1 file changed, 1 insertion(+), 75 deletions(-) diff --git a/recipes/asr.py b/recipes/asr.py index 120871f10..4d9d2b86c 100644 --- a/recipes/asr.py +++ b/recipes/asr.py @@ -80,81 +80,7 @@ def related_workflows(self) -> list: def render_form_v2(self): document_uploader( "##### Audio Files", - accept=( - ".aa", # Audio formats that ffmpeg can demux and decode (https://ffmpeg.org/ffmpeg-formats.html) - ".amr", - ".aac", # aac demuxer - ".m4p", - ".m4r", - ".mp3", - ".asf", # asf demuxer - ".wma", - ".wmv", - ".xml", # imf demuxer - ".mxf", - ".flv", # flv, live_flv, kux demuxer - ".f4v", - ".f4p", - ".f4a", - ".f4b", - ".m3u8", # HLS demuxer - ".mov", # Demuxer for Quicktime File Format & ISO/IEC Base Media File Format (ISO/IEC 14496-12 or MPEG-4 Part 12, ISO/IEC 15444-12 or JPEG 2000 Part 12) - ".mp4", - ".m4a", - ".3gp", - ".3g2", - ".mj2", - ".psp", - ".m4b", - ".ism", - ".ismv", - ".isma", - ".f4v", - ".aax", # Audible AAX - ".aiff", # Audio Interchange File Format - ".aif", - ".aifc", - ".tun", # Muxer for audio of High Voltage Software’s Lego Racers game - ".pcm", - ".ts", # MPEG Transport Stream - ".tsv", - ".tsa", - ".m2t", - ".ogv", # Ogg - ".oga", - ".ogx", - ".ogg", - ".opus", - ".ac3", # Dolby Digital - ".adx", # CRI ADX - ".aptx", # aptX - ".aptxhd", - ".avs", # AVS2-P2/IEEE1857.4 video. - ".avs2", - ".cavs", # Chinese AVS (Audio Video Standard) video - ".drc", # BBC Dirac video. - ".vc2", - ".dnxhd", # Avid DNxHD video - ".dnxhr", - ".evc", # MPEG-5 Essential Video Coding (EVC) / EVC / MPEG-5 Part 1 EVC video. - ".tco", # ITU-T G.723.1 audio. - ".rco", - ".h264", # ITU-T H.264 / MPEG-4 Part 10 AVC video. - ".264", - ".hevc", # ITU-T H.265 / MPEG-H Part 2 HEVC video - ".h265", - ".265", - ".mp2", # MPEG-1 Audio Layer II - ".m2a", - ".mpa", - ".yuv", # raw video - ".rgb", - ".sbc", # SBC (low-complexity subband codec) audio - ".msbc", - ".thd", # Dolby TrueHD audio - ".webm", # WebM - ".wav", # Waveform Audio File Format - ), + accept=("audio/*", "video/*", "application/octet-stream"), ) col1, col2 = st.columns(2, responsive=False) with col1: From cb1b552c22973b826f88139aa72f4b11fcd82ddc Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Fri, 10 Nov 2023 19:16:56 +0530 Subject: [PATCH 16/29] update openai, add dall-e-3 --- daras_ai_v2/language_model.py | 50 ++++++++++++++++++--------------- daras_ai_v2/query_generator.py | 2 +- daras_ai_v2/stable_diffusion.py | 39 +++++++++++++++++-------- poetry.lock | 35 +++++++++++++++-------- pyproject.toml | 2 +- recipes/ChyronPlant.py | 22 ++++++++------- 6 files changed, 93 insertions(+), 57 deletions(-) diff --git a/daras_ai_v2/language_model.py b/daras_ai_v2/language_model.py index f928670f3..d12abcf44 100644 --- a/daras_ai_v2/language_model.py +++ b/daras_ai_v2/language_model.py @@ -1,7 +1,6 @@ import hashlib import io import re -import threading import typing from enum import Enum from functools import wraps @@ -9,7 +8,6 @@ import numpy as np import requests -import tiktoken import typing_extensions from django.conf import settings from jinja2.lexer import whitespace_re @@ -38,8 +36,8 @@ class LLMApis(Enum): class LargeLanguageModels(Enum): - gpt_4 = "GPT-4 (openai)" gpt_4_turbo = "GPT-4 Turbo (openai)" + gpt_4 = "GPT-4 (openai)" gpt_3_5_turbo = "ChatGPT (openai)" gpt_3_5_turbo_16k = "ChatGPT 16k (openai)" @@ -71,7 +69,7 @@ def is_chat_model(self) -> bool: ] -engine_names = { +llm_model_names = { LargeLanguageModels.gpt_4: "gpt-4", LargeLanguageModels.gpt_4_turbo: "gpt-4-1106-preview", LargeLanguageModels.gpt_3_5_turbo: "gpt-3.5-turbo", @@ -155,14 +153,17 @@ def calc_gpt_tokens( def get_openai_error_cls(): - import openai.error + import openai + + class ServiceUnavailableError(openai.APIStatusError): + status_code = 503 return ( - openai.error.Timeout, - openai.error.APIError, - openai.error.APIConnectionError, - openai.error.RateLimitError, - openai.error.ServiceUnavailableError, + openai.APITimeoutError, + openai.APIError, + openai.APIConnectionError, + openai.RateLimitError, + ServiceUnavailableError, ) @@ -250,10 +251,11 @@ def np_dumps(a: np.ndarray) -> bytes: def _openai_embedding_create( *, input: list[str], model: str = "text-embedding-ada-002" ) -> np.ndarray: - import openai + from openai import OpenAI - res = openai.Embedding.create(model=model, input=input) - ret = np.array([data["embedding"] for data in res["data"]]) # type: ignore + client = OpenAI() + res = client.embeddings.create(model=model, input=input) + ret = np.array([data.embedding for data in res.data]) # see - https://community.openai.com/t/text-embedding-ada-002-embeddings-sometime-return-nan/279664/5 if np.isnan(ret).any(): @@ -301,7 +303,7 @@ def run_language_model( is_chatml, messages = parse_chatml(prompt) # type: ignore result = _run_chat_model( api=api, - engine=engine_names[model], + engine=llm_model_names[model], messages=messages or [], # type: ignore max_tokens=max_tokens, num_outputs=num_outputs, @@ -343,10 +345,11 @@ def _run_text_model( ) -> list[str]: match api: case LLMApis.openai: - import openai + from openai import OpenAI - r = openai.Completion.create( - engine=engine_names[model], + client = OpenAI() + r = client.completions.create( + model=llm_model_names[model], prompt=prompt, max_tokens=max_tokens, stop=stop, @@ -356,10 +359,10 @@ def _run_text_model( frequency_penalty=0.1 if avoid_repetition else 0, presence_penalty=0.25 if avoid_repetition else 0, ) - return [choice["text"] for choice in r["choices"]] + return [choice.text for choice in r.choices] case LLMApis.vertex_ai: return _run_palm_text( - model_id=engine_names[model], + model_id=llm_model_names[model], prompt=prompt, max_output_tokens=min(max_tokens, 1024), # because of Vertex AI limits candidate_count=num_outputs, @@ -382,9 +385,12 @@ def _run_chat_model( ) -> list[ConversationEntry]: match api: case LLMApis.openai: - import openai + from openai import OpenAI + + print([len(messages), max_tokens, num_outputs, messages]) - r = openai.ChatCompletion.create( + client = OpenAI() + r = client.chat.completions.create( model=engine, messages=messages, max_tokens=max_tokens, @@ -394,7 +400,7 @@ def _run_chat_model( frequency_penalty=0.1 if avoid_repetition else 0, presence_penalty=0.25 if avoid_repetition else 0, ) - return [choice["message"] for choice in r["choices"]] + return [choice.message.dict() for choice in r.choices] case LLMApis.vertex_ai: return _run_palm_chat( model_id=engine, diff --git a/daras_ai_v2/query_generator.py b/daras_ai_v2/query_generator.py index 849836e9b..2769d273c 100644 --- a/daras_ai_v2/query_generator.py +++ b/daras_ai_v2/query_generator.py @@ -23,7 +23,7 @@ def generate_final_search_query( if not instructions: return "" model = LargeLanguageModels[request.selected_model] - max_tokens = model_max_tokens[model] // 8 # just a sane default + max_tokens = min(model_max_tokens[model] // 8, 1024) # just a sane default return run_language_model( model=request.selected_model, prompt=instructions, diff --git a/daras_ai_v2/stable_diffusion.py b/daras_ai_v2/stable_diffusion.py index a78e962bd..4e5b09b0d 100644 --- a/daras_ai_v2/stable_diffusion.py +++ b/daras_ai_v2/stable_diffusion.py @@ -50,7 +50,8 @@ class Text2ImgModels(Enum): analog_diffusion = "Analog Diffusion (wavymulder)" protogen_5_3 = "Protogen v5.3 (darkstorm2150)" dreamlike_2 = "Dreamlike Photoreal 2.0 (dreamlike.art)" - dall_e = "Dall-E (OpenAI)" + dall_e = "DALL·E 2 (OpenAI)" + dall_e_3 = "DALL·E 3 (OpenAI)" jack_qiao = "Stable Diffusion v1.4 [Deprecated] (Jack Qiao)" deepfloyd_if = "DeepFloyd IF [Deprecated] (stability.ai)" @@ -70,6 +71,8 @@ def _deprecated(cls): Text2ImgModels.openjourney_2: "prompthero/openjourney-v2", Text2ImgModels.dreamlike_2: "dreamlike-art/dreamlike-photoreal-2.0", Text2ImgModels.protogen_5_3: "darkstorm2150/Protogen_v5.3_Official_Release", + Text2ImgModels.dall_e: "dall-e-2", + Text2ImgModels.dall_e_3: "dall-e-3", } @@ -268,17 +271,29 @@ def text2img( _resolution_check(width, height, max_size=(1024, 1024)) match selected_model: + case Text2ImgModels.dall_e_3.name: + from openai import OpenAI + + client = OpenAI() + response = client.images.generate( + model=text2img_model_ids[Text2ImgModels[selected_model]], + n=num_outputs, + prompt=prompt, + response_format="b64_json", + ) + out_imgs = [b64_img_decode(part.b64_json) for part in response.data] case Text2ImgModels.dall_e.name: - import openai + from openai import OpenAI edge = _get_dalle_img_size(width, height) - response = openai.Image.create( + client = OpenAI() + response = client.images.generate( n=num_outputs, prompt=prompt, size=f"{edge}x{edge}", response_format="b64_json", ) - out_imgs = [b64_img_decode(part["b64_json"]) for part in response["data"]] + out_imgs = [b64_img_decode(part.b64_json) for part in response.data] case _: prompt = add_prompt_prefix(prompt, selected_model) return call_sd_multi( @@ -338,12 +353,13 @@ def img2img( match selected_model: case Img2ImgModels.dall_e.name: - import openai + from openai import OpenAI edge = _get_dalle_img_size(width, height) image = resize_img_pad(init_image_bytes, (edge, edge)) - response = openai.Image.create_variation( + client = OpenAI() + response = client.images.create_variation( image=image, n=num_outputs, size=f"{edge}x{edge}", @@ -351,8 +367,8 @@ def img2img( ) out_imgs = [ - resize_img_fit(b64_img_decode(part["b64_json"]), (width, height)) - for part in response["data"] + resize_img_fit(b64_img_decode(part.b64_json), (width, height)) + for part in response.data ] case _: prompt = add_prompt_prefix(prompt, selected_model) @@ -457,21 +473,22 @@ def inpainting( match selected_model: case InpaintingModels.dall_e.name: - import openai + from openai import OpenAI edge = _get_dalle_img_size(width, height) edit_image_bytes = resize_img_pad(edit_image_bytes, (edge, edge)) mask_bytes = resize_img_pad(mask_bytes, (edge, edge)) image = rgb_img_to_rgba(edit_image_bytes, mask_bytes) - response = openai.Image.create_edit( + client = OpenAI() + response = client.images.edit( prompt=prompt, image=image, n=num_outputs, size=f"{edge}x{edge}", response_format="b64_json", ) - out_imgs = [b64_img_decode(part["b64_json"]) for part in response["data"]] + out_imgs = [b64_img_decode(part.b64_json) for part in response.data] case InpaintingModels.sd_2.name | InpaintingModels.runway_ml.name: out_imgs_urls = call_sd_multi( diff --git a/poetry.lock b/poetry.lock index 0e2fdb33b..cd6e44713 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1040,6 +1040,17 @@ files = [ aiohttp = "*" websockets = "*" +[[package]] +name = "distro" +version = "1.8.0" +description = "Distro - an OS platform information API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "distro-1.8.0-py3-none-any.whl", hash = "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff"}, + {file = "distro-1.8.0.tar.gz", hash = "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8"}, +] + [[package]] name = "django" version = "4.2.5" @@ -3344,25 +3355,25 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "openai" -version = "0.27.10" -description = "Python client library for the OpenAI API" +version = "1.2.2" +description = "Client library for the openai API" optional = false python-versions = ">=3.7.1" files = [ - {file = "openai-0.27.10-py3-none-any.whl", hash = "sha256:beabd1757e3286fa166dde3b70ebb5ad8081af046876b47c14c41e203ed22a14"}, - {file = "openai-0.27.10.tar.gz", hash = "sha256:60e09edf7100080283688748c6803b7b3b52d5a55d21890f3815292a0552d83b"}, + {file = "openai-1.2.2-py3-none-any.whl", hash = "sha256:e9238e506b8ee8fc00af5b656de7907986317d7cfcf581964ff6e54a449be727"}, + {file = "openai-1.2.2.tar.gz", hash = "sha256:cbeff4eaf6cdcdccb24c9190d9880a827c034b221ed996201c10d578850b3db8"}, ] [package.dependencies] -aiohttp = "*" -requests = ">=2.20" -tqdm = "*" +anyio = ">=3.5.0,<4" +distro = ">=1.7.0,<2" +httpx = ">=0.23.0,<1" +pydantic = ">=1.9.0,<3" +tqdm = ">4" +typing-extensions = ">=4.5,<5" [package.extras] -datalib = ["numpy", "openpyxl (>=3.0.7)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] -dev = ["black (>=21.6b0,<22.0)", "pytest (==6.*)", "pytest-asyncio", "pytest-mock"] -embeddings = ["matplotlib", "numpy", "openpyxl (>=3.0.7)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)", "plotly", "scikit-learn (>=1.0.2)", "scipy", "tenacity (>=8.0.1)"] -wandb = ["numpy", "openpyxl (>=3.0.7)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)", "wandb"] +datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] [[package]] name = "openapi-schema-pydantic" @@ -6185,4 +6196,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "8e790e1ba90843a0ba41bc725d5310a0acb4d69c8d4e02a752d242e2e7909c50" +content-hash = "14753f1302e71690146109f96bd407f41ad2cd99601d9dae54a6d25f5081e3ba" diff --git a/pyproject.toml b/pyproject.toml index 81368d8f4..f4eb3f519 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ readme = "README.md" [tool.poetry.dependencies] python = ">=3.10,<3.13" streamlit = "^1.15.2" -openai = "^0.27.0" +openai = "^1.2.0" python-decouple = "^3.6" requests = "^2.28.1" glom = "^22.1.0" diff --git a/recipes/ChyronPlant.py b/recipes/ChyronPlant.py index 7bc5bbe6a..b4d3af5c0 100644 --- a/recipes/ChyronPlant.py +++ b/recipes/ChyronPlant.py @@ -83,13 +83,14 @@ def run(self, state: dict): state["chyron_output"] = self.run_chyron(state) def run_midi_notes(self, state: dict): - import openai + from openai import OpenAI prompt = state.get("midi_notes_prompt", "").strip() prompt += "\nMIDI: " + state.get("midi_notes", "") + "\nEnglish:" - r = openai.Completion.create( - engine="text-davinci-002", + client = OpenAI() + r = client.completions.create( + model="text-davinci-002", max_tokens=250, prompt=prompt, stop=["English:", "MIDI:", "\n"], @@ -97,20 +98,21 @@ def run_midi_notes(self, state: dict): n=1, ) # choose the first completion that isn't empty - for choice in r["choices"]: - text = choice["text"].strip() + for choice in r.choices: + text = choice.text.strip() if text: return text return "" def run_chyron(self, state: dict): - import openai + from openai import OpenAI prompt = state.get("chyron_prompt", "").strip() prompt += "\nUser: " + state.get("midi_translation", "").strip() + "\nChyron:" - r = openai.Completion.create( - engine="text-davinci-002", + client = OpenAI() + r = client.completions.create( + model="text-davinci-002", max_tokens=250, prompt=prompt, stop=["\nChyron:", "\nUser:"], @@ -118,8 +120,8 @@ def run_chyron(self, state: dict): n=1, ) # choose the first completion that isn't empty - for choice in r["choices"]: - text = choice["text"].strip() + for choice in r.choices: + text = choice.text.strip() if text: return text return "" From 2f4d1c8ab04f39ca5dba14a392e403eeb1219cae Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Fri, 10 Nov 2023 19:48:20 +0530 Subject: [PATCH 17/29] refactor --- daras_ai_v2/base.py | 10 --- .../text_to_speech_settings_widgets.py | 73 +++++++++++-------- recipes/TextToSpeech.py | 34 --------- recipes/VideoBots.py | 4 - 4 files changed, 43 insertions(+), 78 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index dae5253d8..a1d01cadd 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -91,7 +91,6 @@ class BasePage: slug_versions: list[str] sane_defaults: dict = {} - private_fields: list = [] RequestModel: typing.Type[BaseModel] ResponseModel: typing.Type[BaseModel] @@ -148,16 +147,7 @@ def api_url(self, example_id=None, run_id=None, uid=None) -> furl: def endpoint(self) -> str: return f"/v2/{self.slug_versions[0]}/" - def before_render(self): - """ - Side-effects to apply before doing the actual render. - This shouldn't actually render anything to the page. - """ - pass - def render(self): - self.before_render() - with sentry_sdk.configure_scope() as scope: scope.set_extra("base_url", self.app_url()) scope.set_transaction_name( diff --git a/daras_ai_v2/text_to_speech_settings_widgets.py b/daras_ai_v2/text_to_speech_settings_widgets.py index 047f49a5c..110edc279 100644 --- a/daras_ai_v2/text_to_speech_settings_widgets.py +++ b/daras_ai_v2/text_to_speech_settings_widgets.py @@ -1,13 +1,14 @@ -import hashlib from enum import Enum import requests -import gooey_ui as st from google.cloud import texttospeech +import gooey_ui as st from daras_ai_v2.enum_selector_widget import enum_selector from daras_ai_v2.redis_cache import redis_cache_decorator +SESSION_ELEVENLABS_API_KEY = "__user__elevenlabs_api_key" + UBERDUCK_VOICES = { "Aiden Botha": "b01cf18d-0f10-46dd-adc6-562b599fdae4", "Angus": "7d29a280-8a3e-4c4b-9df4-cbe77e8f4a63", @@ -137,7 +138,7 @@ class TextToSpeechProviders(Enum): } -def text_to_speech_settings(page=None): +def text_to_speech_settings(page): st.write( """ ##### 🗣️ Voice Settings @@ -229,9 +230,14 @@ def text_to_speech_settings(page=None): case TextToSpeechProviders.ELEVEN_LABS.name: with col2: + if not st.session_state.get("elevenlabs_api_key"): + st.session_state["elevenlabs_api_key"] = page.request.session.get( + SESSION_ELEVENLABS_API_KEY + ) + elevenlabs_use_custom_key = st.checkbox( "Use custom API key + Voice ID", - key="__elevenlabs_use_custom_key", + value=bool(st.session_state.get("elevenlabs_api_key")), ) if elevenlabs_use_custom_key: st.session_state["elevenlabs_voice_name"] = None @@ -254,8 +260,8 @@ def text_to_speech_settings(page=None): if elevenlabs_api_key: try: - elevenlabs_voices = get_cached_elevenlabs_voices( - st.session_state, elevenlabs_api_key + elevenlabs_voices = fetch_elevenlabs_voices( + elevenlabs_api_key ) except requests.exceptions.HTTPError as e: st.error( @@ -271,6 +277,8 @@ def text_to_speech_settings(page=None): format_func=elevenlabs_voices.__getitem__, ) else: + st.session_state["elevenlabs_api_key"] = None + st.session_state["elevenlabs_voice_id"] = None if not ( page and ( @@ -298,6 +306,10 @@ def text_to_speech_settings(page=None): options=ELEVEN_LABS_VOICES.keys(), ) + page.request.session[SESSION_ELEVENLABS_API_KEY] = st.session_state.get( + "elevenlabs_api_key" + ) + st.selectbox( """ ###### Voice Model @@ -359,30 +371,6 @@ def _pretty_voice(voice) -> str: return f"{voice.name} ({voice.ssml_gender.name.capitalize()})" -def get_cached_elevenlabs_voices(state, api_key) -> dict[str, str]: - api_key_hash = hashlib.sha256(api_key.encode("utf-8")).hexdigest()[:40] - state.setdefault("__elevenlabs_voices_cache", {}) - if api_key_hash not in state["__elevenlabs_voices_cache"]: - state["__elevenlabs_voices_cache"] = { - api_key_hash: fetch_elevenlabs_voices(api_key) - } - return state["__elevenlabs_voices_cache"][api_key_hash] - - -def fetch_elevenlabs_voices(api_key: str) -> dict[str, str]: - r = requests.get( - "https://api.elevenlabs.io/v1/voices", - headers={"Accept": "application/json", "xi-api-key": api_key}, - ) - r.raise_for_status() - sorted_voices = sorted( - r.json()["voices"], - key=lambda v: (int(v["category"] == "premade"), v["name"]), - ) - describe_voice = lambda v: ", ".join(v["labels"].values()) - return {v["voice_id"]: f"{v['name']} - {describe_voice(v)}" for v in sorted_voices} - - _lang_code_sort = ["en-US", "en-IN", "en-GB", "en-AU"] @@ -399,3 +387,28 @@ def _voice_sort_key(voice: texttospeech.Voice): # sort alphabetically voice.name, ) + + +_elevenlabs_category_order = { + "cloned": 1, + "generated": 2, + "premade": 3, +} + + +@st.cache_in_session_state +def fetch_elevenlabs_voices(api_key: str) -> dict[str, str]: + r = requests.get( + "https://api.elevenlabs.io/v1/voices", + headers={"Accept": "application/json", "xi-api-key": api_key}, + ) + r.raise_for_status() + print(r.json()["voices"]) + sorted_voices = sorted( + r.json()["voices"], + key=lambda v: (_elevenlabs_category_order.get(v["category"], 0), v["name"]), + ) + return { + v["voice_id"]: " - ".join([v["name"], *v["labels"].values()]) + for v in sorted_voices + } diff --git a/recipes/TextToSpeech.py b/recipes/TextToSpeech.py index 4ba336c60..16677d788 100644 --- a/recipes/TextToSpeech.py +++ b/recipes/TextToSpeech.py @@ -80,17 +80,6 @@ def fallback_preivew_image(self) -> str | None: def preview_description(self, state: dict) -> str: return "Input your text, pick a voice & a Text-to-Speech AI engine to create audio. Compare the best voice generators from Google, UberDuck.ai & more to add automated voices to your podcast, YouTube videos, website, or app." - def before_render(self): - super().before_render() - - state = st.session_state - if self._get_tts_provider(state) == TextToSpeechProviders.ELEVEN_LABS: - if state.get("__elevenlabs_use_custom_key") is None: - state["__elevenlabs_use_custom_key"] = bool( - state.get("elevenlabs_voice_id") - ) - self.save_or_restore_elevenlabs_api_key(state) - def render_description(self): st.write( """ @@ -357,26 +346,3 @@ def render_example(self, state: dict): audio_url = state.get("audio_url") if audio_url: st.audio(audio_url) - - def save_or_restore_elevenlabs_api_key(self, state: dict): - # elevenlabs_api_key is in state, so save it to session - if (new_value := state.get("elevenlabs_api_key")) is not None: - self._save_to_cookie("elevenlabs_api_key", new_value) - elif state.get("__elevenlabs_use_custom_key") is True or state.get( - "elevenlabs_voice_id" - ): - state["elevenlabs_api_key"] = self._fetch_from_cookie("elevenlabs_api_key") - - def _save_to_cookie(self, key: str, value: str, store_name: str = "state") -> None: - if value is None: - # delete key - self.request.session.setdefault(store_name, {}) - self.request.session[store_name].pop(key, None) - elif self._fetch_from_cookie(key) != value: - # current value != new value, so update - self.request.session.setdefault(store_name, {}) - self.request.session[store_name][key] = value - - def _fetch_from_cookie(self, key: str, store_name: str = "state") -> str | None: - # returns None if value not found - return self.request.session.get(store_name, {}).get(key) diff --git a/recipes/VideoBots.py b/recipes/VideoBots.py index 5a93ce1c0..26f3feedc 100644 --- a/recipes/VideoBots.py +++ b/recipes/VideoBots.py @@ -275,10 +275,6 @@ class ResponseModel(BaseModel): def preview_image(self, state: dict) -> str | None: return DEFAULT_COPILOT_META_IMG - def before_render(self): - super().before_render() - TextToSpeechPage(request=self.request).before_render() - def related_workflows(self): from recipes.LipsyncTTS import LipsyncTTSPage from recipes.CompareText2Img import CompareText2ImgPage From f7d6cf3da506f575b68b840326bc4c5712cf0919 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Fri, 10 Nov 2023 19:51:37 +0530 Subject: [PATCH 18/29] remove debug line --- daras_ai_v2/language_model.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/daras_ai_v2/language_model.py b/daras_ai_v2/language_model.py index d12abcf44..6d75b7396 100644 --- a/daras_ai_v2/language_model.py +++ b/daras_ai_v2/language_model.py @@ -387,8 +387,6 @@ def _run_chat_model( case LLMApis.openai: from openai import OpenAI - print([len(messages), max_tokens, num_outputs, messages]) - client = OpenAI() r = client.chat.completions.create( model=engine, From 31a4372a5c357b1558854e5836114f8fbd03c799 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Fri, 10 Nov 2023 20:01:36 +0530 Subject: [PATCH 19/29] fix 11labs cost line --- recipes/TextToSpeech.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/recipes/TextToSpeech.py b/recipes/TextToSpeech.py index 16677d788..dfa5da82b 100644 --- a/recipes/TextToSpeech.py +++ b/recipes/TextToSpeech.py @@ -153,13 +153,9 @@ def additional_notes(self): if tts_provider == TextToSpeechProviders.ELEVEN_LABS.name: _, is_user_provided_key = self._get_elevenlabs_api_key(st.session_state) if is_user_provided_key: - return """ - *Eleven Labs cost ≈ No additional credit charge given we'll use your API key* - """ + return "*Eleven Labs cost ≈ No additional credit charge given we'll use your API key*" else: - return """ - *Eleven Labs cost ≈ 4 credits per 10 words* - """ + return "*Eleven Labs cost ≈ 4 credits per 10 words*" else: return "" From 9386cfbd334f9da01e554b61f69232f0d78e259c Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Sat, 11 Nov 2023 02:06:32 +0530 Subject: [PATCH 20/29] dall-e-3 pricing --- recipes/CompareText2Img.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/CompareText2Img.py b/recipes/CompareText2Img.py index 31239baa3..05fd37057 100644 --- a/recipes/CompareText2Img.py +++ b/recipes/CompareText2Img.py @@ -241,7 +241,7 @@ def get_raw_price(self, state: dict) -> int: match name: case Text2ImgModels.deepfloyd_if.name: total += 5 - case Text2ImgModels.dall_e.name: + case Text2ImgModels.dall_e.name | Text2ImgModels.dall_e_3.name: total += 15 case _: total += 2 From 05489175cc125b07c967e4a2f8cba2b6feeb6e09 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Sun, 12 Nov 2023 17:39:20 +0530 Subject: [PATCH 21/29] right align privacy and terms fix alignment of run cost --- daras_ai_v2/base.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index 578c67e65..e74acc160 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -486,15 +486,11 @@ def render_submit_button(self, key="--submit-1"): cost_note = f"({cost_note.strip()})" st.caption( f""" -

- Run cost = {self.get_price_roundoff(st.session_state)} credits - {cost_note} -

+Run cost = {self.get_price_roundoff(st.session_state)} credits {cost_note} +{self.additional_notes() or ""} """, unsafe_allow_html=True, ) - if additional_notes := self.additional_notes(): - st.caption(additional_notes) with col2: submitted = st.button( "🏃 Submit", @@ -640,10 +636,11 @@ def _render_input_col(self): st.text_input("Title", key=StateKeys.page_title) st.text_area("Notes", key=StateKeys.page_notes) submitted = self.render_submit_button() - st.caption( - "_By submitting, you agree to Gooey.AI's [terms](https://gooey.ai/terms) & " - "[privacy policy](https://gooey.ai/privacy)._" - ) + with st.div(style={"textAlign": "right"}): + st.caption( + "_By submitting, you agree to Gooey.AI's [terms](https://gooey.ai/terms) & " + "[privacy policy](https://gooey.ai/privacy)._" + ) return submitted def _render_output_col(self, submitted: bool): From 27dccaa6f96ce0b81fe3ebcfe89581a24a23177f Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 8 Nov 2023 20:17:57 +0530 Subject: [PATCH 22/29] move t&c out of sticky container --- daras_ai_v2/base.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index 4c8af2d8f..21e61e076 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -479,10 +479,12 @@ def render_submit_button(self, key="--submit-1"): col2.node.props[ "className" ] += " d-flex justify-content-end align-items-center" + col1.node.props[ + "className" + ] += " d-flex flex-column justify-content-center" with col1: st.caption( - f"Run cost = [{self.get_price_roundoff(st.session_state)} credits]({self.get_credits_click_url()}) \\\n" - f"_By submitting, you agree to Gooey.AI's [terms](https://gooey.ai/terms) & [privacy policy](https://gooey.ai/privacy)._ ", + f"Run cost = [{self.get_price_roundoff(st.session_state)} credits]({self.get_credits_click_url()})" ) additional_notes = self.additional_notes() if additional_notes: @@ -632,6 +634,10 @@ def _render_input_col(self): st.text_input("Title", key=StateKeys.page_title) st.text_area("Notes", key=StateKeys.page_notes) submitted = self.render_submit_button() + st.caption( + "_By submitting, you agree to Gooey.AI's [terms](https://gooey.ai/terms) &" + "[privacy policy](https://gooey.ai/privacy)._" + ) return submitted def _render_output_col(self, submitted: bool): From b851c777575d55b1c5a96e2e2c63713b5c21b127 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 8 Nov 2023 20:41:47 +0530 Subject: [PATCH 23/29] run black formatter --- daras_ai_v2/base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index 21e61e076..86c7bfd6a 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -479,9 +479,7 @@ def render_submit_button(self, key="--submit-1"): col2.node.props[ "className" ] += " d-flex justify-content-end align-items-center" - col1.node.props[ - "className" - ] += " d-flex flex-column justify-content-center" + col1.node.props["className"] += " d-flex flex-column justify-content-center" with col1: st.caption( f"Run cost = [{self.get_price_roundoff(st.session_state)} credits]({self.get_credits_click_url()})" From f08ce1d02ca9721a98fadf52fac81908c9998ea0 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:20:39 +0530 Subject: [PATCH 24/29] Add cost note for presenting text in parens after showing run cost --- daras_ai_v2/base.py | 9 ++++++++- recipes/DeforumSD.py | 8 ++++---- recipes/Lipsync.py | 6 ++---- recipes/LipsyncTTS.py | 22 ++++++++++++++-------- recipes/TextToSpeech.py | 6 +++--- recipes/asr.py | 6 ++---- 6 files changed, 33 insertions(+), 24 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index 86c7bfd6a..13c3ee662 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -481,8 +481,12 @@ def render_submit_button(self, key="--submit-1"): ] += " d-flex justify-content-end align-items-center" col1.node.props["className"] += " d-flex flex-column justify-content-center" with col1: + cost_note = self.get_cost_note() or "" + if cost_note: + cost_note = f"({cost_note.strip()})" st.caption( f"Run cost = [{self.get_price_roundoff(st.session_state)} credits]({self.get_credits_click_url()})" + f" {cost_note}" ) additional_notes = self.additional_notes() if additional_notes: @@ -633,7 +637,7 @@ def _render_input_col(self): st.text_area("Notes", key=StateKeys.page_notes) submitted = self.render_submit_button() st.caption( - "_By submitting, you agree to Gooey.AI's [terms](https://gooey.ai/terms) &" + "_By submitting, you agree to Gooey.AI's [terms](https://gooey.ai/terms) & " "[privacy policy](https://gooey.ai/privacy)._" ) return submitted @@ -1145,6 +1149,9 @@ def get_example_response_body( def additional_notes(self) -> str | None: pass + def get_cost_note(self) -> str | None: + pass + def is_current_user_admin(self) -> bool: if not self.request or not self.request.user: return False diff --git a/recipes/DeforumSD.py b/recipes/DeforumSD.py index c2ebd231c..b5ab1c327 100644 --- a/recipes/DeforumSD.py +++ b/recipes/DeforumSD.py @@ -229,11 +229,11 @@ def render_form_v2(self): """ ) + def get_cost_note(self) -> str | None: + return f"{CREDITS_PER_FRAME} / frame" + def additional_notes(self) -> str | None: - return f""" -*Cost ≈ {CREDITS_PER_FRAME} credits per frame* \\ -*Process Run Time ≈ 5 seconds per frame* - """ + return "Render Time ≈ 3s / frame" def get_raw_price(self, state: dict) -> float: max_frames = state.get("max_frames", 100) or 0 diff --git a/recipes/Lipsync.py b/recipes/Lipsync.py index 44e2af76f..68ac496a0 100644 --- a/recipes/Lipsync.py +++ b/recipes/Lipsync.py @@ -126,10 +126,8 @@ def render_usage_guide(self): def preview_description(self, state: dict) -> str: return "Create high-quality, realistic Lipsync animations from any audio file. Input a sample face gif/video + audio and we will automatically generate a lipsync animation that matches your audio." - def additional_notes(self) -> str | None: - return f""" - *Cost ≈ {CREDITS_PER_MB} credits per MB* - """ + def get_cost_note(self) -> str | None: + return f"{CREDITS_PER_MB} credits per MB" def get_raw_price(self, state: dict) -> float: total_bytes = 0 diff --git a/recipes/LipsyncTTS.py b/recipes/LipsyncTTS.py index cc900c0c1..132872bf0 100644 --- a/recipes/LipsyncTTS.py +++ b/recipes/LipsyncTTS.py @@ -169,15 +169,21 @@ def get_raw_price(self, state: dict): else: return LipsyncPage.get_raw_price(self, state) + def get_cost_note(self): + return "Lipsync cost + TTS cost" + def additional_notes(self): - lipsync_notes = LipsyncPage.additional_notes(self) - if tts_notes := TextToSpeechPage.additional_notes(self): - notes = f""" - - *Lipsync* {lipsync_notes.strip()} - - *TTS* {tts_notes.strip()} - """ - else: - notes = lipsync_notes + cost_notes = { + "Lipsync": LipsyncPage.get_cost_note(self), + "TTS": TextToSpeechPage.get_cost_note(self), + } + notes = "\n".join([ + f"- *{k} cost: {v.strip()}*" if v else "" + for k, v in cost_notes.items() + ]) + + notes += LipsyncPage.additional_notes(self) or "" + notes += TextToSpeechPage.additional_notes(self) or "" return notes diff --git a/recipes/TextToSpeech.py b/recipes/TextToSpeech.py index dfa5da82b..0df5d1d3d 100644 --- a/recipes/TextToSpeech.py +++ b/recipes/TextToSpeech.py @@ -148,14 +148,14 @@ def _get_tts_provider(self, state: dict): # TODO: validate tts_provider before lookup? return TextToSpeechProviders[tts_provider] - def additional_notes(self): + def get_cost_note(self): tts_provider = st.session_state.get("tts_provider") if tts_provider == TextToSpeechProviders.ELEVEN_LABS.name: _, is_user_provided_key = self._get_elevenlabs_api_key(st.session_state) if is_user_provided_key: - return "*Eleven Labs cost ≈ No additional credit charge given we'll use your API key*" + return "*Eleven Labs ≈ No additional credit charge given we'll use your API key*" else: - return "*Eleven Labs cost ≈ 4 credits per 10 words*" + return "*Eleven Labs ≈ 4 credits per 10 words*" else: return "" diff --git a/recipes/asr.py b/recipes/asr.py index a11f4c762..3798683fa 100644 --- a/recipes/asr.py +++ b/recipes/asr.py @@ -169,10 +169,8 @@ def run(self, state: dict): # Save the raw ASR text for details view state["output_text"] = asr_output - def additional_notes(self) -> str | None: - return """ -*Cost ≈ 1 credit for 12.5 words ≈ 0.08 credits per word* - """ + def get_cost_note(self) -> str | None: + return "1 credit for 12.5 words ≈ 0.08 per word" def get_raw_price(self, state: dict): texts = state.get("output_text", []) From 754d547f6e896a1bee0e83463514bba3f08b6123 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:24:55 +0530 Subject: [PATCH 25/29] fix linting --- recipes/LipsyncTTS.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/recipes/LipsyncTTS.py b/recipes/LipsyncTTS.py index 132872bf0..5a1eb10d4 100644 --- a/recipes/LipsyncTTS.py +++ b/recipes/LipsyncTTS.py @@ -177,10 +177,9 @@ def additional_notes(self): "Lipsync": LipsyncPage.get_cost_note(self), "TTS": TextToSpeechPage.get_cost_note(self), } - notes = "\n".join([ - f"- *{k} cost: {v.strip()}*" if v else "" - for k, v in cost_notes.items() - ]) + notes = "\n".join( + [f"- *{k} cost: {v.strip()}*" if v else "" for k, v in cost_notes.items()] + ) notes += LipsyncPage.additional_notes(self) or "" notes += TextToSpeechPage.additional_notes(self) or "" From 0c1876e3ac428dc7e5eab08a1b8fb77f0ee7dcf8 Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Thu, 9 Nov 2023 18:16:35 +0530 Subject: [PATCH 26/29] Make zero-margin for caption in sticky submit section --- daras_ai_v2/base.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index 13c3ee662..c88247364 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -485,11 +485,15 @@ def render_submit_button(self, key="--submit-1"): if cost_note: cost_note = f"({cost_note.strip()})" st.caption( - f"Run cost = [{self.get_price_roundoff(st.session_state)} credits]({self.get_credits_click_url()})" - f" {cost_note}" + f""" +

+ Run cost = {self.get_price_roundoff(st.session_state)} credits + {cost_note} +

+ """, + unsafe_allow_html=True, ) - additional_notes = self.additional_notes() - if additional_notes: + if additional_notes := self.additional_notes(): st.caption(additional_notes) with col2: submitted = st.button( From 52d17594b469aa4ade270701777585c74f8b626f Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Sun, 12 Nov 2023 17:39:20 +0530 Subject: [PATCH 27/29] right align privacy and terms fix alignment of run cost --- daras_ai_v2/base.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/daras_ai_v2/base.py b/daras_ai_v2/base.py index c88247364..b306cf626 100644 --- a/daras_ai_v2/base.py +++ b/daras_ai_v2/base.py @@ -486,15 +486,11 @@ def render_submit_button(self, key="--submit-1"): cost_note = f"({cost_note.strip()})" st.caption( f""" -

- Run cost = {self.get_price_roundoff(st.session_state)} credits - {cost_note} -

+Run cost = {self.get_price_roundoff(st.session_state)} credits {cost_note} +{self.additional_notes() or ""} """, unsafe_allow_html=True, ) - if additional_notes := self.additional_notes(): - st.caption(additional_notes) with col2: submitted = st.button( "🏃 Submit", @@ -640,10 +636,11 @@ def _render_input_col(self): st.text_input("Title", key=StateKeys.page_title) st.text_area("Notes", key=StateKeys.page_notes) submitted = self.render_submit_button() - st.caption( - "_By submitting, you agree to Gooey.AI's [terms](https://gooey.ai/terms) & " - "[privacy policy](https://gooey.ai/privacy)._" - ) + with st.div(style={"textAlign": "right"}): + st.caption( + "_By submitting, you agree to Gooey.AI's [terms](https://gooey.ai/terms) & " + "[privacy policy](https://gooey.ai/privacy)._" + ) return submitted def _render_output_col(self, submitted: bool): From eaececfb927705d21d1be2d3cdf087c3b2e32565 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Sun, 12 Nov 2023 21:09:50 +0530 Subject: [PATCH 28/29] add index for savedrun --- ...lance_alter_appuser_created_at_and_more.py | 38 +++++++++++++++++++ ...bots_savedr_created_cb8e09_idx_and_more.py | 24 ++++++++++++ bots/models.py | 2 + 3 files changed, 64 insertions(+) create mode 100644 app_users/migrations/0010_alter_appuser_balance_alter_appuser_created_at_and_more.py create mode 100644 bots/migrations/0046_savedrun_bots_savedr_created_cb8e09_idx_and_more.py diff --git a/app_users/migrations/0010_alter_appuser_balance_alter_appuser_created_at_and_more.py b/app_users/migrations/0010_alter_appuser_balance_alter_appuser_created_at_and_more.py new file mode 100644 index 000000000..fc8b7f401 --- /dev/null +++ b/app_users/migrations/0010_alter_appuser_balance_alter_appuser_created_at_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.5 on 2023-11-12 15:40 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + dependencies = [ + ("app_users", "0009_alter_appusertransaction_options_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="appuser", + name="balance", + field=models.IntegerField(verbose_name="bal"), + ), + migrations.AlterField( + model_name="appuser", + name="created_at", + field=models.DateTimeField( + blank=True, + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + migrations.AlterField( + model_name="appuser", + name="display_name", + field=models.TextField(blank=True, verbose_name="name"), + ), + migrations.AlterField( + model_name="appuser", + name="is_paying", + field=models.BooleanField(default=False, verbose_name="paid"), + ), + ] diff --git a/bots/migrations/0046_savedrun_bots_savedr_created_cb8e09_idx_and_more.py b/bots/migrations/0046_savedrun_bots_savedr_created_cb8e09_idx_and_more.py new file mode 100644 index 000000000..ff6fb4d97 --- /dev/null +++ b/bots/migrations/0046_savedrun_bots_savedr_created_cb8e09_idx_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.5 on 2023-11-12 15:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("bots", "0045_savedrun_transaction"), + ] + + operations = [ + migrations.AddIndex( + model_name="savedrun", + index=models.Index( + fields=["-created_at"], name="bots_savedr_created_cb8e09_idx" + ), + ), + migrations.AddIndex( + model_name="savedrun", + index=models.Index( + fields=["-updated_at"], name="bots_savedr_updated_a6c854_idx" + ), + ), + ] diff --git a/bots/models.py b/bots/models.py index b527cd998..937f26e70 100644 --- a/bots/models.py +++ b/bots/models.py @@ -170,6 +170,8 @@ class Meta: ), ] indexes = [ + models.Index(fields=["-created_at"]), + models.Index(fields=["-updated_at"]), models.Index(fields=["workflow"]), models.Index(fields=["workflow", "run_id", "uid"]), models.Index(fields=["workflow", "example_id", "run_id", "uid"]), From b849a6fa7a056502669199be285c65c1812dea48 Mon Sep 17 00:00:00 2001 From: Sean Blagsvedt Date: Mon, 13 Nov 2023 15:40:45 -0800 Subject: [PATCH 29/29] Create LICENSE --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License.