zach commited on
Commit
1f53f73
·
1 Parent(s): dfee4f4

Remove orphaned gradio components, clean up docstrings, fix types in app.py

Browse files
Files changed (1) hide show
  1. src/app.py +132 -127
src/app.py CHANGED
@@ -4,14 +4,14 @@ app.py
4
  Gradio UI for interacting with the Anthropic API, Hume TTS API, and ElevenLabs TTS API.
5
 
6
  Users enter a prompt, which is processed using Claude by Anthropic to generate text.
7
- The text is then converted into speech using both Hume and ElevenLabs TTS APIs.
8
  Users can compare the outputs in an interactive UI.
9
  """
10
 
11
  # Standard Library Imports
12
  from concurrent.futures import ThreadPoolExecutor
13
  import random
14
- from typing import Union
15
  # Third-Party Library Imports
16
  import gradio as gr
17
  # Local Application Imports
@@ -39,66 +39,62 @@ from src.theme import CustomTheme
39
  from src.utils import truncate_text, validate_prompt_length
40
 
41
 
42
- def generate_text(prompt: str) -> tuple[Union[str, gr.update], gr.update]:
43
  """
44
- Validates the prompt before generating text.
45
- - If valid, returns the generated text and keeps the button disabled.
46
- - If invalid, returns an error message and re-enables the button.
47
 
48
  Args:
49
  prompt (str): The user-provided text prompt.
50
 
51
  Returns:
52
- tuple[Union[str, gr.update], gr.update]:
53
- - The generated text or an error message.
54
- - The updated state of the "Generate" button.
 
 
 
55
  """
56
- # Local prompt validation
57
  try:
58
  validate_prompt_length(prompt, PROMPT_MAX_LENGTH, PROMPT_MIN_LENGTH)
59
  except ValueError as ve:
60
  logger.warning(f'Validation error: {ve}')
61
  raise gr.Error(str(ve))
62
 
63
- # Call the Anthropic API to generate text
64
  try:
65
  generated_text = generate_text_with_claude(prompt)
66
  logger.info(f'Generated text ({len(generated_text)} characters).')
67
  return gr.update(value=generated_text)
68
-
69
  except AnthropicError as ae:
70
  logger.error(f'AnthropicError while generating text: {str(ae)}')
71
  raise gr.Error('There was an issue communicating with the Anthropic API. Please try again later.')
72
-
73
  except Exception as e:
74
  logger.error(f'Unexpected error while generating text: {e}')
75
  raise gr.Error('Failed to generate text. Please try again.')
76
 
77
 
78
- def text_to_speech(prompt: str, generated_text: str) -> tuple[gr.update, gr.update, dict, str | None]:
79
  """
80
- Converts generated text to speech using Hume AI and ElevenLabs APIs.
81
-
82
- If the generated text is invalid (empty or an error message), this function
83
- does nothing and returns `None` values to prevent TTS from running.
84
 
85
  Args:
86
- prompt (str): The original user-provided prompt.
87
- generated_text (str): The generated text that will be converted to speech.
88
 
89
  Returns:
90
- tuple[gr.update, gr.update, dict, Union[str, None]]:
91
- - `gr.update(value=option_1_audio, autoplay=True)`: Updates the first audio player.
92
- - `gr.update(value=option_2_audio)`: Updates the second audio player.
93
- - `options_map`: A dictionary mapping OPTION_ONE and OPTION_TWO to their providers.
94
- - `option_2_audio`: The second audio file path or `None` if an error occurs.
 
 
 
