zach commited on
Commit
fcb34bb
·
1 Parent(s): 0dde48b

Update theme configuration and clean up app.py

Browse files
Files changed (5) hide show
  1. src/app.py +112 -39
  2. src/constants.py +2 -0
  3. src/integrations/hume_api.py +1 -1
  4. src/theme.py +76 -54
  5. src/utils.py +12 -10
src/app.py CHANGED
@@ -12,6 +12,7 @@ Users can compare the outputs in an interactive UI.
12
  from concurrent.futures import ThreadPoolExecutor
13
  from functools import partial
14
  import random
 
15
  # Third-Party Library Imports
16
  import gradio as gr
17
  # Local Application Imports
@@ -19,11 +20,13 @@ from src.config import logger
19
  from src.constants import (
20
  OPTION_ONE,
21
  OPTION_TWO,
22
- VOTE_FOR_OPTION_ONE,
23
- VOTE_FOR_OPTION_TWO,
24
  PROMPT_MAX_LENGTH,
25
  PROMPT_MIN_LENGTH,
26
- SAMPLE_PROMPTS
 
 
 
 
27
  )
28
  from src.integrations import (
29
  generate_text_with_claude,
@@ -34,19 +37,22 @@ from src.theme import CustomTheme
34
  from src.utils import truncate_text, validate_prompt_length
35
 
36
 
37
- def generate_text(prompt: str):
38
  """
39
- Generates text from Claude API.
40
 
41
  Args:
42
  prompt (str): User-provided text prompt.
 
 
 
 
43
  """
44
  logger.info(f'Generating text with prompt: {truncate_text(prompt, max_length=100)}')
45
  try:
46
  # Validate prompt length
47
  validate_prompt_length(prompt, PROMPT_MAX_LENGTH, PROMPT_MIN_LENGTH)
48
 
49
-
50
  # Generate text
51
  generated_text = generate_text_with_claude(prompt)
52
  logger.info(f'Generated text ({len(generated_text)} characters).')
@@ -57,15 +63,29 @@ def generate_text(prompt: str):
57
  logger.warning(f'Validation error: {ve}')
58
  return str(ve)
59
 
60
- def text_to_speech(prompt: str, generated_text: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  try:
62
  # Generate TTS output in parallel
63
  with ThreadPoolExecutor(max_workers=2) as executor:
64
- hume_audio, elevenlabs_audio = executor.map(
65
- lambda func: func(),
66
- [partial(text_to_speech_with_hume, prompt, generated_text),
67
- partial(text_to_speech_with_elevenlabs, generated_text)]
68
- )
69
 
70
  logger.info(
71
  f'TTS generated: Hume={len(hume_audio)} bytes, ElevenLabs={len(elevenlabs_audio)} bytes'
@@ -95,45 +115,55 @@ def text_to_speech(prompt: str, generated_text: str):
95
  return None, None, {}
96
 
97
 
98
- def vote(option_mapping: dict, selected_button: str):
 
 
 
 
99
  """
100
- Updates both vote buttons to reflect the user's choice.
101
 
102
  Args:
103
- option_mapping (dict): Maps "Option 1" and "Option 2" to their TTS providers.
104
- selected_button (str): The label of the button that was clicked.
 
105
 
106
  Returns:
107
- tuple[gr.update, gr.update]: Updated properties for both vote buttons.
 
 
 
 
108
  """
109
- if not option_mapping:
110
- return gr.update(), gr.update() # No updates if mapping is missing
111
 
112
  # Determine selected option
113
  is_option_1 = selected_button == VOTE_FOR_OPTION_ONE
114
  selected_option, other_option = (OPTION_ONE, OPTION_TWO) if is_option_1 else (OPTION_TWO, OPTION_ONE)
115
 
116
  # Get provider names
117
- selected_provider = option_mapping.get(selected_option, "Unknown")
118
- other_provider = option_mapping.get(other_option, "Unknown")
119
 
120
  # Return updated button states
121
  return (
122
- gr.update(value=f'{selected_provider} ✔', interactive=False, variant='primary') if is_option_1 else gr.update(value=other_provider, interactive=False, variant='secondary'),
123
- gr.update(value=other_provider, interactive=False, variant='secondary') if is_option_1 else gr.update(value=f'{selected_provider} ✔', interactive=False, variant='primary'),
 
124
  gr.update(interactive=True, variant='primary')
125
  )
126
 
127
 
128
  def build_gradio_interface() -> gr.Blocks:
129
  """
130
- Constructs the Gradio user interface.
131
 
132
  Returns:
133
- gr.Blocks: The Gradio UI layout.
134
  """
135
  custom_theme = CustomTheme()
136
- with gr.Blocks(title='Expressive TTS Arena', theme=custom_theme) as demo:
137
  # Title
138
  gr.Markdown('# Expressive TTS Arena')
139
 
@@ -154,8 +184,8 @@ def build_gradio_interface() -> gr.Blocks:
154
 
155
  # Prompt input
156
  prompt_input = gr.Textbox(
157
- label='Enter your prompt',
158
- placeholder='Or type your own...',
159
  lines=2,
160
  max_lines=2,
161
  show_copy_button=True,
@@ -177,8 +207,16 @@ def build_gradio_interface() -> gr.Blocks:
177
 
178
  # Audio players
179
  with gr.Row():
180
- option1_audio_player = gr.Audio(label=OPTION_ONE, type='filepath', interactive=False)
181
- option2_audio_player = gr.Audio(label=OPTION_TWO, type='filepath', interactive=False)
 
 
 
 
 
 
 
 
182
 
183
  # Vote buttons
184
  with gr.Row():
@@ -186,9 +224,10 @@ def build_gradio_interface() -> gr.Blocks:
186
  vote_button_2 = gr.Button(VOTE_FOR_OPTION_TWO, interactive=False)
187
 
188
  # UI state components
189
- option_mapping_state = gr.State()
190
- option2_audio_state = gr.State()
191
- generated_text_state = gr.State()
 
192
 
193
  # Event handlers
194
  sample_prompt_dropdown.change(
@@ -198,35 +237,69 @@ def build_gradio_interface() -> gr.Blocks:
198
  )
199
 
200
  generate_button.click(
 
201
  fn=lambda _: (
202
  gr.update(interactive=False),
203
  gr.update(interactive=False, value=VOTE_FOR_OPTION_ONE, variant='secondary'),
204
  gr.update(interactive=False, value=VOTE_FOR_OPTION_TWO, variant='secondary'),
205
  None,
206
  None,
 
207
  ),
208
  inputs=[],
209
- outputs=[generate_button, vote_button_1, vote_button_2, option_mapping_state, option2_audio_state]
 
 
 
 
 
 
 
210
  ).then(
 
211
  fn=generate_text,
212
  inputs=[prompt_input],
213
  outputs=[generated_text]
214
  ).then(
 
215
  fn=text_to_speech,
216
  inputs=[prompt_input, generated_text],
217
- outputs=[option1_audio_player, option2_audio_player, option_mapping_state, option2_audio_state]
 
 
 
 
 
218
  )
219
 
220
  vote_button_1.click(
221
  fn=vote,
222
- inputs=[option_mapping_state, vote_button_1],
223
- outputs=[vote_button_1, vote_button_2, generate_button]
 
 
 
 
 
 
 
 
 
224
  )
225
 
226
  vote_button_2.click(
227
  fn=vote,
228
- inputs=[option_mapping_state, vote_button_2],
229
- outputs=[vote_button_1, vote_button_2, generate_button]
 
 
 
 
 
 
 
 
 
230
  )
231
 
232
  # Auto-play second audio after first finishes
 
12
  from concurrent.futures import ThreadPoolExecutor
13
  from functools import partial
14
  import random
15
+ from typing import Union
16
  # Third-Party Library Imports
17
  import gradio as gr
18
  # Local Application Imports
 
20
  from src.constants import (
21
  OPTION_ONE,
22
  OPTION_TWO,
 
 
23
  PROMPT_MAX_LENGTH,
24
  PROMPT_MIN_LENGTH,
25
+ SAMPLE_PROMPTS,
26
+ TROPHY_EMOJI,
27
+ UNKNOWN_PROVIDER,
28
+ VOTE_FOR_OPTION_ONE,
29
+ VOTE_FOR_OPTION_TWO
30
  )
31
  from src.integrations import (
32
  generate_text_with_claude,
 
37
  from src.utils import truncate_text, validate_prompt_length
38
 
39
 
40
+ def generate_text(prompt: str) -> Union[str, gr.update]:
41
  """
42
+ Generates text using the Claude API.
43
 
44
  Args:
45
  prompt (str): User-provided text prompt.
46
+
47
+ Returns:
48
+ Union[str, gr.update]: The generated text wrapped in `gr.update` for Gradio UI,
49
+ or an error message as a string if validation fails.
50
  """
51
  logger.info(f'Generating text with prompt: {truncate_text(prompt, max_length=100)}')
52
  try:
53
  # Validate prompt length
54
  validate_prompt_length(prompt, PROMPT_MAX_LENGTH, PROMPT_MIN_LENGTH)
55
 
 
56
  # Generate text
57
  generated_text = generate_text_with_claude(prompt)
58
  logger.info(f'Generated text ({len(generated_text)} characters).')
 
63
  logger.warning(f'Validation error: {ve}')
64
  return str(ve)
65
 
66
+ def text_to_speech(prompt: str, generated_text: str) -> tuple[gr.update, gr.update, dict, str | None]:
67
+ """
68
+ Converts generated text to speech using Hume AI and ElevenLabs APIs.
69
+
70
+ Args:
71
+ prompt (str): The original user-provided prompt.
72
+ generated_text (str): The generated text that will be converted to speech.
73
+
74
+ Returns:
75
+ tuple[gr.update, gr.update, dict, Union[str, None]]:
76
+ - `gr.update(value=option_1_audio, autoplay=True)`: Updates the first audio player.
77
+ - `gr.update(value=option_2_audio)`: Updates the second audio player.
78
+ - `options_map`: A dictionary mapping OPTION_ONE and OPTION_TWO to their providers.
79
+ - `option_2_audio`: The second audio file path or `None` if an error occurs.
80
+ """
81
  try:
82
  # Generate TTS output in parallel
83
  with ThreadPoolExecutor(max_workers=2) as executor:
84
+ future_hume = executor.submit(text_to_speech_with_hume, prompt, generated_text)
85
+ future_elevenlabs = executor.submit(text_to_speech_with_elevenlabs, generated_text)
86
+
87
+ hume_audio = future_hume.result()
88
+ elevenlabs_audio = future_elevenlabs.result()
89
 
90
  logger.info(
91
  f'TTS generated: Hume={len(hume_audio)} bytes, ElevenLabs={len(elevenlabs_audio)} bytes'
 
115
  return None, None, {}
116
 
117
 
118
+ def vote(
119
+ vote_submitted: bool,
120
+ option_mapping: dict,
121
+ selected_button: str
122
+ ) -> tuple[bool, gr.update, gr.update, gr.update]:
123
  """
124
+ Handles user voting and updates the UI to reflect the selected choice.
125
 
126
  Args:
127
+ vote_submitted (bool): Indicates if a vote has already been submitted.
128
+ option_mapping (dict): Maps "Option 1" and "Option 2" to their respective TTS providers.
129
+ selected_button (str): The button that was clicked by the user.
130
 
131
  Returns:
132
+ tuple[bool, gr.update, gr.update, gr.update]:
133
+ - `True`: Indicates the vote has been submitted.
134
+ - `gr.update`: Updates the selected vote button.
135
+ - `gr.update`: Updates the unselected vote button.
136
+ - `gr.update(interactive=True)`: Enables the "Generate" button after voting.
137
  """
138
+ if not option_mapping or vote_submitted:
139
+ return True, gr.update(), gr.update(), gr.update() # No updates if mapping is missing
140
 
141
  # Determine selected option
142
  is_option_1 = selected_button == VOTE_FOR_OPTION_ONE
143
  selected_option, other_option = (OPTION_ONE, OPTION_TWO) if is_option_1 else (OPTION_TWO, OPTION_ONE)
144
 
145
  # Get provider names
146
+ selected_provider = option_mapping.get(selected_option, UNKNOWN_PROVIDER)
147
+ other_provider = option_mapping.get(other_option, UNKNOWN_PROVIDER)
148
 
149
  # Return updated button states
150
  return (
151
+ True,
152
+ gr.update(value=f'{selected_provider} {TROPHY_EMOJI}', variant='primary') if is_option_1 else gr.update(value=other_provider, variant='secondary'),
153
+ gr.update(value=other_provider, variant='secondary') if is_option_1 else gr.update(value=f'{selected_provider} {TROPHY_EMOJI}', variant='primary'),
154
  gr.update(interactive=True, variant='primary')
155
  )
156
 
157
 
158
  def build_gradio_interface() -> gr.Blocks:
159
  """
160
+ Builds and configures the Gradio user interface.
161
 
162
  Returns:
163
+ gr.Blocks: The fully constructed Gradio UI layout.
164
  """
165
  custom_theme = CustomTheme()
166
+ with gr.Blocks(title='Expressive TTS Arena', theme=custom_theme, fill_width=True) as demo:
167
  # Title
168
  gr.Markdown('# Expressive TTS Arena')
169
 
 
184
 
185
  # Prompt input
186
  prompt_input = gr.Textbox(
187
+ show_label=False,
188
+ placeholder='Enter your prompt...',
189
  lines=2,
190
  max_lines=2,
191
  show_copy_button=True,
 
207
 
208
  # Audio players
209
  with gr.Row():
210
+ option1_audio_player = gr.Audio(
211
+ label=OPTION_ONE,
212
+ type='filepath',
213
+ interactive=False
214
+ )
215
+ option2_audio_player = gr.Audio(
216
+ label=OPTION_TWO,
217
+ type='filepath',
218
+ interactive=False
219
+ )
220
 
221
  # Vote buttons
222
  with gr.Row():
 
224
  vote_button_2 = gr.Button(VOTE_FOR_OPTION_TWO, interactive=False)
225
 
226
  # UI state components
227
+ option_mapping_state = gr.State() # Track option map (option 1 and option 2 are randomized)
228
+ option2_audio_state = gr.State() # Track generated audio for option 2 for playing automatically after option 1 audio finishes
229
+ generated_text_state = gr.State() # Track the text which was generated from the user's prompt
230
+ vote_submitted_state = gr.State(False) # Track whether the user has voted on an option
231
 
232
  # Event handlers
233
  sample_prompt_dropdown.change(
 
237
  )
238
 
239
  generate_button.click(
240
+ # Reset UI
241
  fn=lambda _: (
242
  gr.update(interactive=False),
243
  gr.update(interactive=False, value=VOTE_FOR_OPTION_ONE, variant='secondary'),
244
  gr.update(interactive=False, value=VOTE_FOR_OPTION_TWO, variant='secondary'),
245
  None,
246
  None,
247
+ False
248
  ),
249
  inputs=[],
250
+ outputs=[
251
+ generate_button,
252
+ vote_button_1,
253
+ vote_button_2,
254
+ option_mapping_state,
255
+ option2_audio_state,
256
+ vote_submitted_state
257
+ ]
258
  ).then(
259
+ # Generate text from user prompt
260
  fn=generate_text,
261
  inputs=[prompt_input],
262
  outputs=[generated_text]
263
  ).then(
264
+ # Synthesize text to speech and trigger playback of generated audio
265
  fn=text_to_speech,
266
  inputs=[prompt_input, generated_text],
267
+ outputs=[
268
+ option1_audio_player,
269
+ option2_audio_player,
270
+ option_mapping_state,
271
+ option2_audio_state
272
+ ]
273
  )
274
 
275
  vote_button_1.click(
276
  fn=vote,
277
+ inputs=[
278
+ vote_submitted_state,
279
+ option_mapping_state,
280
+ vote_button_1
281
+ ],
282
+ outputs=[
283
+ vote_submitted_state,
284
+ vote_button_1,
285
+ vote_button_2,
286
+ generate_button
287
+ ]
288
  )
289
 
290
  vote_button_2.click(
291
  fn=vote,
292
+ inputs=[
293
+ vote_submitted_state,
294
+ option_mapping_state,
295
+ vote_button_2
296
+ ],
297
+ outputs=[
298
+ vote_submitted_state,
299
+ vote_button_1,
300
+ vote_button_2,
301
+ generate_button
302
+ ]
303
  )
304
 
305
  # Auto-play second audio after first finishes
src/constants.py CHANGED
@@ -11,6 +11,8 @@ PROMPT_MAX_LENGTH: int = 300
11
  # Vote button constants
12
  OPTION_ONE: str = "Option 1"
13
  OPTION_TWO: str = "Option 2"
 
 
14
  VOTE_FOR_OPTION_ONE: str = "Vote for option 1"
15
  VOTE_FOR_OPTION_TWO: str = "Vote for option 2"
16
 
 
11
  # Vote button constants
12
  OPTION_ONE: str = "Option 1"
13
  OPTION_TWO: str = "Option 2"
14
+ TROPHY_EMOJI = "🏆"
15
+ UNKNOWN_PROVIDER = "Unknown"
16
  VOTE_FOR_OPTION_ONE: str = "Vote for option 1"
17
  VOTE_FOR_OPTION_TWO: str = "Vote for option 2"
18
 
src/integrations/hume_api.py CHANGED
@@ -36,7 +36,7 @@ class HumeConfig:
36
  """Immutable configuration for interacting with the Hume TTS API."""
37
  tts_endpoint_url: str = 'https://api.hume.ai/v0/tts'
38
  api_key: str = validate_env_var('HUME_API_KEY')
39
- voices: List[str] = ('ITO', 'KORA', 'DACHER') # List of available Hume voices
40
  audio_format: str = 'wav'
41
  headers: dict = None # Headers for the API requests
42
 
 
36
  """Immutable configuration for interacting with the Hume TTS API."""
37
  tts_endpoint_url: str = 'https://api.hume.ai/v0/tts'
38
  api_key: str = validate_env_var('HUME_API_KEY')
39
+ voices: List[str] = ('ITO', 'KORA', 'STELLA') # List of available Hume voices
40
  audio_format: str = 'wav'
41
  headers: dict = None # Headers for the API requests
42
 
src/theme.py CHANGED
@@ -1,32 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
  from collections.abc import Iterable
 
3
  from gradio.themes.base import Base
4
  from gradio.themes.utils import colors, fonts, sizes
5
 
6
- # TODO: Update theme styling
7
-
8
- # Custom theme
9
  class CustomTheme(Base):
10
  def __init__(
11
  self,
12
  *,
13
- primary_hue: colors.Color | str = colors.orange,
14
- secondary_hue: colors.Color | str = colors.blue,
15
- neutral_hue: colors.Color | str = colors.zinc,
16
  spacing_size: sizes.Size | str = sizes.spacing_md,
17
  radius_size: sizes.Size | str = sizes.radius_md,
18
  text_size: sizes.Size | str = sizes.text_md,
19
  font: fonts.Font | str | Iterable[fonts.Font | str] = (
20
- fonts.GoogleFont("Source Sans Pro"),
21
- "ui-sans-serif",
22
- "system-ui",
23
- "sans-serif",
24
  ),
25
  font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
26
- fonts.LocalFont("IBM Plex Mono"),
27
- "ui-monospace",
28
- "Consolas",
29
- "monospace",
30
  ),
31
  ):
32
  super().__init__(
@@ -39,53 +54,60 @@ class CustomTheme(Base):
39
  font=font,
40
  font_mono=font_mono,
41
  )
42
- self.name = "custom_theme"
43
  super().set(
44
  # Colors
45
- input_background_fill_dark="*neutral_800",
46
- error_background_fill=colors.red.c50,
47
- error_background_fill_dark="*neutral_900",
48
- error_border_color=colors.red.c700,
49
- error_border_color_dark=colors.red.c500,
50
- error_icon_color=colors.red.c700,
51
- error_icon_color_dark=colors.red.c500,
 
52
  # Shadows
53
- input_shadow_focus="0 0 0 *shadow_spread *secondary_50, *shadow_inset",
54
- input_shadow_focus_dark="0 0 0 *shadow_spread *neutral_700, *shadow_inset",
 
55
  # Button borders
56
- button_border_width="0px",
57
- input_border_width="1px",
58
- input_background_fill="white",
 
59
  # Gradients
60
- stat_background_fill="linear-gradient(to right, *primary_400, *primary_200)",
61
- stat_background_fill_dark="linear-gradient(to right, *primary_400, *primary_600)",
62
- checkbox_label_background_fill="*background_fill_primary",
63
- checkbox_label_background_fill_dark="*neutral_800",
64
- checkbox_label_background_fill_hover="*background_fill_secondary",
65
- checkbox_label_background_fill_hover_dark="*checkbox_label_background_fill",
 
66
  # Primary Button
67
- button_primary_background_fill="*primary_500",
68
- button_primary_background_fill_dark="*primary_600",
69
- button_primary_background_fill_hover="*primary_600",
70
- button_primary_background_fill_hover_dark="*primary_700",
71
- button_primary_text_color="white",
72
- button_primary_text_color_dark="white",
 
73
  # Secondary Button
74
- button_secondary_background_fill="*neutral_200",
75
- button_secondary_background_fill_dark="*neutral_600",
76
- button_secondary_background_fill_hover="*neutral_300",
77
- button_secondary_background_fill_hover_dark="*neutral_700",
78
- button_secondary_text_color="black",
79
- button_secondary_text_color_dark="white",
 
80
  # Cancel Button
81
- button_cancel_background_fill=colors.red.c500,
82
- button_cancel_background_fill_dark=colors.red.c700,
83
- button_cancel_background_fill_hover=colors.red.c600,
84
- button_cancel_background_fill_hover_dark=colors.red.c800,
85
- button_cancel_text_color="white",
86
- button_cancel_text_color_dark="white",
87
- button_cancel_text_color_hover="white",
88
- button_cancel_text_color_hover_dark="white",
 
89
  # Other
90
- border_color_accent_subdued="*primary_200",
91
  )
 
1
+ """
2
+ custom_theme.py
3
+
4
+ Defines a custom Gradio theme.
5
+
6
+ This module creates a `CustomTheme` class, inheriting from `gradio.themes.base.Base`,
7
+ and overrides default styling variables. It uses CSS variables for consistency
8
+ with the application's styling.
9
+
10
+ Key Features:
11
+ - Defines a color palette using CSS variables.
12
+ - Customizes styling for Gradio components (buttons, inputs, etc.).
13
+ - Adjusts shadows, borders, and gradients.
14
+ - Supports light and dark modes.
15
+ """
16
+
17
+ # Standard Library Imports
18
  from __future__ import annotations
19
  from collections.abc import Iterable
20
+ # Third-Party Library Imports
21
  from gradio.themes.base import Base
22
  from gradio.themes.utils import colors, fonts, sizes
23
 
 
 
 
24
  class CustomTheme(Base):
25
  def __init__(
26
  self,
27
  *,
28
+ primary_hue: colors.Color | str = colors.purple,
29
+ secondary_hue: colors.Color | str = colors.stone,
30
+ neutral_hue: colors.Color | str = colors.neutral,
31
  spacing_size: sizes.Size | str = sizes.spacing_md,
32
  radius_size: sizes.Size | str = sizes.radius_md,
33
  text_size: sizes.Size | str = sizes.text_md,
34
  font: fonts.Font | str | Iterable[fonts.Font | str] = (
35
+ fonts.GoogleFont('Source Sans Pro'),
36
+ 'ui-sans-serif',
37
+ 'system-ui',
38
+ 'sans-serif',
39
  ),
40
  font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
41
+ fonts.GoogleFont('IBM Plex Mono'),
42
+ 'ui-monospace',
43
+ 'Consolas',
44
+ 'monospace',
45
  ),
46
  ):
47
  super().__init__(
 
54
  font=font,
55
  font_mono=font_mono,
56
  )
57
+ self.name = 'custom_theme'
58
  super().set(
59
  # Colors
60
+ input_background_fill_dark='#262626',
61
+ error_background_fill='#EF4444',
62
+ error_background_fill_dark='#171717',
63
+ error_border_color='#B91C1C',
64
+ error_border_color_dark='#EF4444',
65
+ error_icon_color='#B91C1C',
66
+ error_icon_color_dark='#EF4444',
67
+
68
  # Shadows
69
+ input_shadow_focus='0 0 0 *shadow_spread #7C3AED80, *shadow_inset',
70
+ input_shadow_focus_dark='0 0 0 *shadow_spread #40404080, *shadow_inset',
71
+
72
  # Button borders
73
+ button_border_width='0px',
74
+ input_border_width='1px',
75
+ input_background_fill='#F9FAFB',
76
+
77
  # Gradients
78
+ stat_background_fill='linear-gradient(to right, #7C3AED, #D8B4FE)',
79
+ stat_background_fill_dark='linear-gradient(to right, #7C3AED, #5B21B6)',
80
+ checkbox_label_background_fill='#F3F4F6',
81
+ checkbox_label_background_fill_dark='#1F2937',
82
+ checkbox_label_background_fill_hover='#E5E7EB',
83
+ checkbox_label_background_fill_hover_dark='#374151',
84
+
85
  # Primary Button
86
+ button_primary_background_fill='#111111',
87
+ button_primary_background_fill_dark='#171717',
88
+ button_primary_background_fill_hover='#3F3F3F',
89
+ button_primary_background_fill_hover_dark='#3F3F3F',
90
+ button_primary_text_color='#FFFFFF',
91
+ button_primary_text_color_dark='#FFFFFF',
92
+
93
  # Secondary Button
94
+ button_secondary_background_fill='#E5E7EB',
95
+ button_secondary_background_fill_dark='#4B5563',
96
+ button_secondary_background_fill_hover='#D1D5DB',
97
+ button_secondary_background_fill_hover_dark='#374151',
98
+ button_secondary_text_color='#111827',
99
+ button_secondary_text_color_dark='#FFFFFF',
100
+
101
  # Cancel Button
102
+ button_cancel_background_fill='#EF4444',
103
+ button_cancel_background_fill_dark='#B91C1C',
104
+ button_cancel_background_fill_hover='#DC2626',
105
+ button_cancel_background_fill_hover_dark='#991B1B',
106
+ button_cancel_text_color='#FFFFFF',
107
+ button_cancel_text_color_dark='#FFFFFF',
108
+ button_cancel_text_color_hover='#FFFFFF',
109
+ button_cancel_text_color_hover_dark='#FFFFFF',
110
+
111
  # Other
112
+ border_color_accent_subdued='#A78BFA',
113
  )
src/utils.py CHANGED
@@ -96,24 +96,26 @@ def validate_prompt_length(prompt: str, max_length: int, min_length: int) -> Non
96
  >>> validate_prompt_length("Hello world", max_length=500, min_length=5)
97
  # Passes validation
98
 
99
- >>> validate_prompt_length("", max_length=500, min_length=1)
100
- # Raises ValueError: "Prompt must be at least 1 character(s) long."
101
  """
102
- logger.debug(f'Prompt length being validated: {len(prompt)} characters')
103
-
104
- # Check if prompt is empty or too short
105
  stripped_prompt = prompt.strip()
106
- if len(stripped_prompt) < min_length:
 
 
 
 
 
107
  raise ValueError(
108
  f'Prompt must be at least {min_length} character(s) long. '
109
- f'Received only {len(stripped_prompt)}.'
110
  )
111
 
112
- # Check if prompt is too long
113
- if len(stripped_prompt) > max_length:
114
  raise ValueError(
115
  f'The prompt exceeds the maximum allowed length of {max_length} characters. '
116
- f'Your prompt contains {len(stripped_prompt)} characters.'
117
  )
118
 
119
  logger.debug(f'Prompt length validation passed for prompt: {truncate_text(stripped_prompt)}')
 
96
  >>> validate_prompt_length("Hello world", max_length=500, min_length=5)
97
  # Passes validation
98
 
99
+ >>> validate_prompt_length("", max_length=300, min_length=10)
100
+ # Raises ValueError: "Prompt must be at least 10 characters long."
101
  """
 
 
 
102
  stripped_prompt = prompt.strip()
103
+ prompt_length = len(stripped_prompt)
104
+
105
+ logger.debug(f'Prompt length being validated: {prompt_length} characters')
106
+
107
+ # Check if prompt is too short
108
+ if prompt_length < min_length:
109
  raise ValueError(
110
  f'Prompt must be at least {min_length} character(s) long. '
111
+ f'Received only {prompt_length}.'
112
  )
113
 
114
+ # Check if prompt exceeds max length
115
+ if prompt_length > max_length:
116
  raise ValueError(
117
  f'The prompt exceeds the maximum allowed length of {max_length} characters. '
118
+ f'Your prompt contains {prompt_length} characters.'
119
  )
120
 
121
  logger.debug(f'Prompt length validation passed for prompt: {truncate_text(stripped_prompt)}')