commit 8625ddf6f617e2cc55a108ea63d304bdc32be3f8 Author: Yandrik Date: Thu Apr 25 15:13:59 2024 +0200 feat: implemented audiogen diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b8e6a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,164 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Files with .secret endings or *.secret.* +*.secret +*.secret.* diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/cody_history.xml b/.idea/cody_history.xml new file mode 100644 index 0000000..0a9379e --- /dev/null +++ b/.idea/cody_history.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + </llm> + </llm> + </chat> + <chat> + <accountId value="VXNlcjozODgwNDA=" /> + <internalId value="ee4dee5b-427a-4b00-a876-99d68351aaa3" /> + <llm> + <llm> + <model value="anthropic/claude-3-opus-20240229" /> + <provider value="Anthropic" /> + <title value="Claude 3 Opus" /> + </llm> + </llm> + </chat> + <chat> + <accountId value="VXNlcjozODgwNDA=" /> + <internalId value="e16bdaff-5b56-4fde-8aa5-787d5f96af7f" /> + </chat> + <chat> + <accountId value="VXNlcjozODgwNDA=" /> + <internalId value="f507e0ec-ef8c-4d67-b3bb-ebeb0e34f9b7" /> + <llm> + <llm> + <model value="anthropic/claude-3-opus-20240229" /> + <provider value="Anthropic" /> + <title value="Claude 3 Opus" /> + </llm> + </llm> + </chat> + </list> + </chats> + <defaultLlm> + <llm> + <model value="anthropic/claude-3-opus-20240229" /> + <provider value="Anthropic" /> + <title value="Claude 3 Opus" /> + </llm> + </defaultLlm> + </component> +</project> \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ +<component name="InspectionProjectProfileManager"> + <settings> + <option name="USE_PROJECT_PROFILE" value="false" /> + <version value="1.0" /> + </settings> +</component> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..4582858 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="Black"> + <option name="sdkName" value="Poetry (tts-markup-utility)" /> + </component> + <component name="ProjectRootManager" version="2" project-jdk-name="Poetry (tts-markup-utility)" project-jdk-type="Python SDK" /> +</project> \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..af09c69 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/tts-markup-utility.iml" filepath="$PROJECT_DIR$/.idea/tts-markup-utility.iml" /> + </modules> + </component> +</project> \ No newline at end of file diff --git a/.idea/tts-markup-utility.iml b/.idea/tts-markup-utility.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/.idea/tts-markup-utility.iml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="PYTHON_MODULE" version="4"> + <component name="NewModuleRootManager"> + <content url="file://$MODULE_DIR$" /> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module> \ No newline at end of file diff --git a/audiogen.py b/audiogen.py new file mode 100644 index 0000000..6e64891 --- /dev/null +++ b/audiogen.py @@ -0,0 +1,143 @@ +import logging +import re +from pathlib import Path +from pydub import AudioSegment, silence +from openai import OpenAI +import time + + +def get_api_key() -> str: + try: + with open('apikey.secret') as f: + api_key = f.read().strip() + if api_key == '': + raise ValueError('API key not found. Please provide your API key in the file \'apikey.secret\'.') + return api_key + except FileNotFoundError: + raise ValueError('Couldn\'t read API key from file \'apikey.secret\'. Does it exist?') + + +class AudioGenerator: + def __init__(self, parsed_data, output_file, default_silence=650, ai_provider="openai", api_key=None): + self.parsed_data = parsed_data + self.output_file = output_file + self.default_silence = default_silence + self.sections = {} + self.current_section = None + + if not api_key: + api_key = get_api_key() + + match ai_provider: + case "openai": + self.client = OpenAI(api_key=api_key) + case "zuki": + self.client = OpenAI(base_url="https://zukijourney.xyzbot.net/v1", api_key=api_key) + case _: + raise ValueError(f"Unsupported AI provider: {ai_provider}") + + def validate_voices(self): + """Check if all voices in the parsed data are valid.""" + valid_voices = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'] + invalid_voices = set() + + for item in self.parsed_data: + if item['type'] == 'voice' and item['voice'] not in valid_voices: + invalid_voices.add(item['voice']) + + if invalid_voices: + raise ValueError(f"Invalid voice(s) found: {', '.join(invalid_voices)}") + print("All voices are valid.") + + def validate_sections(self): + """Check if all sections used are defined beforehand.""" + used_sections = set() + defined_sections = set() + section_errors = [] + + for item in self.parsed_data: + + if item['type'] == 'section_start': + defined_sections.add(item['section_id']) + elif item['type'] == 'insert_section': + section_id = item['section_id'] + if section_id not in defined_sections: + section_errors.append(f"Section {section_id} is used before being defined.") + used_sections.add(item['section_id']) + + undefined_sections = used_sections - defined_sections + + if undefined_sections or len(section_errors) > 0: + raise ValueError(f"Section Validation Errors:\n {'\n '.join(section_errors)}\n\nUndefined section(s) used: {', '.join(map(str, undefined_sections))}") + print("All sections are properly defined.") + + + def text_to_speech(self, text, voice): + """Generate speech using OpenAI's voice API with retry logic.""" + print(f"Voice {voice} chosen") + print(f"TTS: {text[:50]}...") + + temp_path = Path("temp_speech.mp3") + attempts = 0 + success = False + + while not success: + try: + response = self.client.audio.speech.create( + model="tts-1", + voice=voice, + input=text, + ) + response.write_to_file(str(temp_path)) + success = True + return AudioSegment.from_mp3(temp_path) + except Exception as e: + print(f"Failed to generate TTS: {e}") + attempts += 1 + if attempts >= 3: + user_decision = input("Retry TTS generation? (yes/no): ").strip().lower() + if user_decision.lower() in ['y', 'yes']: + attempts = 0 # Reset attempts for another round of retries + else: + print("Exiting due to TTS generation failure.") + exit(1) + else: + print("Retrying...") + time.sleep(1) # Wait a bit before retrying to avoid hammering the API too quickly + + def generate_audio(self): + self.validate_voices() + self.validate_sections() + combined_audio = AudioSegment.empty() + current_voice = None + + for item in self.parsed_data: + if item['type'] == 'voice': + current_voice = item['voice'] + elif item['type'] == 'text': + if not current_voice: + raise ValueError("First text segment before voice was selected!") + audio_segment = self.text_to_speech(item['text'], current_voice) + combined_audio += audio_segment + if self.default_silence > 0: + combined_audio += AudioSegment.silent(duration=self.default_silence) + if self.current_section is not None: + self.sections[self.current_section] += audio_segment + elif item['type'] == 'silence': + combined_audio += AudioSegment.silent(duration=item['duration']) + if self.current_section is not None: + self.sections[self.current_section] += AudioSegment.silent(duration=item['duration']) + elif item['type'] == 'section_start': + self.current_section = item['section_id'] + self.sections[self.current_section] = AudioSegment.empty() + elif item['type'] == 'section_end': + self.current_section = None + elif item['type'] == 'insert_section': + section_id = item['section_id'] + if section_id in self.sections: + combined_audio += self.sections[section_id] + else: + raise ValueError(f"Section {section_id} not found!") + + combined_audio.export(self.output_file, format="mp3") +# Example usage diff --git a/main.py b/main.py new file mode 100644 index 0000000..c5124bb --- /dev/null +++ b/main.py @@ -0,0 +1,146 @@ +import re + +import argparse + +from audiogen import AudioGenerator + + +class SimpleMarkupParser: + def __init__(self, input_text): + self.input_text = ' '.join(input_text.split()) + self.parsed_output = [] + self.sections = {} + + def parse(self): + tokens = re.split(r'(\[[^]]+])', self.input_text) + + for token in tokens: + voice_match = re.match(r'\[voice ([^]]+)]', token) + if voice_match: + self.parsed_output.append({'type': 'voice', 'voice': voice_match.group(1)}) + continue + + silence_match = re.match(r'\[silence (\d+)s]', token) + if silence_match: + duration = int(silence_match.group(1)) * 1000 + self.parsed_output.append({'type': 'silence', 'duration': duration}) + continue + + section_match = re.match(r'\[section (\d+)]', token) + if section_match: + section_id = int(section_match.group(1)) + self.parsed_output.append({'type': 'section_start', 'section_id': section_id}) + continue + + end_section_match = re.match(r'\[end_section]', token) + if end_section_match: + self.parsed_output.append({'type': 'section_end'}) + continue + + insert_section_match = re.match(r'\[insert_section (\d+)]', token) + if insert_section_match: + section_id = int(insert_section_match.group(1)) + self.parsed_output.append({'type': 'insert_section', 'section_id': section_id}) + continue + + if re.match(r'\[.*]', token): + self.parsed_output.append({'type': 'none', 'text': token}) + continue + + if token.strip(): + self.parsed_output.append({'type': 'text', 'text': token.strip()}) + + def get_output(self): + return self.parsed_output + + +def main(): + + + parser_description = """ + TTS text with voice selection, silence intervals, and section functionality. + The script supports a simple markup language to change voices, insert silence, define sections, and insert sections within the text. + + Markup Language Syntax: + - Change Voice: Use [voice VOICE_NAME] to switch to a different voice. + Example: [voice alloy] switches to the 'alloy' voice. + - Insert Silence: Use [silence SECONDSs] to insert a period of silence. + Example: [silence 4s] inserts a 4-second silence. + - Define Section: Use [section SECTION_ID] to start a new section with the given ID. + Example: [section 1] starts a new section with ID 1. + - End Section: Use [end_section] to end the current section. + - Insert Section: Use [insert_section SECTION_ID] to insert the audio from the specified section ID. + Example: [section 1] [voice alloy] Hi there! [end_section] [insert_section 1] inserts the audio from section 1. + In effect, this will say "Hi there" with the 'alloy' voice, and then repeat it exactly. + + Supported voices: + - All OpenAI voices are supported. These are: + valid_voices = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'] + - alloy (male, neutral) + - echo (male, full-bodied) + - fable (male, high) + - onyx (male, deep) + - nova (female, expressive) + - shimmer (female, full-bodied) + + Sample Input: + "[voice alloy] How's it going? [section 1] [voice fable] I love it here! [end_section] [voice alloy] Repeat that please? [insert_section 1]" + + This input will: + 1. Start with the 'alloy' voice saying "How's it going?" + 2. Define a new section (ID 1) with the 'fable' voice saying "I love it here!" + 3. Switch back to the 'alloy' voice saying "Repeat that please?" + 4. Insert fable speaking the audio from section 1 (without regenerating it). + """ + + + parser = argparse.ArgumentParser(description=parser_description, + formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument('--file', type=str, help="File containing the text to parse.") + parser.add_argument('text', nargs='?', default=None, help="Text to parse.") + parser.add_argument('--out-file', type=str, default="out.mp3", + help="Output file to save the audio to (mp3 recommended). Default out.mp3") + parser.add_argument('--provider', type=str, default="openai", help="AI Provider. Supported: openai, zuki") + parser.add_argument('--api-key', type=str, default=None, + help="API Key for AI Provider. Alternatively, create a file 'apikey.secret' in the workdir containing your API key.") + args = parser.parse_args() + + if not args.file and not args.text: + print("Please provide either a file (using --file <PATH>) or a text input!") + exit(1) + + if args.file and args.text: + print("Please provide either a file (using --file <PATH>) or a text input, not both!") + exit(1) + + input_text = args.text + if args.file: + with open(args.file, 'r') as file: + input_text = file.read() + + parser = SimpleMarkupParser(input_text) + parser.parse() + output = parser.get_output() + print("parsed:", output) + + if len(output) == 0: + print("No output found! Does the input text adhere to the expected format?") + exit(3) + + tts = AudioGenerator(output, args.out_file, ai_provider=args.provider) + try: + tts.validate_voices() + except ValueError as e: + print(e) + valid_voices = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'] + print("Voices not valid! Valid voices are: " + "'" + "', '".join(valid_voices) + "'") + + try: + tts.generate_audio() + except ValueError as e: + print("Generating audio failed:") + print(e) + + +if __name__ == "__main__": + main() diff --git a/out.mp3 b/out.mp3 new file mode 100644 index 0000000..a2d03a2 Binary files /dev/null and b/out.mp3 differ diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..681fcff --- /dev/null +++ b/poetry.lock @@ -0,0 +1,402 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[package]] +name = "anyio" +version = "4.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[package]] +name = "argparse" +version = "1.4.0" +description = "Python command-line parsing library" +optional = false +python-versions = "*" +files = [ + {file = "argparse-1.4.0-py2.py3-none-any.whl", hash = "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314"}, + {file = "argparse-1.4.0.tar.gz", hash = "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4"}, +] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "distro" +version = "1.9.0" +description = "Distro - an OS platform information API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, + {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.26.0)"] + +[[package]] +name = "httpx" +version = "0.27.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "openai" +version = "1.23.6" +description = "The official Python library for the openai API" +optional = false +python-versions = ">=3.7.1" +files = [ + {file = "openai-1.23.6-py3-none-any.whl", hash = "sha256:f406c76ba279d16b9aca5a89cee0d968488e39f671f4dc6f0d690ac3c6f6fca1"}, + {file = "openai-1.23.6.tar.gz", hash = "sha256:612de2d54cf580920a1156273f84aada6b3dca26d048f62eb5364a4314d7f449"}, +] + +[package.dependencies] +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +httpx = ">=0.23.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +tqdm = ">4" +typing-extensions = ">=4.7,<5" + +[package.extras] +datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "pathlib" +version = "1.0.1" +description = "Object-oriented filesystem paths" +optional = false +python-versions = "*" +files = [ + {file = "pathlib-1.0.1-py3-none-any.whl", hash = "sha256:f35f95ab8b0f59e6d354090350b44a80a80635d22efdedfa84c7ad1cf0a74147"}, + {file = "pathlib-1.0.1.tar.gz", hash = "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pydantic" +version = "2.7.1" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, + {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.18.2" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.18.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, + {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, + {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, + {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, + {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, + {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, + {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, + {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, + {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, + {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, + {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, + {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, + {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, + {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydub" +version = "0.25.1" +description = "Manipulate audio with an simple and easy high level interface" +optional = false +python-versions = "*" +files = [ + {file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"}, + {file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"}, +] + +[[package]] +name = "pytest" +version = "8.1.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.4,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "tqdm" +version = "4.66.2" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"}, + {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "797f461706f9340f50f5d1b4a52b6af41ccb1565e5b1d199b36aa7ec140bcb72" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7ac8cc0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poetry] +name = "tts-markup-utility" +version = "0.1.0" +description = "" +authors = ["Your Name <you@example.com>"] +license = "MIT" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.12" +openai = "^1.16.2" +argparse = "^1.4.0" +pathlib = "^1.0.1" +pydub = "^0.25.1" +pytest = "^8.1.1" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/section_test.txt b/section_test.txt new file mode 100644 index 0000000..69bb1a6 --- /dev/null +++ b/section_test.txt @@ -0,0 +1 @@ +[voice alloy] How's it going? [section 1] [voice fable] I love it here! [end_section] [voice alloy] Repeat that please? [insert_section 1] diff --git a/section_test_broken.txt b/section_test_broken.txt new file mode 100644 index 0000000..ecf847f --- /dev/null +++ b/section_test_broken.txt @@ -0,0 +1 @@ +[voice alloy] How's it going? [insert_section 2] [section 1] [voice fable] I love it here! [end_section] [voice alloy] Repeat that please? [insert_section 1] diff --git a/test_audiogen_validate.py b/test_audiogen_validate.py new file mode 100644 index 0000000..54ddf22 --- /dev/null +++ b/test_audiogen_validate.py @@ -0,0 +1,56 @@ +from unittest import TestCase +from unittest.mock import patch +from audiogen import AudioGenerator + +class TestAudioGeneratorValidate(TestCase): + def setUp(self): + self.valid_parsed_data = [ + {'type': 'voice', 'voice': 'alloy'}, + {'type': 'text', 'text': 'Hello, world!'}, + {'type': 'silence', 'duration': 1000}, + {'type': 'section_start', 'section_id': 1}, + {'type': 'text', 'text': 'This is section 1.'}, + {'type': 'section_end'}, + {'type': 'insert_section', 'section_id': 1} + ] + self.audio_generator = AudioGenerator(self.valid_parsed_data, 'test_output.mp3') + + def test_validate_voices_valid(self): + self.audio_generator.validate_voices() + # No assertion needed as the function should not raise any exception + + def test_validate_voices_invalid(self): + invalid_parsed_data = [ + {'type': 'voice', 'voice': 'invalid_voice'}, + {'type': 'text', 'text': 'Hello, world!'} + ] + invalid_audio_generator = AudioGenerator(invalid_parsed_data, 'test_output.mp3') + with self.assertRaises(ValueError) as cm: + invalid_audio_generator.validate_voices() + self.assertEqual(str(cm.exception), "Invalid voice(s) found: invalid_voice") + + def test_validate_sections_valid(self): + self.audio_generator.validate_sections() + # No assertion needed as the function should not raise any exception + + def test_validate_sections_invalid(self): + invalid_parsed_data = [ + {'type': 'voice', 'voice': 'alloy'}, + {'type': 'text', 'text': 'Hello, world!'}, + {'type': 'insert_section', 'section_id': 1} + ] + invalid_audio_generator = AudioGenerator(invalid_parsed_data, 'test_output.mp3') + with self.assertRaises(ValueError) as cm: + invalid_audio_generator.validate_sections() + self.assertIn("Section 1 is used before being defined.", str(cm.exception)) + self.assertIn("Undefined section(s) used: 1", str(cm.exception)) + + # @patch('builtins.input', return_value='no') + # @patch('audiogen.AudioGenerator.text_to_speech') + # def test_text_to_speech_retry_logic(self, mock_text_to_speech, mock_input): + # mock_text_to_speech.side_effect = [Exception('API error'), Exception('API error'), Exception('API error')] + # with self.assertRaises(SystemExit) as cm: + # self.audio_generator.text_to_speech('Hello, world!', 'alloy') + # self.assertEqual(cm.exception.code, 1) + # self.assertEqual(mock_text_to_speech.call_count, 3) + # mock_input.assert_called_once_with("Retry TTS generation? (yes/no): ") diff --git a/test_main.py b/test_main.py new file mode 100644 index 0000000..c1a5ec8 --- /dev/null +++ b/test_main.py @@ -0,0 +1,52 @@ +from unittest import TestCase + +from main import SimpleMarkupParser + + + +class Test(TestCase): + def test_simple_markup_parser_0(self): + # Test case with sections + markup_text = "[section 1] [voice alloy] Hello, this is section 1. [end_section] [voice nova] Now we're outside the section. [insert_section 1]" + parser = SimpleMarkupParser(markup_text) + parser.parse() + parsed_output = parser.get_output() + + assert len(parsed_output) == 7, "Expected 7 tokens, got %d" % len(parsed_output) + assert parsed_output[0] == {'type': 'section_start', 'section_id': 1} + assert parsed_output[1] == {'type': 'voice', 'voice': 'alloy'} + assert parsed_output[2] == {'type': 'text', 'text': 'Hello, this is section 1.'} + assert parsed_output[3] == {'type': 'section_end'} + assert parsed_output[4] == {'type': 'voice', 'voice': 'nova'} + assert parsed_output[5] == {'type': 'text', 'text': "Now we're outside the section."} + assert parsed_output[6] == {'type': 'insert_section', 'section_id': 1} + + + def test_simple_markup_parser_1(self): + # Test case with silence + markup_text = "[voice nova] Let's have a moment of silence. [silence 3s] And we're back!" + parser = SimpleMarkupParser(markup_text) + parser.parse() + parsed_output = parser.get_output() + + assert len(parsed_output) == 4 + assert parsed_output[0] == {'type': 'voice', 'voice': 'nova'} + assert parsed_output[1] == {'type': 'text', 'text': "Let's have a moment of silence."} + assert parsed_output[2] == {'type': 'silence', 'duration': 3000} + assert parsed_output[3] == {'type': 'text', 'text': "And we're back!"} + + def test_simple_markup_parser_2(self): + # Test case with unknown markup + markup_text = "[voice fable] Hello! [unknown_markup] This is an unknown markup. [voice nova] Back to a known voice." + parser = SimpleMarkupParser(markup_text) + parser.parse() + parsed_output = parser.get_output() + + assert len(parsed_output) == 6 + assert parsed_output[0] == {'type': 'voice', 'voice': 'fable'} + assert parsed_output[1] == {'type': 'text', 'text': 'Hello!'} + assert parsed_output[2] == {'type': 'none', 'text': '[unknown_markup]'} + assert parsed_output[3] == {'type': 'text', 'text': 'This is an unknown markup.'} + assert parsed_output[4] == {'type': 'voice', 'voice': 'nova'} + assert parsed_output[5] == {'type': 'text', 'text': 'Back to a known voice.'} +