95
  """
96
  if not generated_text:
97
  logger.warning('Skipping text-to-speech due to empty text.')
98
  return gr.skip(), gr.skip(), gr.skip(), gr.skip()
99
 
100
  try:
101
- # Call the Hume and ElevenLabs APIs to synthesize speech from text in parallel
102
  with ThreadPoolExecutor(max_workers=2) as executor:
103
  future_hume = executor.submit(text_to_speech_with_hume, prompt, generated_text)
104
  future_elevenlabs = executor.submit(text_to_speech_with_elevenlabs, generated_text)
@@ -106,71 +102,126 @@ def text_to_speech(prompt: str, generated_text: str) -> tuple[gr.update, gr.upda
106
  hume_audio = future_hume.result()
107
  elevenlabs_audio = future_elevenlabs.result()
108
 
109
- logger.info(
110
- f'TTS generated: Hume={len(hume_audio)} bytes, ElevenLabs={len(elevenlabs_audio)} bytes'
111
- )
112
-
113
- # Randomize audio order
114
  options = [(hume_audio, 'Hume AI'), (elevenlabs_audio, 'ElevenLabs')]
115
  random.shuffle(options)
116
-
117
  option_1_audio, option_2_audio = options[0][0], options[1][0]
118
  options_map = { OPTION_ONE: options[0][1], OPTION_TWO: options[1][1] }
119
 
120
  return (
121
- gr.update(value=option_1_audio, autoplay=True), # Set option 1 audio
122
- gr.update(value=option_2_audio), # Option 2 audio
123
- options_map, # Set option mapping state
124
- option_2_audio, # Set option 2 audio state
125
  )
126
-
127
  except ElevenLabsError as ee:
128
  logger.error(f'ElevenLabsError while synthesizing speech from text: {str(ee)}')
129
  raise gr.Error('There was an issue communicating with the Elevenlabs API. Please try again later.')
130
-
131
  except HumeError as he:
132
  logger.error(f'HumeError while synthesizing speech from text: {str(he)}')
133
  raise gr.Error('There was an issue communicating with the Hume API. Please try again later.')
134
-
135
  except Exception as e:
136
  logger.error(f'Unexpected error during TTS generation: {e}')
137
  raise gr.Error('An unexpected error ocurred. Please try again later.')
138
 
139
 
140
- def vote(vote_submitted: bool, option_mapping: dict, selected_button: str) -> tuple[bool, gr.update, gr.update, gr.update]:
141
  """
142
- Handles user voting and updates the UI to reflect the selected choice.
143
 
144
  Args:
145
- vote_submitted (bool): Indicates if a vote has already been submitted.
146
- option_mapping (dict): Maps "Option 1" and "Option 2" to their respective TTS providers.
147
- selected_button (str): The button that was clicked by the user.
148
 
149
  Returns:
150
- tuple[bool, gr.update, gr.update, gr.update]:
151
- - `True`: Indicates the vote has been submitted.
152
- - `gr.update`: Updates the selected vote button.
153
- - `gr.update`: Updates the unselected vote button.
154
- - `gr.update(interactive=True)`: Enables the "Generate" button after voting.
155
  """
156
  if not option_mapping or vote_submitted:
157
- return gr.skip(), gr.skip(), gr.skip() # No updates if mapping is missing or vote already submitted
158
 
159
- # Determine selected option
160
  is_option_1 = selected_button == VOTE_FOR_OPTION_ONE
161
  selected_option, other_option = (OPTION_ONE, OPTION_TWO) if is_option_1 else (OPTION_TWO, OPTION_ONE)
162
-
163
- # Get provider names
164
  selected_provider = option_mapping.get(selected_option, UNKNOWN_PROVIDER)
165
  other_provider = option_mapping.get(other_option, UNKNOWN_PROVIDER)
166
 
167
- # Return updated button states, reporting the winner
168
  return (
169
  True,
170
- gr.update(value=f'{selected_provider} {TROPHY_EMOJI}', variant='primary') if is_option_1 else gr.update(value=other_provider, variant='secondary'),
171
- gr.update(value=other_provider, variant='secondary') if is_option_1 else gr.update(value=f'{selected_provider} {TROPHY_EMOJI}', variant='primary'),
 
 
172
  )
173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
  def build_gradio_interface() -> gr.Blocks:
176
  """
@@ -183,112 +234,66 @@ def build_gradio_interface() -> gr.Blocks:
183
  with gr.Blocks(
184
  title='Expressive TTS Arena',
185
  theme=custom_theme,
186
- fill_width=True,
187
- css_paths='src/assets/styles.css',
188
  ) as demo:
189
  # Title
190
  gr.Markdown('# Expressive TTS Arena')
191
 
