zach commited on
Commit
a4afe51
·
1 Parent(s): b50c10f

Add '_' prefix to private methods throughout project, cleans up formatting and comments/docstrings

Browse files
src/app.py CHANGED
@@ -47,7 +47,7 @@ class App:
47
  self.config = config
48
  self.db_session_maker = db_session_maker
49
 
50
- def generate_text(
51
  self,
52
  character_description: str,
53
  ) -> Tuple[Union[str, gr.update], gr.update]:
@@ -65,6 +65,7 @@ class App:
65
  Raises:
66
  gr.Error: On validation or API errors.
67
  """
 
68
  try:
69
  validate_character_description_length(character_description)
70
  except ValueError as ve:
@@ -82,7 +83,7 @@ class App:
82
  logger.error(f"Unexpected error while generating text: {e}")
83
  raise gr.Error("Failed to generate text. Please try again later.")
84
 
85
- def synthesize_speech(
86
  self,
87
  character_description: str,
88
  text: str,
@@ -121,6 +122,7 @@ class App:
121
  Raises:
122
  gr.Error: If any API or unexpected errors occur during the TTS synthesis process.
123
  """
 
124
  if not text:
125
  logger.warning("Skipping text-to-speech due to empty text.")
126
  raise gr.Error("Please generate or enter text to synthesize.")
@@ -185,7 +187,7 @@ class App:
185
  logger.error(f"Unexpected error during TTS generation: {e}")
186
  raise gr.Error("An unexpected error ocurred. Please try again later.")
187
 
