feat: implemented audiogen
This commit is contained in:
commit
8625ddf6f6
164
.gitignore
vendored
Normal file
164
.gitignore
vendored
Normal file
@ -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.*
|
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
@ -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
|
52
.idea/cody_history.xml
Normal file
52
.idea/cody_history.xml
Normal file
@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ChatHistory">
|
||||
<chats>
|
||||
<list>
|
||||
<chat>
|
||||
<internalId value="c612aa16-626a-463f-b497-c2f078770f9f" />
|
||||
<llm>
|
||||
<llm>
|
||||
<model value="anthropic/claude-3-sonnet-20240229" />
|
||||
<provider value="Anthropic" />
|
||||
<title value="Claude 3 Sonnet" />
|
||||
</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>
|
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
7
.idea/misc.xml
Normal file
7
.idea/misc.xml
Normal file
@ -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>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -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>
|
8
.idea/tts-markup-utility.iml
Normal file
8
.idea/tts-markup-utility.iml
Normal file
@ -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>
|
143
audiogen.py
Normal file
143
audiogen.py
Normal file
@ -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
|
146
main.py
Normal file
146
main.py
Normal file
@ -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()
|
402
poetry.lock
generated
Normal file
402
poetry.lock
generated
Normal file
@ -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"
|
20
pyproject.toml
Normal file
20
pyproject.toml
Normal file
@ -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"
|
1
section_test.txt
Normal file
1
section_test.txt
Normal file
@ -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]
|
1
section_test_broken.txt
Normal file
1
section_test_broken.txt
Normal file
@ -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]
|
56
test_audiogen_validate.py
Normal file
56
test_audiogen_validate.py
Normal file
@ -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): ")
|
52
test_main.py
Normal file
52
test_main.py
Normal file
@ -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.'}
|
||||
|
Loading…
Reference in New Issue
Block a user