diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..be26e97 --- /dev/null +++ b/.drone.yml @@ -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" diff --git a/audiogen.py b/audiogen.py index 6e64891..42f0e5d 100644 --- a/audiogen.py +++ b/audiogen.py @@ -4,21 +4,33 @@ from pathlib import Path from pydub import AudioSegment, silence from openai import OpenAI import time +from sys import exit def get_api_key() -> str: try: - with open('apikey.secret') as f: + 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\'.') + 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?') + 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: - 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.output_file = output_file self.default_silence = default_silence @@ -32,18 +44,20 @@ class AudioGenerator: 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) + 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'] + 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 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)}") @@ -56,22 +70,24 @@ class AudioGenerator: 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 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']) + 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))}") + 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") @@ -95,15 +111,19 @@ class AudioGenerator: 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']: + 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 + time.sleep( + 1 + ) # Wait a bit before retrying to avoid hammering the API too quickly def generate_audio(self): self.validate_voices() @@ -112,32 +132,36 @@ class AudioGenerator: current_voice = None for item in self.parsed_data: - if item['type'] == 'voice': - current_voice = item['voice'] - elif item['type'] == 'text': + 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) + 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']) + 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.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': + elif item["type"] == "section_end": self.current_section = None - elif item['type'] == 'insert_section': - section_id = item['section_id'] + elif item["type"] == "insert_section": + section_id = item["section_id"] if section_id in self.sections: combined_audio += self.sections[section_id] else: raise ValueError(f"Section {section_id} not found!") combined_audio.export(self.output_file, format="mp3") + + # Example usage diff --git a/poetry.lock b/poetry.lock index c22e657..565760f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -178,6 +178,63 @@ files = [ [package.dependencies] 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]] name = "openai" version = "1.23.6" @@ -500,4 +557,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.12,<3.13" -content-hash = "5edb9bebdbf3d2cbd05201e953830c0bd3cb05956885df55a192ad1026081cc3" +content-hash = "b7a69be6accf7803d29e50194355e464610cabb3e6114dc7c5e4f027b4a475d0" diff --git a/pyproject.toml b/pyproject.toml index 080e27d..97c351e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,11 +13,10 @@ argparse = "^1.4.0" # pathlib = "^1.0.1" pydub = "^0.25.1" -[tool.poetry.dev-dependencies] -pytest = "^8.1.1" - [tool.poetry.group.dev.dependencies] +pytest = "^8.1.1" pyinstaller = "^6.6.0" +mypy = "^1.10.0" [build-system] requires = ["poetry-core"]