feat: implemented audiogen

This commit is contained in:
Yandrik 2024-04-25 15:13:59 +02:00
commit 8625ddf6f6
16 changed files with 1074 additions and 0 deletions

164
.gitignore vendored Normal file
View 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
View 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
View 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>

View 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
View 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
View 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>

View 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
View 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
View 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()

BIN
out.mp3 Normal file

Binary file not shown.

402
poetry.lock generated Normal file
View 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
View 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
View 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
View 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
View 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
View 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.'}