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

Add feedback error toasts when error occurs and clean up UI code

Browse files
Files changed (1) hide show
  1. src/app.py +69 -136
src/app.py CHANGED
@@ -39,7 +39,7 @@ from src.theme import CustomTheme
39
  from src.utils import truncate_text, validate_prompt_length
40
 
41
 
42
- def validate_and_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.
@@ -58,40 +58,21 @@ def validate_and_generate_text(prompt: str) -> tuple[Union[str, gr.update], gr.u
58
  validate_prompt_length(prompt, PROMPT_MAX_LENGTH, PROMPT_MIN_LENGTH)
59
  except ValueError as ve:
60
  logger.warning(f'Validation error: {ve}')
61
- # Show validation error to user, re-enable the "Generate" button
62
- return str(ve), gr.update(interactive=True)
63
 
64
  # Call the Anthropic API to generate text
65
  try:
66
  generated_text = generate_text_with_claude(prompt)
67
-
68
- # Optionally handle empty or unusual responses
69
- if not generated_text.strip():
70
- logger.warning("Anthropic API returned an empty response.")
71
- return "Error: Anthropic API returned an empty response.", gr.update(interactive=True)
72
-
73
  logger.info(f'Generated text ({len(generated_text)} characters).')
74
- return gr.update(value=generated_text), gr.update(interactive=False)
75
 
76
- # Handle Anthropic-specific errors
77
  except AnthropicError as ae:
78
- # You can log the internal error details
79
  logger.error(f'AnthropicError while generating text: {str(ae)}')
80
- # Return an error message about Anthropic API failing to generate text, re-enable the "Generate" button
81
- return (
82
- "Error: There was an issue communicating with the Anthropic API. Please try again later.",
83
- gr.update(interactive=True),
84
- )
85
 
86
- # Catch any other unexpected exceptions
87
  except Exception as e:
88
  logger.error(f'Unexpected error while generating text: {e}')
89
- # Return a generic catch-all error message, re-enable the "Generate" button
90
- return (
91
- "Error: Failed to generate text. Please try again.",
92
- gr.update(interactive=True),
93
- )
94
-
95
 
96
 
97
  def text_to_speech(prompt: str, generated_text: str) -> tuple[gr.update, gr.update, dict, str | None]:
@@ -112,12 +93,12 @@ def text_to_speech(prompt: str, generated_text: str) -> tuple[gr.update, gr.upda
112
  - `options_map`: A dictionary mapping OPTION_ONE and OPTION_TWO to their providers.
113
  - `option_2_audio`: The second audio file path or `None` if an error occurs.
114
  """
115
- if not generated_text or generated_text.startswith("Error:"):
116
- logger.warning("Skipping TTS generation due to invalid text.")
117
- return gr.update(value=None), gr.update(value=None), {}, None # Return empty updates
118
 
119
  try:
120
- # Generate TTS output in parallel
121
  with ThreadPoolExecutor(max_workers=2) as executor:
122
  future_hume = executor.submit(text_to_speech_with_hume, prompt, generated_text)
123
  future_elevenlabs = executor.submit(text_to_speech_with_elevenlabs, generated_text)
@@ -133,39 +114,30 @@ def text_to_speech(prompt: str, generated_text: str) -> tuple[gr.update, gr.upda
133
  options = [(hume_audio, 'Hume AI'), (elevenlabs_audio, 'ElevenLabs')]
134
  random.shuffle(options)
135
 
136
- option_1_audio = options[0][0]
137
- option_2_audio = options[1][0]
138
-
139
- option_1_provider = options[0][1]
140
- option_2_provider = options[1][1]
141
-
142
- options_map = { OPTION_ONE: option_1_provider, OPTION_TWO: option_2_provider }
143
 
144
  return (
145
- gr.update(value=option_1_audio, autoplay=True), # Set option 1 audio
146
- gr.update(value=option_2_audio), # Option 2 audio
147
- options_map, # Set option mapping state
148
- option_2_audio # Set option 2 audio state
149
  )
150
 
151
  except ElevenLabsError as ee:
152
- logger.error(f"ElevenLabsError while synthesizing speech from text: {str(ee)}")
153
- return gr.update(value=None), gr.update(value=None), {}, None
154
 
155
  except HumeError as he:
156
- logger.error(f"HumeError while synthesizing speech from text: {str(he)}")
157
- return gr.update(value=None), gr.update(value=None), {}, None
158
 
159
  except Exception as e:
160
  logger.error(f'Unexpected error during TTS generation: {e}')
161
- return gr.update(value=None), gr.update(value=None), {}, None
162
 
163
 
164
- def vote(
165
- vote_submitted: bool,
166
- option_mapping: dict,
167
- selected_button: str
168
- ) -> tuple[bool, gr.update, gr.update, gr.update]:
169
  """
170
  Handles user voting and updates the UI to reflect the selected choice.
171
 
@@ -182,7 +154,7 @@ def vote(
182
  - `gr.update(interactive=True)`: Enables the "Generate" button after voting.
183
  """
184
  if not option_mapping or vote_submitted:
185
- return True, gr.update(), gr.update(), gr.update() # No updates if mapping is missing
186
 
187
  # Determine selected option
188
  is_option_1 = selected_button == VOTE_FOR_OPTION_ONE
@@ -192,12 +164,11 @@ def vote(
192
  selected_provider = option_mapping.get(selected_option, UNKNOWN_PROVIDER)
193
  other_provider = option_mapping.get(other_option, UNKNOWN_PROVIDER)
194
 
195
- # Return updated button states
196
  return (
197
  True,
198
  gr.update(value=f'{selected_provider} {TROPHY_EMOJI}', variant='primary') if is_option_1 else gr.update(value=other_provider, variant='secondary'),
199
  gr.update(value=other_provider, variant='secondary') if is_option_1 else gr.update(value=f'{selected_provider} {TROPHY_EMOJI}', variant='primary'),
200
- gr.update(interactive=True, variant='primary')
201
  )
202
 
203
 
@@ -213,7 +184,7 @@ def build_gradio_interface() -> gr.Blocks:
213
  title='Expressive TTS Arena',
214
  theme=custom_theme,
215
  fill_width=True,
216
- css='footer{display:none !important}'
217
  ) as demo:
218
  # Title
219
  gr.Markdown('# Expressive TTS Arena')
@@ -221,8 +192,8 @@ def build_gradio_interface() -> gr.Blocks:
221
  with gr.Column(variant='compact'):
222
  # Instructions
223
  gr.Markdown(
224
- 'Generate text using **Claude by Anthropic**, then compare text-to-speech outputs '
225
- 'from **Hume AI** and **ElevenLabs**. Listen to both samples and vote for your favorite!'
226
  )
227
 
228
  # Sample prompt select
@@ -235,7 +206,7 @@ def build_gradio_interface() -> gr.Blocks:
235
 
236
  # Prompt input
237
  prompt_input = gr.Textbox(
238
- show_label=False,
239
  placeholder='Enter your prompt...',
240
  lines=2,
241
  max_lines=2,
@@ -248,38 +219,30 @@ def build_gradio_interface() -> gr.Blocks:
248
  with gr.Column(variant='compact'):
249
  # Generated text
250
  generated_text = gr.Textbox(
251
- label='Generated Text',
252
  interactive=False,
253
  autoscroll=False,
254
- lines=6,
255
- max_lines=6,
256
  max_length=PROMPT_MAX_LENGTH,
257
  show_copy_button=True,
258
  )
259
 
260
  # Audio players
261
- with gr.Row():
262
- option1_audio_player = gr.Audio(
263
- label=OPTION_ONE,
264
- type='filepath',
265
- interactive=False
266
- )
267
- option2_audio_player = gr.Audio(
268
- label=OPTION_TWO,
269
- type='filepath',
270
- interactive=False
271
- )
272
-
273
- # Vote buttons
274
- with gr.Row():
275
- vote_button_1 = gr.Button(VOTE_FOR_OPTION_ONE, interactive=False)
276
- vote_button_2 = gr.Button(VOTE_FOR_OPTION_TWO, interactive=False)
277
 
278
  # UI state components
279
- option_mapping_state = gr.State() # Track option map (option 1 and option 2 are randomized)
280
- option2_audio_state = gr.State() # Track generated audio for option 2 for playing automatically after option 1 audio finishes
281
- generated_text_state = gr.State() # Track the text which was generated from the user's prompt
282
- vote_submitted_state = gr.State(False) # Track whether the user has voted on an option
283
 
284
  # Event handlers
285
  sample_prompt_dropdown.change(
@@ -288,70 +251,48 @@ def build_gradio_interface() -> gr.Blocks:
288
  outputs=[prompt_input],
289
  )
290
 
 
291
  generate_button.click(
292
  # Reset UI
293
  fn=lambda _: (
294
- gr.update(interactive=False),
295
- gr.update(interactive=False, value=VOTE_FOR_OPTION_ONE, variant='secondary'),
296
- gr.update(interactive=False, value=VOTE_FOR_OPTION_TWO, variant='secondary'),
297
- None,
298
- None,
299
- False
300
  ),
301
  inputs=[],
302
- outputs=[
303
- generate_button,
304
- vote_button_1,
305
- vote_button_2,
306
- option_mapping_state,
307
- option2_audio_state,
308
- vote_submitted_state
309
- ]
310
  ).then(
311
- # Validate and prompt and generate text
312
- fn=validate_and_generate_text,
313
  inputs=[prompt_input],
314
- outputs=[generated_text, generate_button] # Ensure button gets re-enabled on failure
315
  ).then(
316
- # Validate generated text and synthesize text-to-speech
317
  fn=text_to_speech,
318
- inputs=[prompt_input, generated_text], # Pass prompt & generated text
319
- outputs=[
320
- option1_audio_player,
321
- option2_audio_player,
322
- option_mapping_state,
323
- option2_audio_state
324
- ]
325
  )
326
 
 
327
  vote_button_1.click(
328
  fn=vote,
329
- inputs=[
330
- vote_submitted_state,
331
- option_mapping_state,
332
- vote_button_1
333
- ],
334
- outputs=[
335
- vote_submitted_state,
336
- vote_button_1,
337
- vote_button_2,
338
- generate_button
339
- ]
340
  )
341
 
 
342
  vote_button_2.click(
343
  fn=vote,
344
- inputs=[
345
- vote_submitted_state,
346
- option_mapping_state,
347
- vote_button_2
348
- ],
349
- outputs=[
350
- vote_submitted_state,
351
- vote_button_1,
352
- vote_button_2,
353
- generate_button
354
- ]
355
  )
356
 
357
  # Auto-play second audio after first finishes
@@ -367,17 +308,9 @@ def build_gradio_interface() -> gr.Blocks:
367
 
368
  # Enable voting after 2nd audio option playback finishes
369
  option2_audio_player.stop(
370
- fn=lambda _: (
371
- gr.update(interactive=True),
372
- gr.update(interactive=True),
373
- gr.update(autoplay=False),
374
- ),
375
  inputs=[],
376
- outputs=[
377
- vote_button_1,
378
- vote_button_2,
379
- option2_audio_player,
380
- ],
381
  )
382
 
383
  logger.debug('Gradio interface built successfully')
 
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.
 
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]:
 
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)
 
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
 
 
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
 
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
 
 
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')
 
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
 
206
 
207
  # Prompt input
208
  prompt_input = gr.Textbox(
209
+ label='Prompt',
210
  placeholder='Enter your prompt...',
211
  lines=2,
212
  max_lines=2,
 
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(
 
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],
295
+ outputs=[vote_submitted_state, vote_button_1, vote_button_2],
 
 
 
 
 
 
 
 
 
296
  )
297
 
298
  # Auto-play second audio after first finishes
 
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=[],
313
+ outputs=[vote_button_1, vote_button_2, option2_audio_player],
 
 
 
 
314
  )
315
 
316
  logger.debug('Gradio interface built successfully')