feat: add pipeline
Some checks failed
continuous-integration/drone Build is failing

This commit is contained in:
Yandrik 2024-04-25 15:37:15 +02:00
parent 69da9f8d25
commit 918eca78cb
4 changed files with 150 additions and 37 deletions

33
.drone.yml Normal file
View 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"

View File

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

@ -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"

View File

@ -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"]