-
Notifications
You must be signed in to change notification settings - Fork 325
Expand file tree
/
Copy pathconfig.py
More file actions
929 lines (822 loc) · 39.5 KB
/
Copy pathconfig.py
File metadata and controls
929 lines (822 loc) · 39.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
# File: config.py
# Manages application configuration using a YAML file (config.yaml).
# Handles loading, saving, and providing access to configuration settings.
import os
import logging
import yaml
import shutil
from copy import deepcopy
from threading import Lock
from typing import Dict, Any, Optional, List, Tuple
import torch # For automatic CUDA/CPU device detection
from pathlib import Path
# Standard logger setup
logger = logging.getLogger(__name__)
# --- File Path Constants ---
# Defines the primary configuration file name.
CONFIG_FILE_PATH = Path("config.yaml")
# --- Default Directory Paths ---
# These paths are used if not specified in config.yaml and are created if they don't exist
# when a default configuration file is generated.
DEFAULT_LOGS_PATH = Path("logs")
DEFAULT_VOICES_PATH = Path("voices") # For predefined voice samples
DEFAULT_REFERENCE_AUDIO_PATH = Path(
"reference_audio"
) # For user-uploaded reference audio
DEFAULT_MODEL_FILES_PATH = Path("./model_cache") # For downloaded model files
DEFAULT_OUTPUT_PATH = Path("./outputs") # For server-saved audio outputs (if any)
# --- Default Configuration Structure ---
# This dictionary defines the complete expected structure of 'config.yaml',
# including default values for all settings. It serves as the template for
# creating a new config.yaml if one does not exist.
DEFAULT_CONFIG: Dict[str, Any] = {
"server": {
"host": "0.0.0.0", # Host address for the server to listen on.
"port": 8000, # Port number for the server.
"use_ngrok": False, # Placeholder for ngrok integration (if used).
"use_auth": False, # Placeholder for basic authentication (if used).
"auth_username": "user", # Default username if authentication is enabled.
"auth_password": "password", # Default password if authentication is enabled.
"log_file_path": str(
DEFAULT_LOGS_PATH / "tts_server.log"
), # Path to the server log file.
"log_file_max_size_mb": 10, # Maximum size of a single log file before rotation.
"log_file_backup_count": 5, # Number of backup log files to keep.
"ssl_certfile": None, # Path to SSL certificate file for HTTPS. None = HTTP only.
"ssl_keyfile": None, # Path to SSL private key file for HTTPS. None = HTTP only.
},
"model": { # Added section for model source configuration
"repo_id": "chatterbox-turbo", # UPDATED: Default to Turbo model
},
"tts_engine": {
"device": "auto", # TTS processing device: 'auto', 'cuda', 'mps', or 'cpu'.
# 'auto' will attempt to use 'cuda' if available, then 'mps' if available, otherwise 'cpu'.
"predefined_voices_path": str(
DEFAULT_VOICES_PATH
), # Directory for predefined voice files.
"reference_audio_path": str(
DEFAULT_REFERENCE_AUDIO_PATH
), # Directory for reference audio files for cloning.
"default_voice_id": "default_sample.wav", # Default voice file to use if none is specified.
},
"paths": { # General configurable paths for the application.
"model_cache": str(
DEFAULT_MODEL_FILES_PATH
), # Directory for caching or storing downloaded models.
"output": str(
DEFAULT_OUTPUT_PATH
), # Default directory for any output files generated by the server.
},
"generation_defaults": { # Default parameters for TTS audio generation.
"temperature": 0.8, # Controls randomness: lower is more deterministic.
"exaggeration": 0.5, # Controls expressiveness or exaggeration in speech.
"cfg_weight": 0.5, # Classifier-Free Guidance weight, influences adherence to prompt/style.
"seed": 0, # Random seed for generation. 0 often means random or engine default.
"speed_factor": 1.0, # Controls the speed of the generated speech.
"language": "en", # Default language for TTS.
},
"audio_output": { # Settings related to the format of generated audio.
"format": "wav", # Output audio format (e.g., 'wav', 'mp3').
"sample_rate": 24000, # Sample rate of the output audio in Hz.
"max_reference_duration_sec": 30, # Maximum duration for reference audio files.
"save_to_disk": False, # If true, save generated audio files to disk in outputs folder.
},
"ui_state": { # Stores user interface preferences and last-used values.
"last_text": "", # Last text entered by the user.
"last_voice_mode": "predefined", # Last selected voice mode ('predefined' or 'clone').
"last_predefined_voice": None, # Filename of the last used predefined voice.
"last_reference_file": None, # Filename of the last used reference audio file.
"last_seed": 0, # Last used generation seed.
"last_chunk_size": 120, # Last used chunk size for text splitting in UI.
"last_split_text_enabled": True, # Whether text splitting was last enabled in UI.
"hide_chunk_warning": False, # Flag to hide the chunking warning modal.
"hide_generation_warning": False, # Flag to hide the general generation quality notice modal.
"theme": "dark", # Default UI theme ('dark' or 'light').
},
"ui": { # General UI display settings.
"title": "Chatterbox TTS Server", # Title displayed in the web UI.
"show_language_select": True, # Whether to show language selection in the UI.
"max_predefined_voices_in_dropdown": 20, # Max predefined voices to list in UI dropdown.
},
"debug": { # Settings for debugging purposes
"save_intermediate_audio": False # If true, save intermediate audio files for debugging
},
}
def _ensure_default_paths_exist():
"""
Creates the default directories specified in DEFAULT_CONFIG if they do not already exist.
This is typically called when generating a default config.yaml file.
"""
paths_to_check = [
Path(
DEFAULT_CONFIG["server"]["log_file_path"]
).parent, # Log file's parent directory
Path(DEFAULT_CONFIG["tts_engine"]["predefined_voices_path"]),
Path(DEFAULT_CONFIG["tts_engine"]["reference_audio_path"]),
Path(DEFAULT_CONFIG["paths"]["model_cache"]),
Path(DEFAULT_CONFIG["paths"]["output"]),
]
for path in paths_to_check:
try:
path.mkdir(parents=True, exist_ok=True)
except Exception as e:
logger.error(f"Error creating default directory {path}: {e}", exc_info=True)
def _deep_merge_dicts(source: Dict, destination: Dict) -> Dict:
"""
Recursively merges the 'source' dictionary into the 'destination' dictionary.
Keys from 'source' will overwrite existing keys in 'destination'.
If a key in 'source' corresponds to a dictionary, a recursive merge is performed.
The 'destination' dictionary is modified in place.
"""
for key, value in source.items():
if isinstance(value, dict):
node = destination.setdefault(key, {})
if isinstance(
node, dict
): # Ensure the destination node is a dict for merging
_deep_merge_dicts(value, node)
else: # If destination's node is not a dict, overwrite it entirely
destination[key] = deepcopy(value)
else:
destination[key] = value
return destination
def _set_nested_value(d: Dict, keys: List[str], value: Any):
"""Helper function to set a value in a nested dictionary using a list of keys."""
for key in keys[:-1]:
d = d.setdefault(key, {})
d[keys[-1]] = value
def _get_nested_value(d: Dict, keys: List[str], default: Any = None) -> Any:
"""Helper function to get a value from a nested dictionary using a list of keys."""
for key in keys:
if isinstance(d, dict) and key in d:
d = d[key]
else:
return default
return d
class YamlConfigManager:
"""
Manages application configuration stored in a YAML file.
This class handles loading, saving, updating, and resetting the configuration.
Operations are thread-safe for file writing.
"""
def __init__(self):
"""Initializes the configuration manager by loading the configuration from YAML."""
self.config: Dict[str, Any] = {}
self._lock = Lock() # Ensures thread-safety for file write operations.
self.load_config()
def _load_defaults(self) -> Dict[str, Any]:
"""
Returns a deep copy of the hardcoded default configuration structure.
Also ensures that default directory paths defined in the structure exist.
"""
_ensure_default_paths_exist() # Create necessary default directories.
return deepcopy(DEFAULT_CONFIG)
def _resolve_paths_and_device(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Resolves device settings (e.g., 'auto' to 'cuda' or 'cpu') and converts
string paths in the configuration data to Path objects for internal use.
The 'config_data' dictionary is modified in place.
"""
# Resolve TTS device setting with robust CUDA detection.
current_device_setting = _get_nested_value(
config_data, ["tts_engine", "device"], "auto"
)
if current_device_setting == "auto":
resolved_device = self._detect_best_device()
_set_nested_value(config_data, ["tts_engine", "device"], resolved_device)
elif current_device_setting not in ["cuda", "mps", "cpu"]:
logger.warning(
f"Invalid TTS device '{current_device_setting}' in configuration. "
f"Defaulting to auto-detection."
)
resolved_device = self._detect_best_device()
_set_nested_value(config_data, ["tts_engine", "device"], resolved_device)
final_device = _get_nested_value(config_data, ["tts_engine", "device"])
logger.info(f"TTS processing device resolved to: {final_device}")
# Convert relevant string paths to Path objects.
path_key_map_for_conversion = {
"server": ["log_file_path"],
"tts_engine": ["predefined_voices_path", "reference_audio_path"],
"paths": ["model_cache", "output"],
}
for section, keys_list in path_key_map_for_conversion.items():
if section in config_data:
for key in keys_list:
current_path_val = _get_nested_value(config_data, [section, key])
if isinstance(current_path_val, str):
_set_nested_value(
config_data, [section, key], Path(current_path_val)
)
return config_data
def _detect_best_device(self) -> str:
"""
Robustly detects the best available device for TTS processing.
Tests actual CUDA/MPS functionality rather than just checking availability.
Returns:
str: 'cuda' if CUDA is truly functional, 'mps' if MPS is functional, 'cpu' otherwise.
"""
# Test CUDA first as it's generally preferred for ML workloads
if torch.cuda.is_available():
try:
# Actually test CUDA functionality by creating a tensor and moving it to CUDA
test_tensor = torch.tensor([1.0])
test_tensor = test_tensor.cuda()
test_tensor = test_tensor.cpu() # Clean up
logger.info("CUDA test successful. Using CUDA device.")
return "cuda"
except Exception as e:
logger.warning(
f"CUDA is reported as available but failed functionality test: {e}. "
f"This usually means PyTorch was not compiled with CUDA support."
)
# Test MPS if CUDA is not available or failed
if torch.backends.mps.is_available():
try:
# Actually test MPS functionality by creating a tensor and moving it to MPS
test_tensor = torch.tensor([1.0])
test_tensor = test_tensor.to("mps")
test_tensor = test_tensor.cpu() # Clean up
logger.info("MPS test successful. Using MPS device.")
return "mps"
except Exception as e:
logger.warning(
f"MPS is reported as available but failed functionality test: {e}. "
f"This usually means PyTorch was not compiled with MPS support."
)
logger.info("Neither CUDA nor MPS is available or functional. Using CPU.")
return "cpu"
def _prepare_config_for_saving(self, config_dict: Dict[str, Any]) -> Dict[str, Any]:
"""
Prepares a configuration dictionary for YAML serialization by converting
internal Path objects back to their string representations.
Returns a deep copy of the configuration dictionary with paths as strings.
"""
config_copy_for_saving = deepcopy(config_dict)
path_key_map_for_conversion = {
"server": ["log_file_path"],
"tts_engine": ["predefined_voices_path", "reference_audio_path"],
"paths": ["model_cache", "output"],
}
for section, keys_list in path_key_map_for_conversion.items():
if section in config_copy_for_saving:
for key in keys_list:
current_path_val = _get_nested_value(
config_copy_for_saving, [section, key]
)
if isinstance(current_path_val, Path):
_set_nested_value(
config_copy_for_saving,
[section, key],
str(current_path_val),
)
return config_copy_for_saving
def load_config(self):
"""
Loads the application configuration from 'config.yaml'.
If the file doesn't exist, it's created using defaults.
The loaded configuration is merged with defaults to ensure all expected keys are present.
Device settings and path types are resolved after loading.
"""
with self._lock: # Ensure thread-safe loading.
base_defaults = self._load_defaults() # Ensures default paths exist.
if CONFIG_FILE_PATH.exists():
logger.info(f"Loading configuration from: {CONFIG_FILE_PATH}")
try:
with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f:
yaml_data = yaml.safe_load(f)
if isinstance(yaml_data, dict):
# Merge loaded YAML data into a copy of defaults.
# YAML data takes precedence, defaults fill in missing parts.
effective_config = deepcopy(base_defaults)
_deep_merge_dicts(yaml_data, effective_config)
self.config = effective_config
logger.info(
f"Successfully loaded and merged configuration from {CONFIG_FILE_PATH}."
)
else:
logger.error(
f"Invalid format in {CONFIG_FILE_PATH}. Expected a dictionary. "
f"Using defaults and attempting to overwrite the invalid file."
)
self.config = base_defaults # Fallback to defaults.
if not self._save_config_yaml_internal(self.config):
logger.error(
f"Failed to overwrite invalid {CONFIG_FILE_PATH} with defaults."
)
except yaml.YAMLError as e:
logger.error(
f"Error parsing YAML from {CONFIG_FILE_PATH}: {e}. "
f"Using defaults and attempting to overwrite the corrupted file."
)
self.config = base_defaults
if not self._save_config_yaml_internal(self.config):
logger.error(
f"Failed to overwrite corrupted {CONFIG_FILE_PATH} with defaults."
)
except Exception as e:
logger.error(
f"Unexpected error loading {CONFIG_FILE_PATH}: {e}. Using in-memory defaults.",
exc_info=True,
)
self.config = base_defaults # Use defaults, avoid saving on unexpected errors.
else:
logger.info(
f"{CONFIG_FILE_PATH} not found. Creating initial configuration using defaults..."
)
# Start with defaults.
self.config = base_defaults
if self._save_config_yaml_internal(self.config):
logger.info(
f"Successfully created and saved initial default configuration to {CONFIG_FILE_PATH}."
)
else:
logger.error(
f"Failed to save initial configuration to {CONFIG_FILE_PATH}. "
f"Using in-memory defaults."
)
# Resolve device and convert path strings to Path objects for the loaded/created config.
self.config = self._resolve_paths_and_device(self.config)
logger.debug(f"Current configuration loaded and resolved: {self.config}")
return self.config
def _save_config_yaml_internal(self, config_dict_to_save: Dict[str, Any]) -> bool:
"""
Internal method to save the provided configuration dictionary to 'config.yaml'.
It includes a backup and restore mechanism for safety during writes.
Assumes the caller holds the necessary lock.
Converts Path objects to strings before YAML serialization.
"""
# Prepare the configuration for saving (e.g., convert Path objects to strings).
prepared_config_for_yaml = self._prepare_config_for_saving(config_dict_to_save)
temp_file = CONFIG_FILE_PATH.with_suffix(CONFIG_FILE_PATH.suffix + ".tmp")
backup_file = CONFIG_FILE_PATH.with_suffix(CONFIG_FILE_PATH.suffix + ".bak")
try:
# Atomically write to a temporary file first.
with open(temp_file, "w", encoding="utf-8") as f:
yaml.dump(
prepared_config_for_yaml,
f,
default_flow_style=False,
sort_keys=False,
indent=2,
)
# If an existing config file exists, back it up.
if CONFIG_FILE_PATH.exists():
try:
shutil.copy2(str(CONFIG_FILE_PATH), str(backup_file))
logger.debug(f"Backed up existing configuration to {backup_file}")
except Exception as backup_error:
logger.debug(
f"Could not create backup of {CONFIG_FILE_PATH}: {backup_error}"
)
# Non-fatal: proceed with saving even without a backup.
# Replace the config file with the new content.
# Use copy+remove instead of move for compatibility with Docker bind mounts.
shutil.copy2(str(temp_file), str(CONFIG_FILE_PATH))
try:
os.remove(str(temp_file))
except OSError:
pass # Temp file cleanup is best-effort
logger.info(f"Configuration successfully saved to {CONFIG_FILE_PATH}")
return True
except yaml.YAMLError as e_yaml:
logger.error(
f"Error formatting data for {CONFIG_FILE_PATH} (YAML error): {e_yaml}",
exc_info=True,
)
return False
except Exception as e_general:
logger.error(
f"Failed to save configuration to {CONFIG_FILE_PATH}: {e_general}",
exc_info=True,
)
# Attempt to restore from backup if the save operation failed.
if backup_file.exists() and not CONFIG_FILE_PATH.exists():
try:
shutil.copy2(str(backup_file), str(CONFIG_FILE_PATH))
logger.info(
f"Restored configuration from backup {backup_file} due to save failure."
)
except Exception as restore_error:
logger.error(
f"Failed to restore configuration from backup: {restore_error}"
)
# Clean up the temporary file if it still exists after a failure.
if temp_file.exists():
try:
os.remove(str(temp_file))
except Exception as remove_error:
logger.warning(
f"Could not remove temporary config file {temp_file}: {remove_error}"
)
return False
finally:
# Clean up the backup file if the main configuration file exists and the save was successful.
if CONFIG_FILE_PATH.exists() and backup_file.exists():
try:
if (
CONFIG_FILE_PATH.stat().st_size > 0
): # Basic check that the new file is not empty.
os.remove(str(backup_file))
logger.debug(
f"Removed backup file {backup_file} after successful save."
)
except Exception as remove_bak_error:
logger.warning(
f"Could not remove backup config file {backup_file}: {remove_bak_error}"
)
def save_config_yaml(self) -> bool:
"""
Public method to save the current in-memory configuration to 'config.yaml'.
Ensures thread-safety using a lock.
"""
with self._lock:
return self._save_config_yaml_internal(self.config)
def get(self, key_path: str, default: Any = None) -> Any:
"""
Retrieves a configuration value using a dot-separated key path (e.g., 'server.port').
If the key path is not found, 'default' is returned.
For mutable types (dicts, lists), a deep copy is returned to prevent
unintended modification of the in-memory configuration.
"""
keys = key_path.split(".")
with self._lock: # Ensure thread-safe access to self.config.
value = _get_nested_value(self.config, keys, default)
return deepcopy(value) if isinstance(value, (dict, list)) else value
def get_string(self, key_path: str, default: Optional[str] = None) -> str:
"""Retrieves a configuration value, ensuring it's a string."""
# Added this method for explicit string retrieval, common for paths/IDs.
raw_value = self.get(key_path)
if raw_value is None:
if default is not None:
logger.debug(
f"Config string '{key_path}' is None, using provided method default: '{default}'"
)
return default
logger.error(
f"Mandatory string config '{key_path}' is None, and no method default. Returning empty string."
)
return ""
if isinstance(
raw_value, (Path, str)
): # Handle Path objects by converting to string
return str(raw_value)
try: # Attempt conversion for other types if necessary
return str(raw_value)
except Exception:
logger.warning(
f"Could not convert value '{raw_value}' for '{key_path}' to string. Using method default or empty string."
)
if default is not None:
return default
return ""
def get_all(self) -> Dict[str, Any]:
"""
Returns a deep copy of the entire current configuration.
Ensures thread-safety during the copy operation.
"""
with self._lock:
return deepcopy(self.config)
def update_and_save(self, partial_update_dict: Dict[str, Any]) -> bool:
"""
Deeply merges a 'partial_update_dict' into the current configuration
and saves the entire updated configuration back to the YAML file.
This allows updating specific nested values without overwriting entire sections.
"""
if not isinstance(partial_update_dict, dict):
logger.error("Invalid partial update data: input must be a dictionary.")
return False
with self._lock:
try:
# Work on a deep copy of the current config to avoid altering it before a successful save.
config_copy_for_update = deepcopy(self.config)
# Merge the partial update into this copy.
_deep_merge_dicts(partial_update_dict, config_copy_for_update)
# Before saving, the merged config might need path/device re-resolution
# if those specific keys were part of partial_update_dict.
# For robustness, always re-resolve.
resolved_updated_config = self._resolve_paths_and_device(
config_copy_for_update
)
if self._save_config_yaml_internal(resolved_updated_config):
# If save was successful, update the active in-memory config.
self.config = resolved_updated_config
logger.info(
"Configuration updated, saved, and re-resolved successfully."
)
return True
else:
logger.error("Failed to save updated configuration after merging.")
return False
except Exception as e:
logger.error(
f"Error during configuration update and save process: {e}",
exc_info=True,
)
return False
def reset_and_save(self) -> bool:
"""
Resets the application configuration to its hardcoded defaults.
The reset configuration (after resolving paths/device) is then saved to 'config.yaml'.
"""
with self._lock:
logger.warning("Initiating configuration reset to hardcoded defaults...")
# Start with hardcoded defaults (this also ensures default directories are created).
reset_config_base = self._load_defaults()
# Resolve device settings and ensure paths are Path objects for the new in-memory config.
final_reset_config = self._resolve_paths_and_device(reset_config_base)
if self._save_config_yaml_internal(
final_reset_config
): # Save the fully resolved reset config.
self.config = final_reset_config # Update the active in-memory config.
logger.info(
"Configuration successfully reset to defaults, saved, and resolved."
)
return True
else:
logger.error(
"Failed to save the reset configuration. Current configuration remains unchanged."
)
# If save failed, the old self.config is retained.
return False
# --- Type-specific Getters ---
# These provide convenient, type-checked access to configuration values.
def get_int(self, key_path: str, default: Optional[int] = None) -> int:
"""Retrieves a configuration value, converting it to an integer."""
raw_value = self.get(key_path)
if raw_value is None:
if default is not None:
logger.debug(
f"Config '{key_path}' is None, using provided method default: {default}"
)
return default
logger.error(
f"Mandatory integer config '{key_path}' is None, and no method default. Returning 0."
)
return 0
try:
return int(raw_value)
except (ValueError, TypeError):
logger.warning(
f"Invalid integer value '{raw_value}' for '{key_path}'. Using method default or 0."
)
if isinstance(default, int):
return default
logger.error(
f"Cannot parse '{raw_value}' as int for '{key_path}' and no valid method default. Returning 0."
)
return 0
def get_float(self, key_path: str, default: Optional[float] = None) -> float:
"""Retrieves a configuration value, converting it to a float."""
raw_value = self.get(key_path)
if raw_value is None:
if default is not None:
logger.debug(
f"Config '{key_path}' is None, using provided method default: {default}"
)
return default
logger.error(
f"Mandatory float config '{key_path}' is None, and no method default. Returning 0.0."
)
return 0.0
try:
return float(raw_value)
except (ValueError, TypeError):
logger.warning(
f"Invalid float value '{raw_value}' for '{key_path}'. Using method default or 0.0."
)
if isinstance(default, float):
return default
logger.error(
f"Cannot parse '{raw_value}' as float for '{key_path}' and no valid method default. Returning 0.0."
)
return 0.0
def get_bool(self, key_path: str, default: Optional[bool] = None) -> bool:
"""Retrieves a configuration value, converting it to a boolean."""
raw_value = self.get(key_path)
if raw_value is None:
if default is not None:
logger.debug(
f"Config '{key_path}' is None, using provided method default: {default}"
)
return default
logger.error(
f"Mandatory boolean config '{key_path}' is None, and no method default. Returning False."
)
return False
if isinstance(raw_value, bool):
return raw_value
if isinstance(
raw_value, str
): # Handle common string representations of booleans.
return raw_value.lower() in ("true", "1", "t", "yes", "y")
try: # Handle numeric representations (e.g., 1 for True, 0 for False).
return bool(int(raw_value))
except (ValueError, TypeError):
logger.warning(
f"Invalid boolean value '{raw_value}' for '{key_path}'. Using method default or False."
)
if isinstance(default, bool):
return default
logger.error(
f"Cannot parse '{raw_value}' as bool for '{key_path}' and no valid method default. Returning False."
)
return False
def get_path(
self,
key_path: str,
default_str_path: Optional[str] = None,
ensure_absolute: bool = False,
) -> Path:
"""
Retrieves a configuration value expected to be a path, returning it as a Path object.
If 'ensure_absolute' is True, the path is resolved to an absolute path.
"""
value_from_config = self.get(key_path)
path_obj_to_return: Path
if isinstance(value_from_config, Path):
path_obj_to_return = value_from_config
elif isinstance(value_from_config, str): # Convert string from config to Path.
path_obj_to_return = Path(value_from_config)
elif default_str_path is not None: # Fallback to provided string default.
logger.debug(
f"Config Path '{key_path}' not found or invalid type, using provided default string path: '{default_str_path}'"
)
path_obj_to_return = Path(default_str_path)
else: # Ultimate fallback if no value and no default.
logger.error(
f"Config Path '{key_path}' not found or invalid type, and no default provided. Returning Path('.')"
)
path_obj_to_return = Path(".") # Current directory.
return path_obj_to_return.resolve() if ensure_absolute else path_obj_to_return
# --- Singleton Instance ---
# This provides a single, globally accessible instance of the configuration manager.
config_manager = YamlConfigManager()
# --- Convenience Accessor Functions ---
# These functions provide easy, module-level access to common configuration settings
# using the singleton 'config_manager' instance.
def _get_default_from_structure(key_path: str) -> Any:
"""Internal helper to retrieve a default value directly from the DEFAULT_CONFIG structure."""
keys = key_path.split(".")
return _get_nested_value(DEFAULT_CONFIG, keys)
# Server Settings Accessors
def get_host() -> str:
"""Returns the server host address."""
return config_manager.get_string(
"server.host", _get_default_from_structure("server.host")
)
def get_port() -> int:
"""Returns the server port number."""
return config_manager.get_int(
"server.port", _get_default_from_structure("server.port")
)
def get_ssl_config() -> Dict[str, str]:
"""Returns uvicorn SSL kwargs if both certfile and keyfile are configured and exist.
Returns an empty dict when SSL is not configured (HTTP mode)."""
certfile = config_manager.get("server.ssl_certfile")
keyfile = config_manager.get("server.ssl_keyfile")
if not certfile or not keyfile:
return {}
certpath = Path(str(certfile))
keypath = Path(str(keyfile))
if not certpath.is_file():
logger.error(f"SSL certificate file not found: {certpath} — falling back to HTTP")
return {}
if not keypath.is_file():
logger.error(f"SSL key file not found: {keypath} — falling back to HTTP")
return {}
return {"ssl_certfile": str(certpath), "ssl_keyfile": str(keypath)}
# Audio Output Settings Accessors
def get_audio_output_format() -> str:
"""Returns the default audio output format (e.g., 'wav')."""
return config_manager.get_string(
"audio_output.format", _get_default_from_structure("audio_output.format")
)
def get_log_file_path() -> Path:
"""Returns the absolute path to the server log file."""
default_path_str = str(_get_default_from_structure("server.log_file_path"))
return config_manager.get_path(
"server.log_file_path", default_path_str, ensure_absolute=True
)
# Model Settings Accessors
def get_model_repo_id() -> str:
"""Returns the Hugging Face repository ID for the model."""
return config_manager.get_string(
"model.repo_id", _get_default_from_structure("model.repo_id")
)
# TTS Engine Settings Accessors
def get_tts_device() -> str:
"""Returns the resolved TTS processing device ('cuda' or 'cpu')."""
# Device is resolved during load_config, so direct get is appropriate.
return config_manager.get_string(
"tts_engine.device", _get_default_from_structure("tts_engine.device")
)
def get_predefined_voices_path(ensure_absolute: bool = True) -> Path:
"""Returns the path to the predefined voices directory."""
default_path_str = str(
_get_default_from_structure("tts_engine.predefined_voices_path")
)
return config_manager.get_path(
"tts_engine.predefined_voices_path",
default_path_str,
ensure_absolute=ensure_absolute,
)
def get_reference_audio_path(ensure_absolute: bool = True) -> Path:
"""Returns the path to the reference audio directory."""
default_path_str = str(
_get_default_from_structure("tts_engine.reference_audio_path")
)
return config_manager.get_path(
"tts_engine.reference_audio_path",
default_path_str,
ensure_absolute=ensure_absolute,
)
def get_default_voice_id() -> str:
"""Returns the ID (filename) of the default predefined voice."""
return config_manager.get_string(
"tts_engine.default_voice_id",
_get_default_from_structure("tts_engine.default_voice_id"),
)
# General Path Settings Accessors
def get_model_cache_path(ensure_absolute: bool = True) -> Path:
"""Returns the path to the model cache directory."""
default_path_str = str(_get_default_from_structure("paths.model_cache"))
return config_manager.get_path(
"paths.model_cache", default_path_str, ensure_absolute=ensure_absolute
)
def get_output_path(ensure_absolute: bool = True) -> Path:
"""Returns the path to the default output directory."""
default_path_str = str(_get_default_from_structure("paths.output"))
return config_manager.get_path(
"paths.output", default_path_str, ensure_absolute=ensure_absolute
)
# Default Generation Parameter Accessors
def get_gen_default_temperature() -> float:
"""Returns the default temperature for TTS generation."""
return config_manager.get_float(
"generation_defaults.temperature",
_get_default_from_structure("generation_defaults.temperature"),
)
def get_gen_default_exaggeration() -> float:
"""Returns the default exaggeration factor for TTS generation."""
return config_manager.get_float(
"generation_defaults.exaggeration",
_get_default_from_structure("generation_defaults.exaggeration"),
)
def get_gen_default_cfg_weight() -> float:
"""Returns the default CFG weight for TTS generation."""
return config_manager.get_float(
"generation_defaults.cfg_weight",
_get_default_from_structure("generation_defaults.cfg_weight"),
)
def get_gen_default_seed() -> int:
"""Returns the default seed for TTS generation."""
return config_manager.get_int(
"generation_defaults.seed",
_get_default_from_structure("generation_defaults.seed"),
)
def get_gen_default_speed_factor() -> float:
"""Returns the default speed factor for TTS generation."""
return config_manager.get_float(
"generation_defaults.speed_factor",
_get_default_from_structure("generation_defaults.speed_factor"),
)
def get_gen_default_language() -> str:
"""Returns the default language for TTS generation."""
return config_manager.get_string(
"generation_defaults.language",
_get_default_from_structure("generation_defaults.language"),
)
# Audio Output Settings Accessors
def get_audio_output_format() -> str:
"""Returns the default audio output format (e.g., 'wav')."""
return config_manager.get_string(
"audio_output.format", _get_default_from_structure("audio_output.format")
)
def get_audio_sample_rate() -> int:
"""Returns the default audio sample rate."""
return config_manager.get_int(
"audio_output.sample_rate",
_get_default_from_structure("audio_output.sample_rate"),
)
# UI State Accessors
def get_ui_state() -> Dict[str, Any]:
"""Returns the entire UI state dictionary (for UI persistence)."""
return config_manager.get(
"ui_state", deepcopy(_get_default_from_structure("ui_state"))
)
# General UI Settings Accessors
def get_ui_title() -> str:
"""Returns the title for the web UI."""
return config_manager.get_string(
"ui.title", _get_default_from_structure("ui.title")
)
def get_full_config_for_template() -> Dict[str, Any]:
"""
Returns a deep copy of the current configuration, with Path objects
converted to strings. This is suitable for serialization (e.g., JSON)
or for passing to web templates or API responses.
"""
config_snapshot = config_manager.get_all() # Gets a deep copy.
# Convert Path objects in this snapshot to strings for serialization.
return config_manager._prepare_config_for_saving(config_snapshot)
# --- End File: config.py ---