diff --git a/pyproject.toml b/pyproject.toml index 36298c7..34bedf6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,19 +27,22 @@ classifiers = [ ] dependencies = [ - "load-dotenv>=0.1.0", + "audioop-lts; python_version >= '3.13'", "beautifulsoup4", "ebooklib", "kokoro>=0.9.4", + "load-dotenv>=0.1.0", "lxml", "mutagen", "nltk", - "numpy", - "pillow", + "numpy", # --index-url https://download.pytorch.org/whl/cu132 + "pillow", # --index-url https://download.pytorch.org/whl/cu132 "pydub", "soundfile", - "tqdm", - "audioop-lts; python_version >= '3.13'", + "torch", # --index-url https://download.pytorch.org/whl/cu132 + "torchaudio", # --index-url https://download.pytorch.org/whl/cu132 + "torchcodec", # PyTorch 2.9+ # --index-url https://download.pytorch.org/whl/cu132 + "tqdm", # --index-url https://download.pytorch.org/whl/cu132 ] [project.optional-dependencies] diff --git a/spantrack.py b/spantrack.py new file mode 100644 index 0000000..bae30b0 --- /dev/null +++ b/spantrack.py @@ -0,0 +1,22 @@ +import os +import logging +logger = logging.get_logger() +os.environ["NLTK_DATA" = 'C:\\Users\\renat\\AppData\\Roaming\\nltk_data' +os.environ["HF_TOKEN"] = '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' +from spantrack import cli +root = 'D:\DATA\EBOOKS\_SEM_DRM\' +globs = [ +"Night School*.epub", +"The Midnight*.epub", +"Past Tense*.epub", +"Blue Moon*.epub", +"The Sentinel", +"Better Off Dead", +"No Plan B", +] +for glob in globs: + f = root.glob(glob) + if len(f) > 1: + logger.error("Glob pattern found more than 1 file: %s", f) + continue + cli.main([f.resolve(), "--voice", "am_liam"]) \ No newline at end of file diff --git a/src/epub_tts/__main__.py b/src/epub_tts/__main__.py index 9543fda..44e0140 100644 --- a/src/epub_tts/__main__.py +++ b/src/epub_tts/__main__.py @@ -33,7 +33,8 @@ from argparse import ArgumentParser from dotenv import load_dotenv # Local imports -from . import tts_aedocw, tts_generic, tts_kokoro +from . import tts_aedocw, tts_generic +#from . import tts_kokoro from . import log, PathLike from .utils import check_env @@ -54,7 +55,7 @@ BACKENDS = { "epub2tts-chatterbox": tts_aedocw.AedocwChatterbox, "epub2tts-kokoro": tts_aedocw.AedocwKokoro, "generic-epub2tts": tts_aedocw.Epub2TTS, - "kokoro": tts_kokoro.KokoroBackend, + #"kokoro": tts_kokoro.KokoroBackend, } diff --git a/src/epub_tts/tts_kokoro.py b/src/epub_tts/tts_kokoro.py index a8e4a40..8c421df 100644 --- a/src/epub_tts/tts_kokoro.py +++ b/src/epub_tts/tts_kokoro.py @@ -9,12 +9,14 @@ No vendored repository needed, kokoro is a pure Python package that can be insta # stdlib modules import subprocess +from pathlib import Path import os import sys # Automatically enable MPS fallback on Apple Silicon macOS if sys.platform == 'darwin': os.environ['PYTORCH_ENABLE_MPS_FALLBACK'] = '1' + # pip installed packages import numpy as np import soundfile @@ -203,3 +205,109 @@ class KokoroBackend(GenericTTSBackend): os.remove(file) segments.append(partname) return segments + + # *** + + # READ + def read_from_txt_to_wav(text_file:PathLike, + speaker:str="af_heart") -> Path: + if not isinstance(text_file,Path): + text_file = Path(text_file).resolve() + else: + text_file = text_file.resolve() + + # Check if text file exists + if not text_file.exists(): + print(f"Error: Text file '{text_file}' not found.") + return False + + # Read the text from the file + with open(text_file, 'r', encoding='utf-8') as f: + text_contents = f.read() + + # Generate output filename (replace .txt extension with .wav) + output_file = text_file.with_suffix('.wav') + + # Check for CUDA GPU + if torch.cuda.is_available(): + print('CUDA GPU available') + torch.set_default_device('cuda') + + print(f"Generating audio for speaker '{speaker}' from '{text_file}'...") + + # Create pipeline with language code (first character of speaker name) + pipeline = KPipeline(lang_code=speaker[0]) + + # Generate audio segments + audio_segments = [] + for gs, ps, audio in pipeline(text_contents, voice=speaker, speed=1, split_pattern=r'\n\n\n'): + audio_segments.append(audio) + + # Concatenate all audio segments + final_audio = np.concatenate(audio_segments) + + # Write to wav file + soundfile.write(output_file, final_audio, 24000) + + print(f"Audio saved to '{output_file}'") + return output_file + + + + def get_speakers(self) -> list[str]: + """Return list of available speakers. + See https://huggingface.co/hexgrad/Kokoro-82M/blob/main/VOICES.md""" + speakers = [ + # πŸ‡ΊπŸ‡Έ American English: 11F 9M + # Overall Grade 'A' + "af_heart", + # Overall Grade 'C+' + "af_aoede","af_kore","af_sarah", + "am_fenrir","am_michael","am_puck", + # Other + "af_alloy", "af_bella", "af_jessica", "af_nicole", "af_nova", "af_river", "af_sky", "am_adam", "am_echo", "am_eric", "am_liam", "am_onyx", "am_santa", "bf_alice", + # πŸ‡¬πŸ‡§ British English: 4F 4M + "bf_emma", "bf_isabella", "bf_lily", "bm_daniel", "bm_fable", "bm_george", "bm_lewis", + # πŸ‡§πŸ‡· Brazilian Portuguese: 1F 2M + "pf_dora", + "pm_alex", + "pm_santa"] + # Old list: + # ["af_heart", "af_joy", "af_sad", "af_angry", "af_fear", "af_surprise"] + return speakers + + def gen_speaker_samples( + self, + samples: list =None, + output_path: PathLike=None, + ) -> list[str]: + """Generate sample speakers audio in output_path.""" + result = self._build_command("gen_samples.py", *samples, output_path=output_path) + return [str(self._normalize_path(s)) for s in samples] + + def gen_speaker_samples(): + + + if torch.cuda.is_available(): + print('CUDA GPU available') + torch.set_default_device('cuda') + + for speaker in speakers: + file = speaker + "_sample.wav" + if os.path.exists(file): + print(f"Sample for {speaker} already exists.") + continue + else: + print(f"Creating {speaker}") + pipeline = KPipeline(lang_code=speaker[0]) + sentence = f"Hello, this voice is {speaker[3:]}. The quick brown fox jumped over the lazy dog. The fish twisted and turned on the bent hook. Press the pants and sew a button on the vest. The swan dive was far short of perfect." + audio_segments = [] + for gs, ps, audio in pipeline( + sentence, + repo_id='hexgrad/Kokoro-82M', + voice=speaker, + speed=1, + split_pattern=r'\n\n\n'): + audio_segments.append(audio) + final_audio = np.concatenate(audio_segments) + soundfile.write(file, final_audio, 24000)