192
- with gr.Column(variant='compact'):
193
- # Instructions
194
- gr.Markdown(
195
- 'Generate text with **Claude by Anthropic**, listen to text-to-speech outputs '
196
- 'from **Hume AI** and **ElevenLabs**, and vote for your favorite!'
197
- )
198
-
199
- # Sample prompt select
200
- sample_prompt_dropdown = gr.Dropdown(
201
- choices=list(SAMPLE_PROMPTS.keys()),
202
- label='Choose a sample prompt (or enter your own)',
203
- value=None,
204
- interactive=True,
205
- )
206
-
207
- # Prompt input
208
- prompt_input = gr.Textbox(
209
- label='Prompt',
210
- placeholder='Enter your prompt...',
211
- lines=2,
212
- max_lines=2,
213
- show_copy_button=True,
214
- )
215
-
216
- # Generate Button
217
- generate_button = gr.Button('Generate', variant='primary')
218
-
219
- with gr.Column(variant='compact'):
220
- # Generated text
221
- generated_text = gr.Textbox(
222
- label='Generated text',
223
- interactive=False,
224
- autoscroll=False,
225
- lines=5,
226
- max_lines=5,
227
- max_length=PROMPT_MAX_LENGTH,
228
- show_copy_button=True,
229
- )
230
-
231
- # Audio players
232
- with gr.Row(equal_height=True):
233
- option1_audio_player = gr.Audio(label=OPTION_ONE, type='filepath', interactive=False)
234
- option2_audio_player = gr.Audio(label=OPTION_TWO, type='filepath', interactive=False)
235
-
236
- # Vote buttons
237
- with gr.Row():
238
- vote_button_1 = gr.Button(VOTE_FOR_OPTION_ONE, interactive=False)
239
- vote_button_2 = gr.Button(VOTE_FOR_OPTION_TWO, interactive=False)
240
 
241
  # UI state components
242
  option_mapping_state = gr.State() # Track option map (option 1 and option 2 are randomized)
243
  option2_audio_state = gr.State() # Track generated audio for option 2 for playing automatically after option 1 audio finishes
244
- generated_text_state = gr.State() # Track the text which was generated from the user's prompt
245
  vote_submitted_state = gr.State(False) # Track whether the user has voted on an option
246
 
247
- # Event handlers
 
 
248
  sample_prompt_dropdown.change(
249
  fn=lambda choice: SAMPLE_PROMPTS.get(choice, ''),
250
  inputs=[sample_prompt_dropdown],
251
  outputs=[prompt_input],
252
  )
253
 
254
- # Generate button click handler
 
 
 
 
 
255
  generate_button.click(
256
- # Reset UI
257
- fn=lambda _: (
258
- gr.update(interactive=False), # Disable "Generate" button
259
- gr.update(interactive=False, value=VOTE_FOR_OPTION_ONE, variant='secondary'), # Reset vote button 1 text
260
- gr.update(interactive=False, value=VOTE_FOR_OPTION_TWO, variant='secondary'), # Reset vote button 2 text
261
- None, # Clear option mapping state
262
- None, # Clear option 2 audio state
263
- False, # Reset vote submitted state
264
- ),
265
  inputs=[],
266
- outputs=[generate_button, vote_button_1, vote_button_2, option_mapping_state, option2_audio_state, vote_submitted_state],
 
 
 
 
267
  ).then(
268
- # Validate prompt and generate text
269
  fn=generate_text,
270
  inputs=[prompt_input],
271
  outputs=[generated_text],
272
  ).then(
273
- # Validate generated text and synthesize speech
274
  fn=text_to_speech,
275
  inputs=[prompt_input, generated_text],
276
  outputs=[option1_audio_player, option2_audio_player, option_mapping_state, option2_audio_state],
277
  ).then(
278
- # Re-enable "Generate" button
279
- fn=lambda: gr.update(interactive=True),
280
  inputs=[],
281
  outputs=[generate_button]
282
  )
283
 
284
- # Option 1 vote button click handler
285
  vote_button_1.click(
286
  fn=vote,
287
  inputs=[vote_submitted_state, option_mapping_state, vote_button_1],
288
  outputs=[vote_submitted_state, vote_button_1, vote_button_2],
289
  )
290
-
291
- # Option 2 vote button click handler
292
  vote_button_2.click(
293
  fn=vote,
294
  inputs=[vote_submitted_state, option_mapping_state, vote_button_2],
@@ -306,7 +311,7 @@ def build_gradio_interface() -> gr.Blocks:
306
  outputs=[option2_audio_player],
307
  )
308
 
