This commit is contained in:
parent
69da9f8d25
commit
918eca78cb
33
.drone.yml
Normal file
33
.drone.yml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Unit Tests
|
||||||
|
image: python:3.12
|
||||||
|
commands:
|
||||||
|
- pip install poetry
|
||||||
|
- poetry config virtualenvs.create false
|
||||||
|
- poetry install
|
||||||
|
- poetry run pytest
|
||||||
|
|
||||||
|
- name: Python Code Lint
|
||||||
|
image: python:3.12
|
||||||
|
commands:
|
||||||
|
- pip install poetry
|
||||||
|
- poetry config virtualenvs.create false
|
||||||
|
- poetry install
|
||||||
|
- poetry run black .
|
||||||
|
|
||||||
|
- name: Static Type check
|
||||||
|
image: python:3.12
|
||||||
|
commands:
|
||||||
|
- pip install poetry
|
||||||
|
- poetry config virtualenvs.create false
|
||||||
|
- poetry install
|
||||||
|
- poetry run mypy .
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
image: python:3.12
|
||||||
|
commands:
|
||||||
|
- echo "TODO"
|
90
audiogen.py
90
audiogen.py
@ -4,21 +4,33 @@ from pathlib import Path
|
|||||||
from pydub import AudioSegment, silence
|
from pydub import AudioSegment, silence
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
import time
|
import time
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
|
||||||
def get_api_key() -> str:
|
def get_api_key() -> str:
|
||||||
try:
|
try:
|
||||||
with open('apikey.secret') as f:
|
with open("apikey.secret") as f:
|
||||||
api_key = f.read().strip()
|
api_key = f.read().strip()
|
||||||
if api_key == '':
|
if api_key == "":
|
||||||
raise ValueError('API key not found. Please provide your API key in the file \'apikey.secret\'.')
|
raise ValueError(
|
||||||
|
"API key not found. Please provide your API key in the file 'apikey.secret'."
|
||||||
|
)
|
||||||
return api_key
|
return api_key
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise ValueError('Couldn\'t read API key from file \'apikey.secret\'. Does it exist?')
|
raise ValueError(
|
||||||
|
"Couldn't read API key from file 'apikey.secret'. Does it exist? Alternatively, use the argument '--api-key' to provide your API key."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AudioGenerator:
|
class AudioGenerator:
|
||||||
def __init__(self, parsed_data, output_file, default_silence=650, ai_provider="openai", api_key=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
parsed_data,
|
||||||
|
output_file,
|
||||||
|
default_silence=650,
|
||||||
|
ai_provider="openai",
|
||||||
|
api_key=None,
|
||||||
|
):
|
||||||
self.parsed_data = parsed_data
|
self.parsed_data = parsed_data
|
||||||
self.output_file = output_file
|
self.output_file = output_file
|
||||||
self.default_silence = default_silence
|
self.default_silence = default_silence
|
||||||
@ -32,18 +44,20 @@ class AudioGenerator:
|
|||||||
case "openai":
|
case "openai":
|
||||||
self.client = OpenAI(api_key=api_key)
|
self.client = OpenAI(api_key=api_key)
|
||||||
case "zuki":
|
case "zuki":
|
||||||
self.client = OpenAI(base_url="https://zukijourney.xyzbot.net/v1", api_key=api_key)
|
self.client = OpenAI(
|
||||||
|
base_url="https://zukijourney.xyzbot.net/v1", api_key=api_key
|
||||||
|
)
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(f"Unsupported AI provider: {ai_provider}")
|
raise ValueError(f"Unsupported AI provider: {ai_provider}")
|
||||||
|
|
||||||
def validate_voices(self):
|
def validate_voices(self):
|
||||||
"""Check if all voices in the parsed data are valid."""
|
"""Check if all voices in the parsed data are valid."""
|
||||||
valid_voices = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer']
|
valid_voices = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"]
|
||||||
invalid_voices = set()
|
invalid_voices = set()
|
||||||
|
|
||||||
for item in self.parsed_data:
|
for item in self.parsed_data:
|
||||||
if item['type'] == 'voice' and item['voice'] not in valid_voices:
|
if item["type"] == "voice" and item["voice"] not in valid_voices:
|
||||||
invalid_voices.add(item['voice'])
|
invalid_voices.add(item["voice"])
|
||||||
|
|
||||||
if invalid_voices:
|
if invalid_voices:
|
||||||
raise ValueError(f"Invalid voice(s) found: {', '.join(invalid_voices)}")
|
raise ValueError(f"Invalid voice(s) found: {', '.join(invalid_voices)}")
|
||||||
@ -56,22 +70,24 @@ class AudioGenerator:
|
|||||||
section_errors = []
|
section_errors = []
|
||||||
|
|
||||||
for item in self.parsed_data:
|
for item in self.parsed_data:
|
||||||
|
if item["type"] == "section_start":
|
||||||
if item['type'] == 'section_start':
|
defined_sections.add(item["section_id"])
|
||||||
defined_sections.add(item['section_id'])
|
elif item["type"] == "insert_section":
|
||||||
elif item['type'] == 'insert_section':
|
section_id = item["section_id"]
|
||||||
section_id = item['section_id']
|
|
||||||
if section_id not in defined_sections:
|
if section_id not in defined_sections:
|
||||||
section_errors.append(f"Section {section_id} is used before being defined.")
|
section_errors.append(
|
||||||
used_sections.add(item['section_id'])
|
f"Section {section_id} is used before being defined."
|
||||||
|
)
|
||||||
|
used_sections.add(item["section_id"])
|
||||||
|
|
||||||
undefined_sections = used_sections - defined_sections
|
undefined_sections = used_sections - defined_sections
|
||||||
|
|
||||||
if undefined_sections or len(section_errors) > 0:
|
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))}")
|
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.")
|
print("All sections are properly defined.")
|
||||||
|
|
||||||
|
|
||||||
def text_to_speech(self, text, voice):
|
def text_to_speech(self, text, voice):
|
||||||
"""Generate speech using OpenAI's voice API with retry logic."""
|
"""Generate speech using OpenAI's voice API with retry logic."""
|
||||||
print(f"Voice {voice} chosen")
|
print(f"Voice {voice} chosen")
|
||||||
@ -95,15 +111,19 @@ class AudioGenerator:
|
|||||||
print(f"Failed to generate TTS: {e}")
|
print(f"Failed to generate TTS: {e}")
|
||||||
attempts += 1
|
attempts += 1
|
||||||
if attempts >= 3:
|
if attempts >= 3:
|
||||||
user_decision = input("Retry TTS generation? (yes/no): ").strip().lower()
|
user_decision = (
|
||||||
if user_decision.lower() in ['y', 'yes']:
|
input("Retry TTS generation? (yes/no): ").strip().lower()
|
||||||
|
)
|
||||||
|
if user_decision.lower() in ["y", "yes"]:
|
||||||
attempts = 0 # Reset attempts for another round of retries
|
attempts = 0 # Reset attempts for another round of retries
|
||||||
else:
|
else:
|
||||||
print("Exiting due to TTS generation failure.")
|
print("Exiting due to TTS generation failure.")
|
||||||
exit(1)
|
exit(1)
|
||||||
else:
|
else:
|
||||||
print("Retrying...")
|
print("Retrying...")
|
||||||
time.sleep(1) # Wait a bit before retrying to avoid hammering the API too quickly
|
time.sleep(
|
||||||
|
1
|
||||||
|
) # Wait a bit before retrying to avoid hammering the API too quickly
|
||||||
|
|
||||||
def generate_audio(self):
|
def generate_audio(self):
|
||||||
self.validate_voices()
|
self.validate_voices()
|
||||||
@ -112,32 +132,36 @@ class AudioGenerator:
|
|||||||
current_voice = None
|
current_voice = None
|
||||||
|
|
||||||
for item in self.parsed_data:
|
for item in self.parsed_data:
|
||||||
if item['type'] == 'voice':
|
if item["type"] == "voice":
|
||||||
current_voice = item['voice']
|
current_voice = item["voice"]
|
||||||
elif item['type'] == 'text':
|
elif item["type"] == "text":
|
||||||
if not current_voice:
|
if not current_voice:
|
||||||
raise ValueError("First text segment before voice was selected!")
|
raise ValueError("First text segment before voice was selected!")
|
||||||
audio_segment = self.text_to_speech(item['text'], current_voice)
|
audio_segment = self.text_to_speech(item["text"], current_voice)
|
||||||
combined_audio += audio_segment
|
combined_audio += audio_segment
|
||||||
if self.default_silence > 0:
|
if self.default_silence > 0:
|
||||||
combined_audio += AudioSegment.silent(duration=self.default_silence)
|
combined_audio += AudioSegment.silent(duration=self.default_silence)
|
||||||
if self.current_section is not None:
|
if self.current_section is not None:
|
||||||
self.sections[self.current_section] += audio_segment
|
self.sections[self.current_section] += audio_segment
|
||||||
elif item['type'] == 'silence':
|
elif item["type"] == "silence":
|
||||||
combined_audio += AudioSegment.silent(duration=item['duration'])
|
combined_audio += AudioSegment.silent(duration=item["duration"])
|
||||||
if self.current_section is not None:
|
if self.current_section is not None:
|
||||||
self.sections[self.current_section] += AudioSegment.silent(duration=item['duration'])
|
self.sections[self.current_section] += AudioSegment.silent(
|
||||||
elif item['type'] == 'section_start':
|
duration=item["duration"]
|
||||||
self.current_section = item['section_id']
|
)
|
||||||
|
elif item["type"] == "section_start":
|
||||||
|
self.current_section = item["section_id"]
|
||||||
self.sections[self.current_section] = AudioSegment.empty()
|
self.sections[self.current_section] = AudioSegment.empty()
|
||||||
elif item['type'] == 'section_end':
|
elif item["type"] == "section_end":
|
||||||
self.current_section = None
|
self.current_section = None
|
||||||
elif item['type'] == 'insert_section':
|
elif item["type"] == "insert_section":
|
||||||
section_id = item['section_id']
|
section_id = item["section_id"]
|
||||||
if section_id in self.sections:
|
if section_id in self.sections:
|
||||||
combined_audio += self.sections[section_id]
|
combined_audio += self.sections[section_id]
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Section {section_id} not found!")
|
raise ValueError(f"Section {section_id} not found!")
|
||||||
|
|
||||||
combined_audio.export(self.output_file, format="mp3")
|
combined_audio.export(self.output_file, format="mp3")
|
||||||
|
|
||||||
|
|
||||||
# Example usage
|
# Example usage
|
||||||
|
59
poetry.lock
generated
59
poetry.lock
generated
@ -178,6 +178,63 @@ files = [
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
altgraph = ">=0.17"
|
altgraph = ">=0.17"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy"
|
||||||
|
version = "1.10.0"
|
||||||
|
description = "Optional static typing for Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"},
|
||||||
|
{file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"},
|
||||||
|
{file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"},
|
||||||
|
{file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"},
|
||||||
|
{file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"},
|
||||||
|
{file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"},
|
||||||
|
{file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"},
|
||||||
|
{file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"},
|
||||||
|
{file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"},
|
||||||
|
{file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"},
|
||||||
|
{file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"},
|
||||||
|
{file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"},
|
||||||
|
{file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"},
|
||||||
|
{file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"},
|
||||||
|
{file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"},
|
||||||
|
{file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"},
|
||||||
|
{file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"},
|
||||||
|
{file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"},
|
||||||
|
{file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"},
|
||||||
|
{file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"},
|
||||||
|
{file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"},
|
||||||
|
{file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"},
|
||||||
|
{file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"},
|
||||||
|
{file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"},
|
||||||
|
{file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"},
|
||||||
|
{file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"},
|
||||||
|
{file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
mypy-extensions = ">=1.0.0"
|
||||||
|
typing-extensions = ">=4.1.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dmypy = ["psutil (>=4.0)"]
|
||||||
|
install-types = ["pip"]
|
||||||
|
mypyc = ["setuptools (>=50)"]
|
||||||
|
reports = ["lxml"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Type system extensions for programs checked with the mypy type checker."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
files = [
|
||||||
|
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||||
|
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "1.23.6"
|
version = "1.23.6"
|
||||||
@ -500,4 +557,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.12,<3.13"
|
python-versions = ">=3.12,<3.13"
|
||||||
content-hash = "5edb9bebdbf3d2cbd05201e953830c0bd3cb05956885df55a192ad1026081cc3"
|
content-hash = "b7a69be6accf7803d29e50194355e464610cabb3e6114dc7c5e4f027b4a475d0"
|
||||||
|
@ -13,11 +13,10 @@ argparse = "^1.4.0"
|
|||||||
# pathlib = "^1.0.1"
|
# pathlib = "^1.0.1"
|
||||||
pydub = "^0.25.1"
|
pydub = "^0.25.1"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
|
||||||
pytest = "^8.1.1"
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
pytest = "^8.1.1"
|
||||||
pyinstaller = "^6.6.0"
|
pyinstaller = "^6.6.0"
|
||||||
|
mypy = "^1.10.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
Loading…
Reference in New Issue
Block a user