speechjenerator 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: speechjenerator
3
+ Version: 0.1.0
4
+ Summary: It does TTS.
5
+ Author: Warren Davies
6
+ Requires-Python: >=3.10,<3.13
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.12
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
11
+ Requires-Dist: basejenerator (>=0.2.0,<0.3.0)
12
+ Requires-Dist: gtts (>=2.5.4,<3.0.0)
13
+ Requires-Dist: kokoro (>=0.9.4,<0.10.0)
14
+ Requires-Dist: sounddevice (>=0.5.5,<0.6.0)
15
+ Requires-Dist: soundfile (>=0.13.1,<0.14.0)
16
+ Description-Content-Type: text/markdown
17
+
18
+ # model-registry-template
19
+ A template for projects that require the ability to select from multiple models.
20
+
@@ -0,0 +1,2 @@
1
+ # model-registry-template
2
+ A template for projects that require the ability to select from multiple models.
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "speechjenerator"
3
+ version = "0.1.0"
4
+ description = "It does TTS."
5
+ authors = [{ name = "Warren Davies"}]
6
+ readme = "README.md"
7
+ homepage = "https://github.com/WarrenDavies/speechjenerator"
8
+ repository = "https://github.com/WarrenDavies/speechjenerator"
9
+ packages = [
10
+ { include = "speechjenerator", from = "src" }
11
+ ]
12
+ requires-python = ">=3.10,<3.13"
13
+
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
19
+ ]
20
+
21
+ [tool.poetry.dependencies]
22
+ python = ">=3.10,<3.13"
23
+ basejenerator = "^0.2.0"
24
+ gtts = "^2.5.4"
25
+ sounddevice = "^0.5.5"
26
+ soundfile = "^0.13.1"
27
+ kokoro = "^0.9.4"
28
+
29
+ [build-system]
30
+ requires = ["poetry-core>=1.0.0"]
31
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,5 @@
1
+ import pkgutil
2
+ import importlib
3
+
4
+ for _, name, _ in pkgutil.walk_packages(__path__, __name__ + "."):
5
+ importlib.import_module(name)
@@ -0,0 +1,72 @@
1
+ import os
2
+ import platform
3
+ import subprocess
4
+
5
+ import sounddevice as sd
6
+ import soundfile as sf
7
+
8
+ from basejenerator.artifacts.base_artifact import BaseArtifact
9
+
10
+ class GTTSArtifact(BaseArtifact):
11
+ """
12
+ Holds a gTTS "model" object. This is more like a promise to create an audio file.
13
+ Due to how gTTS works, the Google Translate API is not called until `.save()`
14
+ is called.
15
+
16
+ Attributes:
17
+ data (str): The gTTS "model" object.
18
+ item_extras (dict): any additional metadata related to this artifact
19
+ """
20
+
21
+ def __init__(self, data, item_extras):
22
+ self.data = data
23
+ self.item_extras = item_extras
24
+ self.extension = ".mp3"
25
+ self.system = platform.system()
26
+ self.linux_mp3_players = [
27
+ ["mpg123", "--quiet"],
28
+ ["mpv", "--no-terminal"],
29
+ ]
30
+ self.player_funcs = {
31
+ "Windows": self._play_on_windows,
32
+ "Darwin": self._play_on_mac,
33
+ "Linux": self._play_on_linux
34
+ }
35
+
36
+ def save(self, path):
37
+ """
38
+ Called Google Translate API, gets the audio as WAV, and saves to disk.
39
+ """
40
+ self.data.save(path)
41
+
42
+
43
+ def _play_on_windows(self, path):
44
+ os.startfile(path)
45
+
46
+ def _play_on_mac(self, path):
47
+ subprocess.run(['afplay', path])
48
+
49
+ def _play_on_linux(self, path):
50
+ for player_command in self.linux_mp3_players:
51
+ player_command.append(path)
52
+ try:
53
+ subprocess.run(player_command, check=True)
54
+ break
55
+ except FileNotFoundError:
56
+ continue
57
+
58
+ def play(self, path=None):
59
+ """
60
+ Calls Google Translate API, gets the audio as mp3, saves to disk,
61
+ and plays the audio on the system in which speechjenerator is running
62
+ using .
63
+ """
64
+ if not path:
65
+ path = self.data["config"]["path"]
66
+ self.save(path)
67
+
68
+ mp3_player = self.player_funcs[self.system]
69
+ try:
70
+ mp3_player(path)
71
+ except Exception as e:
72
+ print(f"Error playing audio: {e}")
@@ -0,0 +1,52 @@
1
+ """
2
+ Implements a simple decorator-based model registry for dynamically managing
3
+ different artifact classes.
4
+
5
+ This system allows concrete subclasses to register themselves
6
+ using a specific string key, enabling the application to instantiate the
7
+ correct class based on a configuration setting without explicit imports.
8
+ """
9
+
10
+ ARTIFACT_REGISTRY = {}
11
+
12
+ def register_model(name):
13
+ """
14
+ A decorator factory used to register a subclass.
15
+
16
+ The decorated class is stored in the global ARTIFACT_REGISTRY dictionary
17
+ under the provided `name`.
18
+
19
+ Args:
20
+ name (str): The string key used to reference the model class
21
+ in the configuration.
22
+
23
+ Returns:
24
+ Callable: A decorator function that takes a class and registers it.
25
+ """
26
+ def decorator(cls):
27
+ ARTIFACT_REGISTRY[name] = cls
28
+ return cls
29
+ return decorator
30
+
31
+
32
+ def get_class(artifact_name):
33
+ """
34
+ Retrieves and instantiates the correct class based on the
35
+ configuration dictionary.
36
+
37
+ It looks up the class in ARTIFACT_REGISTRY using `artifact_name`.
38
+
39
+ Args:
40
+ artifact_name: Must correspond to a registered artifact name.
41
+
42
+ Returns:
43
+ An instantiated object of the registered class.
44
+
45
+ Raises:
46
+ KeyError: If the value of `artifact_name` is not found in the
47
+ ARTIFACT_REGISTRY.
48
+ """
49
+ Class_ = ARTIFACT_REGISTRY[artifact_name]
50
+ artifact = Class_()
51
+
52
+ return model_object
@@ -0,0 +1,76 @@
1
+ import os
2
+ import platform
3
+ import subprocess
4
+ import datetime
5
+ import soundfile as sf
6
+
7
+ from basejenerator.artifacts.base_artifact import BaseArtifact
8
+
9
+
10
+ class WavArtifact(BaseArtifact):
11
+ """
12
+
13
+ Attributes:
14
+ data (str): The gTTS "model" object.
15
+ item_extras (dict): any additional metadata related to this artifact
16
+ """
17
+
18
+ def __init__(self, data, item_extras):
19
+ self.data = data
20
+ self.item_extras = item_extras
21
+ self.extension = ".wav"
22
+ self.system = platform.system()
23
+ self.linux_players = [
24
+ ["mpv", "--no-terminal"],
25
+ ["mpg123", "--quiet"],
26
+
27
+ ]
28
+ self.player_funcs = {
29
+ "Windows": self._play_on_windows,
30
+ "Darwin": self._play_on_mac,
31
+ "Linux": self._play_on_linux
32
+ }
33
+
34
+
35
+ def save(self, path):
36
+ """
37
+ """
38
+ if "chunk_index" in self.item_extras:
39
+ extension = f"_{str(self.item_extras['chunk_index'])}{self.extension}"
40
+ else:
41
+ extension = self.extension
42
+
43
+ self.path = f'{path}{extension}'
44
+ sf.write(self.path, self.data.data, 24000)
45
+
46
+
47
+ def _play_on_windows(self, path):
48
+ os.startfile(path)
49
+
50
+
51
+ def _play_on_mac(self, path):
52
+ subprocess.run(['afplay', path])
53
+
54
+
55
+ def _play_on_linux(self, path):
56
+ for player_command in self.linux_players:
57
+ player_command.append(path)
58
+ try:
59
+ subprocess.run(player_command, check=True)
60
+ break
61
+ except FileNotFoundError:
62
+ continue
63
+
64
+
65
+ def play(self, path=None):
66
+ """
67
+ """
68
+ if not path:
69
+ path = self.data["config"]["path"]
70
+ self.save(path)
71
+
72
+ player = self.player_funcs[self.system]
73
+ try:
74
+ player(self.path)
75
+ except Exception as e:
76
+ print(f"Error playing audio: {e}")
@@ -0,0 +1,121 @@
1
+ from dataclasses import dataclass, fields, field
2
+ import csv
3
+ import os
4
+ from typing import List
5
+
6
+ @dataclass
7
+ class BaseClassRecord():
8
+ """
9
+ A data container for tracking and saving inference metadata.
10
+
11
+ This class serves as a structured record for a single inference event.
12
+ It includes functionality to serialize its own data to a CSV file, handling
13
+ formatting (e.g., float precision) and file management (creation vs. appending).
14
+
15
+ Attributes:
16
+ gen_data_file_path (str): Path to the CSV file where records are saved.
17
+ filename (str): Name of the generated file.
18
+ timestamp (str): Timestamp of generation.
19
+ model (str): Name of the model used.
20
+ device (str): Compute device used (e.g., 'cuda', 'cpu').
21
+ dtype (str): Data type used (e.g., 'float16').
22
+ total_generation_time (float): Total time taken
23
+ rating (float): Placeholder for user rating (defaults to -1).
24
+ fields_to_not_save (list[str]): Internal list of field names to exclude from the CSV.
25
+ """
26
+ gen_data_file_path: str = ""
27
+ filename: str = ""
28
+ timestamp: str = ""
29
+ model: str = ""
30
+ device: str = ""
31
+ dtype: str = ""
32
+ total_generation_time: float = 0
33
+ rating: float = -1
34
+ fields_to_not_save: list[str] = field(default_factory=list)
35
+
36
+
37
+ def __post_init__(self):
38
+ """
39
+ Post-initialization hook to define excluded fields.
40
+
41
+ Populates `fields_to_not_save` with technical fields that should not
42
+ appear in the final CSV report (e.g., file paths or the exclusion list itself).
43
+ """
44
+ self.fields_to_not_save += ["gen_data_file_path", "fields_to_not_save", "path_to_csv"]
45
+
46
+
47
+ def create_data_row(self):
48
+ """
49
+ Constructs a list of values for the CSV row based on current attributes.
50
+
51
+ Iterates through the dataclass fields, filtering out those in `fields_to_not_save`.
52
+ Floats are formatted to 2 decimal places.
53
+
54
+ Returns:
55
+ list: A list of values corresponding to the CSV columns.
56
+ """
57
+ row = []
58
+ for field in fields(self):
59
+ if field.name in self.fields_to_not_save:
60
+ continue
61
+ field_value = getattr(self, field.name)
62
+
63
+ if isinstance(field_value, float):
64
+ row.append(f"{field_value:.2f}")
65
+ else:
66
+ row.append(field_value)
67
+ return row
68
+
69
+
70
+ def append_data_to_csv(self):
71
+ """
72
+ Appends the current record to the CSV file.
73
+
74
+ Opens the file at `gen_data_file_path` in append mode and writes
75
+ the data row generated by `create_data_row()`.
76
+ """
77
+ with open(self.gen_data_file_path, "a", newline="") as f:
78
+ writer = csv.writer(f)
79
+ writer.writerow(self.create_data_row())
80
+
81
+
82
+ def create_header_row(self):
83
+ """
84
+ Generates the CSV header row based on field names.
85
+
86
+ Returns:
87
+ list[str]: A list of column names, excluding those in `fields_to_not_save`.
88
+ """
89
+ headers = []
90
+ for header in fields(self):
91
+ if header.name in self.fields_to_not_save:
92
+ continue
93
+ headers.append(header.name)
94
+ return headers
95
+
96
+
97
+ def create_new_stats_file(self):
98
+ """
99
+ Creates a new CSV file and writes the header row.
100
+
101
+ Uses 'x' mode to open the file, which ensures a new file is created
102
+ and raises an error if the file already exists (though `save_data()` guards against this).
103
+ """
104
+ with open(self.gen_data_file_path, "x", newline="") as f:
105
+ writer = csv.writer(f)
106
+ writer.writerow(self.create_header_row())
107
+
108
+
109
+ def save_data(self):
110
+ """
111
+ Saves the record of the generation stats to disk.
112
+
113
+ Checks if the target CSV file exists:
114
+ - If no: Creates the file and writes headers.
115
+ - If yes: Appends the current data row.
116
+ """
117
+ if not os.path.exists(self.gen_data_file_path):
118
+ self.create_new_stats_file()
119
+
120
+ self.append_data_to_csv()
121
+
@@ -0,0 +1,114 @@
1
+ from abc import ABC, abstractmethod
2
+ import datetime
3
+ import time
4
+ import os
5
+ import random
6
+ import torch
7
+ import gc
8
+
9
+ from basejenerator.base_generator import BaseGenerator
10
+
11
+
12
+ class BaseSpeechGenerator(BaseGenerator):
13
+ """
14
+ Abstract base class for TTS.
15
+
16
+ This class handles...
17
+
18
+ Subclasses must implement create_pipeline() and run_pipeline()
19
+
20
+ Attributes:
21
+ config (dict): Configuration dictionary containing model parameters, paths, and settings.
22
+ """
23
+
24
+ def __init__(self, config):
25
+ """
26
+ Initializes the object with a config.
27
+
28
+ Args:
29
+ config (dict): A dictionary containing configuration parameters.
30
+ Expected keys include:
31
+ -
32
+ """
33
+ self.config = config
34
+ self.device = self.set_device()
35
+
36
+
37
+ def set_device(self):
38
+ """
39
+ Sets the computation device based on CUDA availability.
40
+
41
+ Sets `self.device` to 'cuda' if available, otherwise defaults to 'cpu'.
42
+ """
43
+ if "device" in self.config:
44
+ return self.config["device"]
45
+
46
+ if torch.cuda.is_available():
47
+ return "cuda"
48
+ else:
49
+ return "cpu"
50
+
51
+
52
+ @abstractmethod
53
+ def load(self):
54
+ """
55
+ Abstract method to initialize the model pipeline.
56
+
57
+ Subclasses must implement this to load the specific model and tokenizer/pipeline object, assigning it to self.pipe (e.g., a Hugging Face Pipeline object).
58
+ """
59
+ pass
60
+
61
+
62
+ @abstractmethod
63
+ def prepare(self):
64
+ """
65
+ Reset lifecycle without tearing down the model - e.g., clear cache, etc.
66
+ """
67
+ pass
68
+
69
+
70
+ @abstractmethod
71
+ def warmup(self):
72
+ """
73
+ Initial inference run to create cache etc., mainly for benchmarking.
74
+ """
75
+ pass
76
+
77
+
78
+ def generate(self):
79
+ """
80
+ The public API that runs inference.
81
+
82
+ Returns:
83
+ GeneratorOutput
84
+ """
85
+ return self.generate_impl()
86
+
87
+
88
+ @abstractmethod
89
+ def generate_impl(self):
90
+ """
91
+ The public API that runs inference.
92
+
93
+ Returns:
94
+ GeneratorOutput
95
+ """
96
+ pass
97
+
98
+
99
+ def teardown(self):
100
+ """
101
+ Deletes the pipeline, empties the torch cache, and forces Python's garbage collector to run. Clears the slate to create
102
+ another pipeline.
103
+ """
104
+ if self.model is None:
105
+ print("No pipeline found. You cannot teardown that which was not created.")
106
+ return
107
+
108
+ del self.model
109
+ self.model = None
110
+
111
+ if torch.cuda.is_available():
112
+ torch.cuda.empty_cache()
113
+
114
+ gc.collect()
@@ -0,0 +1,17 @@
1
+ from speechjenerator import registry
2
+
3
+ output_path = "./outputs/test.wav"
4
+ config = {
5
+ "model": "gtts",
6
+
7
+ "text": "Hazlo o no lo hagas. Intentar no existe.",
8
+ "lang": "es"
9
+
10
+ }
11
+
12
+ speech_generator = registry.get_model_class(config)
13
+ speech_generator.load()
14
+ print(speech_generator.config)
15
+ output = speech_generator.generate()
16
+ speech = output.batch[0]
17
+ speech.play(output_path)
@@ -0,0 +1,26 @@
1
+ from speechjenerator import registry
2
+ import datetime
3
+
4
+
5
+ output_path = "./outputs/"
6
+ config = {
7
+ "model": "kokoro",
8
+ "text": """Do, or do not. There is no try.""",
9
+ "lang": "a",
10
+ "voice": "af_heart",
11
+ }
12
+
13
+
14
+ filename_stem = datetime.datetime.now().strftime(format="%Y-%m-%d_%H-%M-%s")
15
+ speech_generator = registry.get_model_class(config)
16
+ print("loading model...")
17
+ speech_generator.load()
18
+ print("warmup run...")
19
+ speech_generator.warmup()
20
+ print("running inference...")
21
+ output = speech_generator.generate()
22
+ for chunk in output.batch:
23
+ chunk.play(output_path + filename_stem)
24
+ print("tearing down...")
25
+ speech_generator.teardown()
26
+ print("done!")
@@ -0,0 +1,6 @@
1
+ # import pkgutil
2
+ # import importlib
3
+
4
+ # for module in pkgutil.iter_modules(__path__):
5
+ # importlib.import_module(f"{__name__}.{module.name}")
6
+
@@ -0,0 +1,88 @@
1
+ import time
2
+ import datetime
3
+
4
+ from kokoro import KPipeline
5
+
6
+ from basejenerator.generator_output import GeneratorOutput
7
+
8
+ from speechjenerator.artifacts.wav_artifact import WavArtifact
9
+ from speechjenerator.registry import register_model
10
+ from speechjenerator.core.base_speech_generator import BaseSpeechGenerator
11
+
12
+ @register_model("kokoro")
13
+ class Kokoro(BaseSpeechGenerator):
14
+ """
15
+ Concrete implementation of BaseClass for Kokoro.
16
+
17
+ """
18
+
19
+ def __init__(self, config):
20
+ """
21
+ Initializes the generator.
22
+
23
+ Args:
24
+ config (dict): Configuration dictionary. Must include standard BaseClass
25
+ keys plus model-specific keys.
26
+ """
27
+ super().__init__(config)
28
+ self.model = None
29
+
30
+
31
+ def load(self):
32
+ """
33
+ Loads Kokoro pipeline.
34
+ """
35
+ pipeline = KPipeline(
36
+ lang_code=self.config["lang"],
37
+ device=self.device,
38
+ )
39
+
40
+ self.model = pipeline
41
+
42
+
43
+ def prepare(self):
44
+ """
45
+ """
46
+ pass
47
+
48
+
49
+ def warmup(self):
50
+ self.generate_impl({
51
+ "text": "Do or do not. There is no try."
52
+ })
53
+
54
+
55
+ def generate_impl(self, config = None):
56
+ """
57
+ """
58
+ inf_config = self.config.copy()
59
+ if config:
60
+ inf_config.update(config)
61
+
62
+ artifacts = []
63
+ for i, (gs, ps, audio) in enumerate(
64
+ self.model(
65
+ text=inf_config["text"],
66
+ voice=inf_config["voice"]
67
+ )
68
+ ):
69
+ artifacts.append(
70
+ WavArtifact(
71
+ data=audio,
72
+ item_extras={
73
+ "text": gs,
74
+ "phonemes": ps,
75
+ "chunk_index": i
76
+ }
77
+ )
78
+ )
79
+
80
+ return GeneratorOutput(artifacts)
81
+
82
+
83
+ def get_params_schema(self):
84
+ class ParamsSchema(BaseModel):
85
+ text: str = ""
86
+ lang: str = ""
87
+
88
+ return ParamsSchema
@@ -0,0 +1,54 @@
1
+ """
2
+ Implements a simple decorator-based model registry for dynamically managing
3
+ different model classes.
4
+
5
+ This system allows concrete subclasses to register themselves
6
+ using a specific string key, enabling the application to instantiate the
7
+ correct class based on a configuration setting without explicit imports.
8
+ """
9
+
10
+ MODEL_REGISTRY = {}
11
+
12
+ def register_model(name):
13
+ """
14
+ A decorator factory used to register a subclass.
15
+
16
+ The decorated class is stored in the global MODEL_REGISTRY dictionary
17
+ under the provided `name`.
18
+
19
+ Args:
20
+ name (str): The string key used to reference the model class
21
+ in the configuration.
22
+
23
+ Returns:
24
+ Callable: A decorator function that takes a class and registers it.
25
+ """
26
+ def decorator(cls):
27
+ MODEL_REGISTRY[name] = cls
28
+ return cls
29
+ return decorator
30
+
31
+
32
+ def get_model_class(config):
33
+ """
34
+ Retrieves and instantiates the correct class based on the
35
+ configuration dictionary.
36
+
37
+ It looks up the class in MODEL_REGISTRY using the key found in
38
+ `config['model']`.
39
+
40
+ Args:
41
+ config (dict): The configuration dictionary, which must contain a
42
+ 'model' key corresponding to a registered model name.
43
+
44
+ Returns:
45
+ BaseClass: An instantiated object of the registered generator class.
46
+
47
+ Raises:
48
+ KeyError: If the value of `config['model']` is not found in the
49
+ MODEL_REGISTRY.
50
+ """
51
+ ModelClass = MODEL_REGISTRY[config["model"]]
52
+ model_object = ModelClass(config)
53
+
54
+ return model_object
@@ -0,0 +1,6 @@
1
+ # import pkgutil
2
+ # import importlib
3
+
4
+ # for module in pkgutil.iter_modules(__path__):
5
+ # importlib.import_module(f"{__name__}.{module.name}")
6
+
@@ -0,0 +1,80 @@
1
+ import time
2
+ import datetime
3
+
4
+ from gtts import gTTS
5
+
6
+ from basejenerator.generator_output import GeneratorOutput
7
+
8
+ from speechjenerator.artifacts.gtts_artifact import GTTSArtifact
9
+ from speechjenerator.registry import register_model
10
+ from speechjenerator.core.base_speech_generator import BaseSpeechGenerator
11
+
12
+
13
+ @register_model("gtts")
14
+ class GTTS(BaseSpeechGenerator):
15
+ """
16
+ Concrete implementation of BaseClass for gTTS.
17
+
18
+ """
19
+
20
+ def __init__(self, config):
21
+ """
22
+ Initializes the gTTS generator.
23
+
24
+ Args:
25
+ config (dict): Configuration dictionary. Must include standard BaseClass
26
+ keys plus model-specific keys.
27
+ """
28
+ super().__init__(config)
29
+
30
+
31
+ def load(self):
32
+ """
33
+ No-op - gTTS uses a remote API (Google Translate).
34
+ """
35
+ pass
36
+
37
+
38
+ def prepare(self):
39
+ """
40
+ No-op - gTTS uses a remote API (Google Translate).
41
+ """
42
+ pass
43
+
44
+
45
+ def warmup(self):
46
+ """
47
+ No-op - gTTS uses a remote API (Google Translate).
48
+ """
49
+ pass
50
+
51
+
52
+ def generate_impl(self, text=None, lang=None):
53
+ """
54
+ """
55
+ if not text:
56
+ text = self.config["text"]
57
+
58
+ if not lang:
59
+ lang = self.config["lang"]
60
+
61
+ gtts_obj = gTTS(text=text, lang=lang)
62
+
63
+ artifacts = self._quick_wrap([gtts_obj], [{}], GTTSArtifact)
64
+
65
+ return GeneratorOutput(artifacts)
66
+
67
+
68
+ def teardown(self):
69
+ """
70
+ No-op - gTTS uses a remote API (Google Translate).
71
+ """
72
+ pass
73
+
74
+
75
+ def get_params_schema(self):
76
+ class ParamsSchema(BaseModel):
77
+ text: str = ""
78
+ lang: str = ""
79
+
80
+ return ParamsSchema