309
- # Enable voting after 2nd audio option playback finishes
310
  option2_audio_player.stop(
311
  fn=lambda _: (gr.update(interactive=True), gr.update(interactive=True), gr.update(autoplay=False)),
312
  inputs=[],
 
4
  Gradio UI for interacting with the Anthropic API, Hume TTS API, and ElevenLabs TTS API.
5
 
6
  Users enter a prompt, which is processed using Claude by Anthropic to generate text.
7
+ The text is then synthesized into speech using both Hume and ElevenLabs TTS APIs.
8
  Users can compare the outputs in an interactive UI.
9
  """
10
 
11
  # Standard Library Imports
12
  from concurrent.futures import ThreadPoolExecutor
13
  import random
14
+ from typing import Union, Tuple
15
  # Third-Party Library Imports
16
  import gradio as gr
17
  # Local Application Imports
 
39
  from src.utils import truncate_text, validate_prompt_length
40
 
41
 
42
+ def generate_text(prompt: str) -> Tuple[Union[str, gr.update], gr.update]:
43
  """
44
+ Validates the prompt and generates text using Anthropic API.
 
 
45
 
46
  Args:
47
  prompt (str): The user-provided text prompt.
48
 
49
  Returns:
50
+ Tuple containing:
51
+ - The generated text (as a gr.update) if successful,
52
+ - An update for the "Generate" button.
53
+
54
+ Raises:
55
+ gr.Error: On validation or API errors.
56
  """
 
57
  try:
58
  validate_prompt_length(prompt, PROMPT_MAX_LENGTH, PROMPT_MIN_LENGTH)
59
  except ValueError as ve:
60
  logger.warning(f'Validation error: {ve}')
61
  raise gr.Error(str(ve))
62
 
 
63
  try:
64
  generated_text = generate_text_with_claude(prompt)
65
  logger.info(f'Generated text ({len(generated_text)} characters).')
66
  return gr.update(value=generated_text)
 
67
  except AnthropicError as ae:
68
  logger.error(f'AnthropicError while generating text: {str(ae)}')
69
  raise gr.Error('There was an issue communicating with the Anthropic API. Please try again later.')
 
70
  except Exception as e:
71
  logger.error(f'Unexpected error while generating text: {e}')
72
  raise gr.Error('Failed to generate text. Please try again.')
73
 
74
 
75
+ def text_to_speech(prompt: str, generated_text: str) -> Tuple[gr.update, gr.update, dict, Union[str, None]]:
76
  """
77
+ Synthesizes generated text to speech using Hume and ElevenLabs APIs in parallel.
 
 
 
78
 
79
  Args:
80
+ prompt (str): The original prompt.
81
+ generated_text (str): The generated text.
82
 
83
  Returns:
84
+ A tuple of:
85
+ - Update for first audio player (with autoplay)
86
+ - Update for second audio player
87
+ - A dictionary mapping options to providers
88
+ - The raw audio value for option 2 (if needed)
89
+
90
+ Raises:
91
+ gr.Error: On API or unexpected errors.
92
  """
93
  if not generated_text:
94
  logger.warning('Skipping text-to-speech due to empty text.')
95
  return gr.skip(), gr.skip(), gr.skip(), gr.skip()
96
 
97
  try:
 
98
  with ThreadPoolExecutor(max_workers=2) as executor:
99
  future_hume = executor.submit(text_to_speech_with_hume, prompt, generated_text)
100
  future_elevenlabs = executor.submit(text_to_speech_with_elevenlabs, generated_text)
 
102
  hume_audio = future_hume.result()
103
  elevenlabs_audio = future_elevenlabs.result()
104
 
105
+ logger.info(f'TTS generated: Hume={len(hume_audio)} bytes, ElevenLabs={len(elevenlabs_audio)} bytes')
 
 
 
 
106
  options = [(hume_audio, 'Hume AI'), (elevenlabs_audio, 'ElevenLabs')]
107
  random.shuffle(options)
 
108
  option_1_audio, option_2_audio = options[0][0], options[1][0]
109
  options_map = { OPTION_ONE: options[0][1], OPTION_TWO: options[1][1] }
110
 
111
  return (
112
+ gr.update(value=option_1_audio, autoplay=True),
113
+ gr.update(value=option_2_audio),
114
+ options_map,
115
+ option_2_audio,
116
  )
 
117
  except ElevenLabsError as ee:
118
  logger.error(f'ElevenLabsError while synthesizing speech from text: {str(ee)}')
119
  raise gr.Error('There was an issue communicating with the Elevenlabs API. Please try again later.')
 
120
  except HumeError as he:
121
  logger.error(f'HumeError while synthesizing speech from text: {str(he)}')
122
  raise gr.Error('There was an issue communicating with the Hume API. Please try again later.')
 
123
  except Exception as e:
124
  logger.error(f'Unexpected error during TTS generation: {e}')
125
  raise gr.Error('An unexpected error ocurred. Please try again later.')
126
 
127
 
128
+ def vote(vote_submitted: bool, option_mapping: dict, selected_button: str) -> Tuple[bool, gr.update, gr.update]:
129
  """
130
+ Handles user voting.
131
 
132
  Args:
133
+ vote_submitted (bool): True if a vote was already submitted
134
+ option_mapping (dict): Maps option labels to provider names
135
+ selected_button (str): The button clicked
136
 
137
  Returns:
138
+ A tuple of:
139
+ - True if the vote was accepted
140
+ - Update for the selected vote button
141
+ - Update for the unselected vote button
142
+ - Update for re-enabling the Generate button
143
  """
144
  if not option_mapping or vote_submitted:
145
+ return gr.skip(), gr.skip(), gr.skip()
146
 
 
147
  is_option_1 = selected_button == VOTE_FOR_OPTION_ONE
148
  selected_option, other_option = (OPTION_ONE, OPTION_TWO) if is_option_1 else (OPTION_TWO, OPTION_ONE)
 
 
149
  selected_provider = option_mapping.get(selected_option, UNKNOWN_PROVIDER)
150
  other_provider = option_mapping.get(other_option, UNKNOWN_PROVIDER)
151
 
 
152
  return (
153
  True,
154
+ gr.update(value=f'{selected_provider} {TROPHY_EMOJI}', variant='primary') if is_option_1
155
+ else gr.update(value=other_provider, variant='secondary'),
156
+ gr.update(value=other_provider, variant='secondary') if is_option_1
157
+ else gr.update(value=f'{selected_provider} {TROPHY_EMOJI}', variant='primary'),
158
  )
159
 
160
+ def reset_ui() -> Tuple[gr.update, gr.update, None, None, bool]:
161
+ """
162
+ Resets UI state before generating new text.
163
+
164
+ Returns:
165
+ A tuple of updates for:
166
+ - vote_button_1
167
+ - vote_button_2
168
+ - option_mapping_state
169
+ - option2_audio_state
170
+ - vote_submitted_state
171
+ """
172
+ return (
173
+ gr.update(interactive=False, value=VOTE_FOR_OPTION_ONE, variant='secondary'),
174
+ gr.update(interactive=False, value=VOTE_FOR_OPTION_TWO, variant='secondary'),
175
+ None,
176
+ None,
177
+ False,
178
+ )
179
+
180
+
181
+ def build_input_section() -> Tuple[gr.Markdown, gr.Dropdown, gr.Textbox, gr.Button]:
182
+ """Builds the input section including instructions, sample prompt dropdown, prompt input, and generate button"""
183
+ with gr.Column(variant='compact'):
184
+ instructions = gr.Markdown(
185
+ 'Generate text with **Claude by Anthropic**, listen to text-to-speech outputs '
186
+ 'from **Hume AI** and **ElevenLabs**, and vote for your favorite!'
187
+ )
188
+ sample_prompt_dropdown = gr.Dropdown(
189
+ choices=list(SAMPLE_PROMPTS.keys()),
190
+ label='Choose a sample prompt (or enter your own)',
191
+ value=None,
192
+ interactive=True,
193
+ )
194
+ prompt_input = gr.Textbox(
195
+ label='Prompt',
196
+ placeholder='Enter your prompt...',
197
+ lines=2,
198
+ max_lines=2,
199
+ show_copy_button=True,
200
+ )
201
+ generate_button = gr.Button('Generate', variant='primary')
202
+ return instructions, sample_prompt_dropdown, prompt_input, generate_button
203
+
204
+
205
+ def build_output_section() -> Tuple[gr.Textbox, gr.Audio, gr.Audio, gr.Button, gr.Button]:
206
+ """Builds the output section including generated text, audio players, and vote buttons."""
207
+ with gr.Column(variant='compact'):
208
+ generated_text = gr.Textbox(
209
+ label='Generated text',
210
+ interactive=False,
211
+ autoscroll=False,
212
+ lines=5,
213
+ max_lines=5,
214
+ max_length=PROMPT_MAX_LENGTH,
215
+ show_copy_button=True,
216
+ )
217
+ with gr.Row(equal_height=True):
218
+ option1_audio_player = gr.Audio(label=OPTION_ONE, type='filepath', interactive=False)
219
+ option2_audio_player = gr.Audio(label=OPTION_TWO, type='filepath', interactive=False)
220
+ with gr.Row():
221
+ vote_button_1 = gr.Button(VOTE_FOR_OPTION_ONE, interactive=False)
222
+ vote_button_2 = gr.Button(VOTE_FOR_OPTION_TWO, interactive=False)
223
+ return generated_text, option1_audio_player, option2_audio_player, vote_button_1, vote_button_2
224
+
225
 
226
  def build_gradio_interface() -> gr.Blocks:
227
  """
 
234
  with gr.Blocks(
235
  title='Expressive TTS Arena',
236
  theme=custom_theme,
237
+ fill_width=True,
238
+ css_paths='src/assets/styles.css'
239
  ) as demo:
240
  # Title
241
  gr.Markdown('# Expressive TTS Arena')
242
 
243
+ # Build input section
244
+ instructions, sample_prompt_dropdown, prompt_input, generate_button = build_input_section()
245
+
246
+ # Build output section
247
+ generated_text, option1_audio_player, option2_audio_player, vote_button_1, vote_button_2 = build_output_section()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
  # UI state components
250
  option_mapping_state = gr.State() # Track option map (option 1 and option 2 are randomized)
251
  option2_audio_state = gr.State() # Track generated audio for option 2 for playing automatically after option 1 audio finishes
 
252
  vote_submitted_state = gr.State(False) # Track whether the user has voted on an option
253
 
254
+ # --- Register event handlers ---
255
+
256
+ # When a sample prompt is chosen, update the prompt textbox
257
  sample_prompt_dropdown.change(
258
  fn=lambda choice: SAMPLE_PROMPTS.get(choice, ''),
259
  inputs=[sample_prompt_dropdown],
260
  outputs=[prompt_input],
261
  )
262
 
263
+ # Generate Button Click Handler Chain:
264
+ # 1. Disable the Generate button
265
+ # 2. Reset UI state
266
+ # 3. Generate text
267
+ # 4. Synthesize TTS
268
+ # 5. Re-enable the Generate button
269
  generate_button.click(
270
+ fn=lambda: gr.update(interactive=False), # Disable the button immediately
 
 
 
 
 
 
 
 
271
  inputs=[],
272
+ outputs=[generate_button]
273
+ ).then(
274
+ fn=reset_ui,
275
+ inputs=[],
276
+ outputs=[vote_button_1, vote_button_2, option_mapping_state, option2_audio_state, vote_submitted_state],
277
  ).then(
 
278
  fn=generate_text,
279
  inputs=[prompt_input],
280
  outputs=[generated_text],
281
  ).then(
 
282
  fn=text_to_speech,
283
  inputs=[prompt_input, generated_text],
284
  outputs=[option1_audio_player, option2_audio_player, option_mapping_state, option2_audio_state],
285
  ).then(
286
+ fn=lambda: gr.update(interactive=True), # Re-enable the button
 
287
  inputs=[],
288
  outputs=[generate_button]
289
  )
290
 
291
+ # Vote button click handlers
292
  vote_button_1.click(
293
  fn=vote,
294
  inputs=[vote_submitted_state, option_mapping_state, vote_button_1],
295
  outputs=[vote_submitted_state, vote_button_1, vote_button_2],
296
  )
 
 
297
  vote_button_2.click(
298
  fn=vote,
299
  inputs=[vote_submitted_state, option_mapping_state, vote_button_2],
 
311
  outputs=[option2_audio_player],
312
  )
313
 
314
+ # Enable voting after second audio option playback finishes
315
  option2_audio_player.stop(
316
  fn=lambda _: (gr.update(interactive=True), gr.update(interactive=True), gr.update(autoplay=False)),
317
  inputs=[],