188
- def vote(
189
  self,
190
  vote_submitted: bool,
191
  option_map: OptionMap,
@@ -214,6 +216,7 @@ class App:
214
  - An update for the unselected vote button (showing provider).
215
  - An update for enabling vote interactions.
216
  """
 
217
  if not option_map or vote_submitted:
218
  return gr.skip(), gr.skip(), gr.skip(), gr.skip()
219
 
@@ -251,7 +254,7 @@ class App:
251
  gr.update(interactive=True),
252
  )
253
 
254
- def reset_ui(self) -> Tuple[gr.update, gr.update, gr.update, gr.update, None, bool]:
255
  """
256
  Resets UI state before generating new text.
257
 
@@ -264,6 +267,7 @@ class App:
264
  - option_map_state (reset option map state)
265
  - vote_submitted_state (reset submitted vote state)
266
  """
 
267
  return (
268
  gr.update(value=None),
269
  gr.update(value=None, autoplay=False),
@@ -273,11 +277,12 @@ class App:
273
  False,
274
  )
275
 
276
- def build_input_section(self) -> Tuple[gr.Dropdown, gr.Textbox, gr.Button]:
277
  """
278
  Builds the input section including the sample character description dropdown, character
279
  description input, and generate text button.
280
  """
 
281
  sample_character_description_dropdown = gr.Dropdown(
282
  choices=list(constants.SAMPLE_CHARACTER_DESCRIPTIONS.keys()),
283
  label="Choose a sample character description",
@@ -299,10 +304,11 @@ class App:
299
  generate_text_button,
300
  )
301
 
302
- def build_output_section(self) -> Tuple[gr.Textbox, gr.Button, gr.Audio, gr.Audio, gr.Button, gr.Button]:
303
  """
304
  Builds the output section including text input, audio players, and vote buttons.
305
  """
 
306
  text_input = gr.Textbox(
307
  label="Input Text",
308
  placeholder="Enter or generate text for synthesis...",
@@ -336,6 +342,7 @@ class App:
336
  Returns:
337
  gr.Blocks: The fully constructed Gradio UI layout.
338
  """
 
339
  custom_theme = CustomTheme()
340
  with gr.Blocks(
341
  title="Expressive TTS Arena",
@@ -364,7 +371,7 @@ class App:
364
  sample_character_description_dropdown,
365
  character_description_input,
366
  generate_text_button,
367
- ) = self.build_input_section()
368
 
369
  # Build synthesize speech section
370
  (
@@ -374,7 +381,7 @@ class App:
374
  option_b_audio_player,
375
  vote_button_a,
376
  vote_button_b,
377
- ) = self.build_output_section()
378
 
379
  # --- UI state components ---
380
 
@@ -411,7 +418,7 @@ class App:
411
  inputs=[],
412
  outputs=[generate_text_button],
413
  ).then(
414
- fn=self.generate_text,
415
  inputs=[character_description_input],
416
  outputs=[text_input, generated_text_state],
417
  ).then(
@@ -434,7 +441,7 @@ class App:
434
  inputs=[],
435
  outputs=[synthesize_speech_button, vote_button_a, vote_button_b],
436
  ).then(
437
- fn=self.reset_ui,
438
  inputs=[],
439
  outputs=[
440
  option_a_audio_player,
@@ -445,7 +452,7 @@ class App:
445
  vote_submitted_state,
446
  ],
447
  ).then(
448
- fn=self.synthesize_speech,
449
  inputs=[character_description_input, text_input, generated_text_state],
450
  outputs=[
451
  option_a_audio_player,
@@ -474,7 +481,7 @@ class App:
474
  inputs=[],
475
  outputs=[vote_button_a, vote_button_b],
476
  ).then(
477
- fn=self.vote,
478
  inputs=[
479
  vote_submitted_state,
480
  option_map_state,
 
47
  self.config = config
48
  self.db_session_maker = db_session_maker
49
 
50
+ def _generate_text(
51
  self,
52
  character_description: str,
53
  ) -> Tuple[Union[str, gr.update], gr.update]:
 
65
  Raises:
66
  gr.Error: On validation or API errors.
67
  """
68
+
69
  try:
70
  validate_character_description_length(character_description)
71
  except ValueError as ve:
 
83
  logger.error(f"Unexpected error while generating text: {e}")
84
  raise gr.Error("Failed to generate text. Please try again later.")
85
 
86
+ def _synthesize_speech(
87
  self,
88
  character_description: str,
89
  text: str,
 
122
  Raises:
123
  gr.Error: If any API or unexpected errors occur during the TTS synthesis process.
124
  """
125
+
126
  if not text:
127
  logger.warning("Skipping text-to-speech due to empty text.")
128
  raise gr.Error("Please generate or enter text to synthesize.")
 
187
  logger.error(f"Unexpected error during TTS generation: {e}")
188
  raise gr.Error("An unexpected error ocurred. Please try again later.")
189
 
190
+ def _vote(
191
  self,
192
  vote_submitted: bool,
193
  option_map: OptionMap,
 
216
  - An update for the unselected vote button (showing provider).
217
  - An update for enabling vote interactions.
218
  """
219
+
220
  if not option_map or vote_submitted:
221
  return gr.skip(), gr.skip(), gr.skip(), gr.skip()
222
 
 
254
  gr.update(interactive=True),
255
  )
256
 
257
+ def _reset_ui(self) -> Tuple[gr.update, gr.update, gr.update, gr.update, None, bool]:
258
  """
259
  Resets UI state before generating new text.
260
 
 
267
  - option_map_state (reset option map state)
268
  - vote_submitted_state (reset submitted vote state)
269
  """
270
+
271
  return (
272
  gr.update(value=None),
273
  gr.update(value=None, autoplay=False),
 
277
  False,
278
  )
279
 
280
+ def _build_input_section(self) -> Tuple[gr.Dropdown, gr.Textbox, gr.Button]:
281
  """
282
  Builds the input section including the sample character description dropdown, character
283
  description input, and generate text button.
284
  """
285
+
286
  sample_character_description_dropdown = gr.Dropdown(
287
  choices=list(constants.SAMPLE_CHARACTER_DESCRIPTIONS.keys()),
288
  label="Choose a sample character description",
 
304
  generate_text_button,
305
  )
306
 
307
+ def _build_output_section(self) -> Tuple[gr.Textbox, gr.Button, gr.Audio, gr.Audio, gr.Button, gr.Button]:
308
  """
309
  Builds the output section including text input, audio players, and vote buttons.
310
  """
311
+
312
  text_input = gr.Textbox(
313
  label="Input Text",
314
  placeholder="Enter or generate text for synthesis...",
 
342
  Returns:
343
  gr.Blocks: The fully constructed Gradio UI layout.
344
  """
345
+
346
  custom_theme = CustomTheme()
347
  with gr.Blocks(
348
  title="Expressive TTS Arena",
 
371
  sample_character_description_dropdown,
372
  character_description_input,
373
  generate_text_button,
374
+ ) = self._build_input_section()
375
 
376
  # Build synthesize speech section
377
  (
 
381
  option_b_audio_player,
382
  vote_button_a,
383
  vote_button_b,
384
+ ) = self._build_output_section()
385
 
386
  # --- UI state components ---
387
 
 
418
  inputs=[],
419
  outputs=[generate_text_button],
420
  ).then(
421
+ fn=self._generate_text,
422
  inputs=[character_description_input],
423
  outputs=[text_input, generated_text_state],
424
  ).then(
 
441
  inputs=[],
442
  outputs=[synthesize_speech_button, vote_button_a, vote_button_b],
443
  ).then(
444
+ fn=self._reset_ui,
445
  inputs=[],
446
  outputs=[
447
  option_a_audio_player,
 
452
  vote_submitted_state,
453
  ],
454
  ).then(
455
+ fn=self._synthesize_speech,
456
  inputs=[character_description_input, text_input, generated_text_state],
457
  outputs=[
458
  option_a_audio_player,
 
481
  inputs=[],
482
  outputs=[vote_button_a, vote_button_b],
483
  ).then(
484
+ fn=self._vote,
485
  inputs=[
486
  vote_submitted_state,
487
  option_map_state,
src/config.py CHANGED
@@ -23,6 +23,7 @@ from dotenv import load_dotenv
23
  if TYPE_CHECKING:
24
  from src.integrations import AnthropicConfig, ElevenLabsConfig, HumeConfig
25
 
 
26
  logger: logging.Logger = logging.getLogger("tts_arena")
27
 
28
 
 
23
  if TYPE_CHECKING:
24
  from src.integrations import AnthropicConfig, ElevenLabsConfig, HumeConfig
25
 
26
+
27
  logger: logging.Logger = logging.getLogger("tts_arena")
28
 
29
 
src/constants.py CHANGED
@@ -13,6 +13,7 @@ from src.custom_types import ComparisonType, OptionKey, OptionLabel, TTSProvider
13
  CLIENT_ERROR_CODE = 400
14
  SERVER_ERROR_CODE = 500
15
 
 
16
  # UI constants
17
  HUME_AI: TTSProviderName = "Hume AI"
18
  ELEVENLABS: TTSProviderName = "ElevenLabs"
@@ -28,13 +29,15 @@ OPTION_A_KEY: OptionKey = "option_a"
28
  OPTION_B_KEY: OptionKey = "option_b"
29
  OPTION_A_LABEL: OptionLabel = "Option A"
30
  OPTION_B_LABEL: OptionLabel = "Option B"
 
31
  TROPHY_EMOJI: str = "🏆"
 
32
  SELECT_OPTION_A: str = "Select Option A"
33
  SELECT_OPTION_B: str = "Select Option B"
34
 
35
 
36
  # A collection of pre-defined character descriptions categorized by theme, used to provide users with
37
- # inspiration for generating creative text for expressive TTS, and generating novel voices.
38
  SAMPLE_CHARACTER_DESCRIPTIONS: dict = {
39
  "🚀 Stranded Astronaut": (
40
  "A lone astronaut whose voice mirrors the silent vastness of space—a low, steady tone imbued "
 
13
  CLIENT_ERROR_CODE = 400
14
  SERVER_ERROR_CODE = 500
15
 
16
+
17
  # UI constants
18
  HUME_AI: TTSProviderName = "Hume AI"
19
  ELEVENLABS: TTSProviderName = "ElevenLabs"
 
29
  OPTION_B_KEY: OptionKey = "option_b"
30
  OPTION_A_LABEL: OptionLabel = "Option A"
31
  OPTION_B_LABEL: OptionLabel = "Option B"
32
+
33
  TROPHY_EMOJI: str = "🏆"
34
+
35
  SELECT_OPTION_A: str = "Select Option A"
36
  SELECT_OPTION_B: str = "Select Option B"
37
 
38
 
39
  # A collection of pre-defined character descriptions categorized by theme, used to provide users with
40
+ # inspiration for generating creative, expressive text inputs for TTS, and generating novel voices.
41
  SAMPLE_CHARACTER_DESCRIPTIONS: dict = {
42
  "🚀 Stranded Astronaut": (
43
  "A lone astronaut whose voice mirrors the silent vastness of space—a low, steady tone imbued "
src/database/database.py CHANGED
@@ -52,6 +52,7 @@ class DummySession:
52
  Base = declarative_base()
53
  engine: Optional[Engine] = None
54
 
 
55
  DBSessionMaker = sessionmaker | Callable[[], DummySession]
56
 
57
 
 
52
  Base = declarative_base()
53
  engine: Optional[Engine] = None
54
 
55
+
56
  DBSessionMaker = sessionmaker | Callable[[], DummySession]
57
 
58
 
src/integrations/anthropic_api.py CHANGED
@@ -69,6 +69,7 @@ where you are in the narrative.
69
  Remember: A shorter, complete response is ALWAYS better than a longer, truncated one."""
70
  )
71
 
 
72
  @dataclass(frozen=True)
73
  class AnthropicConfig:
74
  """Immutable configuration for interacting with the Anthropic API."""
@@ -166,6 +167,7 @@ def generate_text_with_claude(character_description: str, config: Config) -> str
166
  UnretryableAnthropicError: For errors that should not be retried.
167
  AnthropicError: For other errors communicating with the Anthropic API.
168
  """
 
169
  try:
170
  anthropic_config = config.anthropic_config
171
  prompt = anthropic_config.build_expressive_prompt(character_description)
 
69
  Remember: A shorter, complete response is ALWAYS better than a longer, truncated one."""
70
  )
71
 
72
+
73
  @dataclass(frozen=True)
74
  class AnthropicConfig:
75
  """Immutable configuration for interacting with the Anthropic API."""
 
167
  UnretryableAnthropicError: For errors that should not be retried.
168
  AnthropicError: For other errors communicating with the Anthropic API.
169
  """
170
+
171
  try:
172
  anthropic_config = config.anthropic_config
173
  prompt = anthropic_config.build_expressive_prompt(character_description)
src/integrations/elevenlabs_api.py CHANGED
@@ -105,6 +105,7 @@ def text_to_speech_with_elevenlabs(
105
  Raises:
106
  ElevenLabsError: If there is an error communicating with the ElevenLabs API or processing the response.
107
  """
 
108
  logger.debug(f"Synthesizing speech with ElevenLabs. Text length: {len(text)} characters.")
109
 
110
  elevenlabs_config = config.elevenlabs_config
 
105
  Raises:
106
  ElevenLabsError: If there is an error communicating with the ElevenLabs API or processing the response.
107
  """
108
+
109
  logger.debug(f"Synthesizing speech with ElevenLabs. Text length: {len(text)} characters.")
110
 
111
  elevenlabs_config = config.elevenlabs_config
src/integrations/hume_api.py CHANGED
@@ -128,6 +128,7 @@ def text_to_speech_with_hume(
128
  Exception: Any other exceptions raised during the request or processing will be wrapped and
129
  re-raised as HumeError.
130
  """
 
131
  logger.debug(
132
  f"Processing TTS with Hume. Prompt length: {len(character_description)} characters. "
133
  f"Text length: {len(text)} characters."
@@ -161,13 +162,13 @@ def text_to_speech_with_hume(
161
 
162
  # Extract the base64 encoded audio and generation ID from the generation.
163
  generation_a = generations[0]
164
- generation_a_id, audio_a_path = parse_hume_tts_generation(generation_a, config)
165
 
166
  if num_generations == 1:
167
  return (generation_a_id, audio_a_path)
168
 
169
  generation_b = generations[1]
170
- generation_b_id, audio_b_path = parse_hume_tts_generation(generation_b, config)
171
  return (generation_a_id, audio_a_path, generation_b_id, audio_b_path)
172
 
173
  except Exception as e:
@@ -187,7 +188,7 @@ def text_to_speech_with_hume(
187
  ) from e
188
 
189
 
190
- def parse_hume_tts_generation(generation: Dict[str, Any], config: Config) -> Tuple[str, str]:
191
  """
192
  Parse a Hume TTS generation response and save the decoded audio as an MP3 file.
193
 
 
128
  Exception: Any other exceptions raised during the request or processing will be wrapped and
129
  re-raised as HumeError.
130
  """
131
+
132
  logger.debug(
133
  f"Processing TTS with Hume. Prompt length: {len(character_description)} characters. "
134
  f"Text length: {len(text)} characters."
 
162
 
163
  # Extract the base64 encoded audio and generation ID from the generation.
164
  generation_a = generations[0]
165
+ generation_a_id, audio_a_path = _parse_hume_tts_generation(generation_a, config)
166
 
167
  if num_generations == 1:
168
  return (generation_a_id, audio_a_path)
169
 
170
  generation_b = generations[1]
171
+ generation_b_id, audio_b_path = _parse_hume_tts_generation(generation_b, config)
172
  return (generation_a_id, audio_a_path, generation_b_id, audio_b_path)
173
 
174
  except Exception as e:
 
188
  ) from e
189
 
190
 
191
+ def _parse_hume_tts_generation(generation: Dict[str, Any], config: Config) -> Tuple[str, str]:
192
  """
193
  Parse a Hume TTS generation response and save the decoded audio as an MP3 file.
194
 
src/main.py CHANGED
@@ -4,9 +4,10 @@ main.py
4
  This module is the entry point for the app. It loads configuration and starts the Gradio app.
5
  """
6
 
 
7
  from src.app import App
8
  from src.config import Config, logger
9
- from src.database.database import init_db
10
 
11
  if __name__ == "__main__":
12
  config = Config.get()
 
4
  This module is the entry point for the app. It loads configuration and starts the Gradio app.
5
  """
6
 
7
+ # Local Application Imports
8
  from src.app import App
9
  from src.config import Config, logger
10
+ from src.database import init_db
11
 
12
  if __name__ == "__main__":
13
  config = Config.get()
src/utils.py CHANGED
@@ -100,7 +100,7 @@ def validate_character_description_length(character_description: str) -> None:
100
  logger.debug(f"Character description length validation passed for character_description: {truncated_description}")
101
 
102
 
103
- def delete_files_older_than(directory: Path, minutes: int = 30) -> None:
104
  """
105
  Delete all files in the specified directory that are older than a given number of minutes.
106
 
@@ -154,15 +154,12 @@ def save_base64_audio_to_file(base64_audio: str, filename: str, config: Config)
154
  Raises:
155
  FileNotFoundError: If the audio file was not created.
156
  """
157
- # Decode the base64-encoded audio into binary data.
158
- audio_bytes = base64.b64decode(base64_audio)
159
 
160
- # Construct the full absolute file path within the AUDIO_DIR directory using Path.
161
  file_path = Path(config.audio_dir) / filename
162
-
163
- # Delete all audio files older than 30 minutes before writing the new audio file.
164
  num_minutes = 30
165
- delete_files_older_than(config.audio_dir, num_minutes)
 
166
 
167
  # Write the binary audio data to the file.
168
  with file_path.open("wb") as audio_file:
@@ -201,6 +198,7 @@ def choose_providers(
201
  where the first is always "Hume AI" and the second is determined by the text_modified
202
  flag and random selection.
203
  """
 
204
  hume_comparison_only = text_modified or not character_description
205
 
206
  provider_a = constants.HUME_AI
@@ -225,16 +223,11 @@ def create_shuffled_tts_options(option_a: Option, option_b: Option) -> OptionMap
225
  OptionMap: A mapping of shuffled TTS options, where each option includes
226
  its provider, audio file path, and generation ID.
227
  """
228
- # Create a list of Option instances for the available providers.
229
- options = [option_a, option_b]
230
 
231
- # Randomly shuffle the list of options.
232
  random.shuffle(options)
233
-
234
- # Unpack the two options.
235
  shuffled_option_a, shuffled_option_b = options
236
 
237
- # Build a mapping from option constants to the corresponding providers.
238
  return {
239
  "option_a": {
240
  "provider": shuffled_option_a.provider,
@@ -264,6 +257,7 @@ def determine_selected_option(
264
  - selected_option is the same as the selected_option.
265
  - other_option is the alternative option.
266
  """
 
267
  if selected_option_button == constants.SELECT_OPTION_A:
268
  selected_option, other_option = constants.OPTION_A_KEY, constants.OPTION_B_KEY
269
  elif selected_option_button == constants.SELECT_OPTION_B:
@@ -291,6 +285,7 @@ def _determine_comparison_type(provider_a: TTSProviderName, provider_b: TTSProvi
291
  Raises:
292
  ValueError: If the combination of providers is not recognized.
293
  """
 
294
  if provider_a == constants.HUME_AI and provider_b == constants.HUME_AI:
295
  return constants.HUME_TO_HUME
296
 
@@ -302,6 +297,7 @@ def _determine_comparison_type(provider_a: TTSProviderName, provider_b: TTSProvi
302
 
303
  def _log_voting_results(voting_results: VotingResults) -> None:
304
  """Log the full voting results."""
 
305
  logger.info("Voting results:\n%s", json.dumps(voting_results, indent=4))
306
 
307
 
@@ -321,6 +317,7 @@ def _handle_vote_failure(
321
  In development with a dummy session:
322
  - Only logs the voting results.
323
  """
 
324
  if config.app_env == "prod" or (config.app_env == "dev" and not is_dummy_db_session):
325
  logger.error("Failed to create vote record: %s", e, exc_info=(config.app_env == "prod"))
326
  _log_voting_results(voting_results)
@@ -332,6 +329,23 @@ def _handle_vote_failure(
332
 
333
 
334
  def _persist_vote(db_session_maker: DBSessionMaker, voting_results: VotingResults, config: Config) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  db = db_session_maker()
336
  is_dummy_db_session = getattr(db, "is_dummy", False)
337
  if is_dummy_db_session:
@@ -369,6 +383,7 @@ def submit_voting_results(
369
  character_description (str): Description of the voice/character.
370
  text (str): The text associated with the TTS generation.
371
  """
 
372
  provider_a: TTSProviderName = option_map[constants.OPTION_A_KEY]["provider"]
373
  provider_b: TTSProviderName = option_map[constants.OPTION_B_KEY]["provider"]
374
  comparison_type: ComparisonType = _determine_comparison_type(provider_a, provider_b)
 
100
  logger.debug(f"Character description length validation passed for character_description: {truncated_description}")
101
 
102
 
103
+ def _delete_files_older_than(directory: Path, minutes: int = 30) -> None:
104
  """
105
  Delete all files in the specified directory that are older than a given number of minutes.
106
 
 
154
  Raises:
155
  FileNotFoundError: If the audio file was not created.
156
  """
 
 
157
 
158
+ audio_bytes = base64.b64decode(base64_audio)
159
  file_path = Path(config.audio_dir) / filename
 
 
160
  num_minutes = 30
161
+
162
+ _delete_files_older_than(config.audio_dir, num_minutes)
163
 
164
  # Write the binary audio data to the file.
165
  with file_path.open("wb") as audio_file:
 
198
  where the first is always "Hume AI" and the second is determined by the text_modified
199
  flag and random selection.
200
  """
201
+
202
  hume_comparison_only = text_modified or not character_description
203
 
204
  provider_a = constants.HUME_AI
 
223
  OptionMap: A mapping of shuffled TTS options, where each option includes
224
  its provider, audio file path, and generation ID.
225
  """
 
 
226
 
227
+ options = [option_a, option_b]
228
  random.shuffle(options)
 
 
229
  shuffled_option_a, shuffled_option_b = options
230
 
 
231
  return {
232
  "option_a": {
233
  "provider": shuffled_option_a.provider,
 
257
  - selected_option is the same as the selected_option.
258
  - other_option is the alternative option.
259
  """
260
+
261
  if selected_option_button == constants.SELECT_OPTION_A:
262
  selected_option, other_option = constants.OPTION_A_KEY, constants.OPTION_B_KEY
263
  elif selected_option_button == constants.SELECT_OPTION_B:
 
285
  Raises:
286
  ValueError: If the combination of providers is not recognized.
287
  """
288
+
289
  if provider_a == constants.HUME_AI and provider_b == constants.HUME_AI:
290
  return constants.HUME_TO_HUME
291
 
 
297
 
298
  def _log_voting_results(voting_results: VotingResults) -> None:
299
  """Log the full voting results."""
300
+
301
  logger.info("Voting results:\n%s", json.dumps(voting_results, indent=4))
302
 
303
 
 
317
  In development with a dummy session:
318
  - Only logs the voting results.
319
  """
320
+
321
  if config.app_env == "prod" or (config.app_env == "dev" and not is_dummy_db_session):
322
  logger.error("Failed to create vote record: %s", e, exc_info=(config.app_env == "prod"))
323
  _log_voting_results(voting_results)
 
329
 
330
 
331
  def _persist_vote(db_session_maker: DBSessionMaker, voting_results: VotingResults, config: Config) -> None:
332
+ """
333
+ Persist a vote record in the database and handle potential failures.
334
+
335
+ This function obtains a database session using the provided session maker and attempts
336
+ to create a vote record using the specified voting results. If the session is identified
337
+ as a dummy session, it logs a success message and outputs the voting results. If an error
338
+ occurs during vote creation, the function delegates error handling to _handle_vote_failure.
339
+ On successful vote creation, it logs the success and, when running in a development environment,
340
+ logs the full voting results for debugging purposes. In all cases, the database session is
341
+ properly closed after the operation.
342
+
343
+ Args:
344
+ db_session_maker (DBSessionMaker): A callable that returns a new database session.
345
+ voting_results (VotingResults): A dictionary containing the details of the vote to persist.
346
+ config (Config): The application configuration, used to determine environment-specific behavior.
347
+ """
348
+
349
  db = db_session_maker()
350
  is_dummy_db_session = getattr(db, "is_dummy", False)
351
  if is_dummy_db_session:
 
383
  character_description (str): Description of the voice/character.
384
  text (str): The text associated with the TTS generation.
385
  """
386
+
387
  provider_a: TTSProviderName = option_map[constants.OPTION_A_KEY]["provider"]
388
  provider_b: TTSProviderName = option_map[constants.OPTION_B_KEY]["provider"]
389
  comparison_type: ComparisonType = _determine_comparison_type(provider_a, provider_b)