Spaces:
Running
on
Zero
fix: resolve audio integration issues and improve UI layout
Browse filesFix multiple issues preventing audio features from working correctly:
Backend fixes:
- Remove incorrect await on ElevenLabs async generator in TTSService
- Remove incorrect await in DebateAudioGenerator._generate_segment
- Fix async generator pattern (use async for without await on generator call)
Frontend fixes:
- Fix handle_analysis to return and handle audio button in all yields
- Fix handle_build_portfolio to include audio button in all 4 yield statements
- Fix handle_compare_portfolio to include audio button in all 5 yield statements
- Handle both list and dict formats for portfolio_data in build result storage
- Add os import for file existence checks
- Wrap all audio players in Row containers for proper layout spacing
- Add debug logging to generate_debate_audio for troubleshooting
All three audio features (Analyse Portfolio, Build Portfolio, Compare Strategies) now work correctly with visible audio players.
- app.py +59 -28
- backend/audio/tts_service.py +2 -2
|
@@ -11,6 +11,7 @@ Features:
|
|
| 11 |
|
| 12 |
import gradio as gr
|
| 13 |
import asyncio
|
|
|
|
| 14 |
import re
|
| 15 |
import logging
|
| 16 |
import warnings
|
|
@@ -979,7 +980,8 @@ async def run_analysis_with_ui_update(
|
|
| 979 |
"input",
|
| 980 |
f"❌ Error: {str(e)}",
|
| 981 |
"",
|
| 982 |
-
None, None, None, None, None
|
|
|
|
| 983 |
)
|
| 984 |
|
| 985 |
|
|
@@ -2077,12 +2079,13 @@ def create_interface() -> gr.Blocks:
|
|
| 2077 |
size="sm",
|
| 2078 |
visible=False
|
| 2079 |
)
|
| 2080 |
-
|
| 2081 |
-
|
| 2082 |
-
|
| 2083 |
-
|
| 2084 |
-
|
| 2085 |
-
|
|
|
|
| 2086 |
|
| 2087 |
with gr.Row():
|
| 2088 |
build_regenerate_btn = gr.Button("Regenerate", variant="secondary", size="sm")
|
|
@@ -2155,12 +2158,13 @@ def create_interface() -> gr.Blocks:
|
|
| 2155 |
size="sm",
|
| 2156 |
visible=False
|
| 2157 |
)
|
| 2158 |
-
|
| 2159 |
-
|
| 2160 |
-
|
| 2161 |
-
|
| 2162 |
-
|
| 2163 |
-
|
|
|
|
| 2164 |
|
| 2165 |
# Debate transcript
|
| 2166 |
with gr.Accordion("View Full Debate", open=False):
|
|
@@ -2475,12 +2479,13 @@ def create_interface() -> gr.Blocks:
|
|
| 2475 |
size="sm",
|
| 2476 |
visible=False
|
| 2477 |
)
|
| 2478 |
-
|
| 2479 |
-
|
| 2480 |
-
|
| 2481 |
-
|
| 2482 |
-
|
| 2483 |
-
|
|
|
|
| 2484 |
|
| 2485 |
# Performance Metrics Accordion (progressive disclosure)
|
| 2486 |
with gr.Accordion("Performance Metrics & Reasoning", open=False):
|
|
@@ -2901,6 +2906,13 @@ def create_interface() -> gr.Blocks:
|
|
| 2901 |
"""Generate multi-speaker debate audio on-demand."""
|
| 2902 |
global LAST_DEBATE_DATA
|
| 2903 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2904 |
if not LAST_DEBATE_DATA:
|
| 2905 |
logger.warning("No debate data available for audio generation")
|
| 2906 |
return (
|
|
@@ -2947,6 +2959,8 @@ def create_interface() -> gr.Blocks:
|
|
| 2947 |
)
|
| 2948 |
|
| 2949 |
logger.info(f"Debate audio generated: {audio_path}")
|
|
|
|
|
|
|
| 2950 |
|
| 2951 |
return (
|
| 2952 |
gr.update(value=audio_path, visible=True),
|
|
@@ -2982,7 +2996,8 @@ def create_interface() -> gr.Blocks:
|
|
| 2982 |
yield (
|
| 2983 |
gr.update(value=[], visible=False), # build_agent_chat (empty, hidden)
|
| 2984 |
gr.update(visible=True), # build_results_container
|
| 2985 |
-
"Please select at least one investment goal." # build_status
|
|
|
|
| 2986 |
)
|
| 2987 |
return
|
| 2988 |
|
|
@@ -3029,7 +3044,8 @@ def create_interface() -> gr.Blocks:
|
|
| 3029 |
yield (
|
| 3030 |
gr.update(value=chat_messages, visible=True), # build_agent_chat (visible, growing list)
|
| 3031 |
gr.update(visible=False), # build_results_container (hidden during streaming)
|
| 3032 |
-
"" # build_status
|
|
|
|
| 3033 |
)
|
| 3034 |
logger.debug(f"UI update yielded successfully")
|
| 3035 |
except Exception as e:
|
|
@@ -3052,9 +3068,17 @@ def create_interface() -> gr.Blocks:
|
|
| 3052 |
final_message = chat_messages[-1]
|
| 3053 |
if isinstance(final_message, dict) and "metadata" in final_message:
|
| 3054 |
portfolio_data = final_message.get("metadata", {}).get("portfolio", {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3055 |
LAST_BUILD_RESULT = {
|
| 3056 |
"summary": final_message.get("content", ""),
|
| 3057 |
-
"holdings":
|
| 3058 |
"reasoning": final_message.get("metadata", {}).get("reasoning_trace", [])
|
| 3059 |
}
|
| 3060 |
logger.info("Build result stored for audio generation")
|
|
@@ -3092,7 +3116,8 @@ def create_interface() -> gr.Blocks:
|
|
| 3092 |
yield (
|
| 3093 |
gr.update(value=[], visible=False), # build_agent_chat (empty, hidden on error)
|
| 3094 |
gr.update(visible=True), # build_results_container
|
| 3095 |
-
f"Error building portfolio: {str(e)}" # build_status
|
|
|
|
| 3096 |
)
|
| 3097 |
|
| 3098 |
def handle_build_accept(portfolio_table):
|
|
@@ -3160,7 +3185,8 @@ def create_interface() -> gr.Blocks:
|
|
| 3160 |
0, # compare_bear_confidence
|
| 3161 |
"", # compare_consensus
|
| 3162 |
"", # compare_stance
|
| 3163 |
-
[] # compare_debate_transcript
|
|
|
|
| 3164 |
)
|
| 3165 |
return
|
| 3166 |
|
|
@@ -3172,7 +3198,8 @@ def create_interface() -> gr.Blocks:
|
|
| 3172 |
gr.update(value=[], visible=False), # compare_debate_chat (empty, hidden)
|
| 3173 |
gr.update(visible=True), # compare_results_container
|
| 3174 |
"Could not parse portfolio. Please check format.", # compare_status
|
| 3175 |
-
"", 0, "", 0, "", "", []
|
|
|
|
| 3176 |
)
|
| 3177 |
return
|
| 3178 |
|
|
@@ -3205,7 +3232,8 @@ def create_interface() -> gr.Blocks:
|
|
| 3205 |
0, # compare_bear_confidence
|
| 3206 |
"", # compare_consensus
|
| 3207 |
"", # compare_stance
|
| 3208 |
-
[] # compare_debate_transcript
|
|
|
|
| 3209 |
)
|
| 3210 |
|
| 3211 |
# Extract data from final consensus message
|
|
@@ -3264,7 +3292,8 @@ def create_interface() -> gr.Blocks:
|
|
| 3264 |
gr.update(value=[], visible=False), # compare_debate_chat (empty, hidden on error)
|
| 3265 |
gr.update(visible=True), # compare_results_container
|
| 3266 |
f"Error: {str(e)}", # compare_status
|
| 3267 |
-
"", 0, "", 0, "", "", []
|
|
|
|
| 3268 |
)
|
| 3269 |
|
| 3270 |
async def handle_test_changes(portfolio_text, changes_text, portfolio_value, session_state):
|
|
@@ -4068,7 +4097,7 @@ Please try again with different parameters.
|
|
| 4068 |
}
|
| 4069 |
|
| 4070 |
# Run analysis with progress updates
|
| 4071 |
-
page, analysis, perf_metrics, alloc, risk, perf, corr, opt = await run_analysis_with_ui_update(
|
| 4072 |
session_state, portfolio_text, roast_mode, persona, progress
|
| 4073 |
)
|
| 4074 |
|
|
@@ -4087,6 +4116,7 @@ Please try again with different parameters.
|
|
| 4087 |
performance_plot: perf,
|
| 4088 |
correlation_plot: corr,
|
| 4089 |
optimization_plot: opt,
|
|
|
|
| 4090 |
load_past_portfolio_dropdown: gr.update(choices=dropdown_choices) if dropdown_choices else gr.skip(),
|
| 4091 |
export_pdf_btn: LAST_EXPORT_PDF_PATH,
|
| 4092 |
export_csv_btn: LAST_EXPORT_CSV_PATH
|
|
@@ -4104,6 +4134,7 @@ Please try again with different parameters.
|
|
| 4104 |
performance_plot: None,
|
| 4105 |
correlation_plot: None,
|
| 4106 |
optimization_plot: None,
|
|
|
|
| 4107 |
load_past_portfolio_dropdown: gr.skip(),
|
| 4108 |
export_pdf_btn: gr.skip(),
|
| 4109 |
export_csv_btn: gr.skip()
|
|
|
|
| 11 |
|
| 12 |
import gradio as gr
|
| 13 |
import asyncio
|
| 14 |
+
import os
|
| 15 |
import re
|
| 16 |
import logging
|
| 17 |
import warnings
|
|
|
|
| 980 |
"input",
|
| 981 |
f"❌ Error: {str(e)}",
|
| 982 |
"",
|
| 983 |
+
None, None, None, None, None,
|
| 984 |
+
gr.update(visible=False) # analysis_audio_btn (hide on error)
|
| 985 |
)
|
| 986 |
|
| 987 |
|
|
|
|
| 2079 |
size="sm",
|
| 2080 |
visible=False
|
| 2081 |
)
|
| 2082 |
+
with gr.Row():
|
| 2083 |
+
build_audio_player = gr.Audio(
|
| 2084 |
+
label="Portfolio Summary Audio",
|
| 2085 |
+
interactive=False,
|
| 2086 |
+
visible=False,
|
| 2087 |
+
show_download_button=True
|
| 2088 |
+
)
|
| 2089 |
|
| 2090 |
with gr.Row():
|
| 2091 |
build_regenerate_btn = gr.Button("Regenerate", variant="secondary", size="sm")
|
|
|
|
| 2158 |
size="sm",
|
| 2159 |
visible=False
|
| 2160 |
)
|
| 2161 |
+
with gr.Row():
|
| 2162 |
+
compare_audio_player = gr.Audio(
|
| 2163 |
+
label="Advisory Council Debate Audio",
|
| 2164 |
+
interactive=False,
|
| 2165 |
+
visible=False,
|
| 2166 |
+
show_download_button=True
|
| 2167 |
+
)
|
| 2168 |
|
| 2169 |
# Debate transcript
|
| 2170 |
with gr.Accordion("View Full Debate", open=False):
|
|
|
|
| 2479 |
size="sm",
|
| 2480 |
visible=False
|
| 2481 |
)
|
| 2482 |
+
with gr.Row():
|
| 2483 |
+
analysis_audio_player = gr.Audio(
|
| 2484 |
+
label="Audio Summary",
|
| 2485 |
+
interactive=False,
|
| 2486 |
+
visible=False,
|
| 2487 |
+
show_download_button=True
|
| 2488 |
+
)
|
| 2489 |
|
| 2490 |
# Performance Metrics Accordion (progressive disclosure)
|
| 2491 |
with gr.Accordion("Performance Metrics & Reasoning", open=False):
|
|
|
|
| 2906 |
"""Generate multi-speaker debate audio on-demand."""
|
| 2907 |
global LAST_DEBATE_DATA
|
| 2908 |
|
| 2909 |
+
logger.info(f"generate_debate_audio called. LAST_DEBATE_DATA exists: {LAST_DEBATE_DATA is not None}")
|
| 2910 |
+
if LAST_DEBATE_DATA:
|
| 2911 |
+
logger.info(f"Debate data keys: {LAST_DEBATE_DATA.keys()}")
|
| 2912 |
+
logger.info(f"Bull case length: {len(LAST_DEBATE_DATA.get('bull_case', ''))}")
|
| 2913 |
+
logger.info(f"Bear case length: {len(LAST_DEBATE_DATA.get('bear_case', ''))}")
|
| 2914 |
+
logger.info(f"Consensus length: {len(LAST_DEBATE_DATA.get('consensus', ''))}")
|
| 2915 |
+
|
| 2916 |
if not LAST_DEBATE_DATA:
|
| 2917 |
logger.warning("No debate data available for audio generation")
|
| 2918 |
return (
|
|
|
|
| 2959 |
)
|
| 2960 |
|
| 2961 |
logger.info(f"Debate audio generated: {audio_path}")
|
| 2962 |
+
logger.info(f"Audio file exists: {os.path.exists(audio_path) if audio_path else False}")
|
| 2963 |
+
logger.info(f"Returning audio player update with visible=True")
|
| 2964 |
|
| 2965 |
return (
|
| 2966 |
gr.update(value=audio_path, visible=True),
|
|
|
|
| 2996 |
yield (
|
| 2997 |
gr.update(value=[], visible=False), # build_agent_chat (empty, hidden)
|
| 2998 |
gr.update(visible=True), # build_results_container
|
| 2999 |
+
"Please select at least one investment goal.", # build_status
|
| 3000 |
+
gr.update(visible=False) # build_audio_btn (hide on error)
|
| 3001 |
)
|
| 3002 |
return
|
| 3003 |
|
|
|
|
| 3044 |
yield (
|
| 3045 |
gr.update(value=chat_messages, visible=True), # build_agent_chat (visible, growing list)
|
| 3046 |
gr.update(visible=False), # build_results_container (hidden during streaming)
|
| 3047 |
+
"", # build_status
|
| 3048 |
+
gr.update(visible=False) # build_audio_btn (hide during streaming)
|
| 3049 |
)
|
| 3050 |
logger.debug(f"UI update yielded successfully")
|
| 3051 |
except Exception as e:
|
|
|
|
| 3068 |
final_message = chat_messages[-1]
|
| 3069 |
if isinstance(final_message, dict) and "metadata" in final_message:
|
| 3070 |
portfolio_data = final_message.get("metadata", {}).get("portfolio", {})
|
| 3071 |
+
# Handle both dict and list formats
|
| 3072 |
+
if isinstance(portfolio_data, list):
|
| 3073 |
+
holdings = portfolio_data
|
| 3074 |
+
elif isinstance(portfolio_data, dict):
|
| 3075 |
+
holdings = portfolio_data.get("holdings", [])
|
| 3076 |
+
else:
|
| 3077 |
+
holdings = []
|
| 3078 |
+
|
| 3079 |
LAST_BUILD_RESULT = {
|
| 3080 |
"summary": final_message.get("content", ""),
|
| 3081 |
+
"holdings": holdings,
|
| 3082 |
"reasoning": final_message.get("metadata", {}).get("reasoning_trace", [])
|
| 3083 |
}
|
| 3084 |
logger.info("Build result stored for audio generation")
|
|
|
|
| 3116 |
yield (
|
| 3117 |
gr.update(value=[], visible=False), # build_agent_chat (empty, hidden on error)
|
| 3118 |
gr.update(visible=True), # build_results_container
|
| 3119 |
+
f"Error building portfolio: {str(e)}", # build_status
|
| 3120 |
+
gr.update(visible=False) # build_audio_btn (hide on error)
|
| 3121 |
)
|
| 3122 |
|
| 3123 |
def handle_build_accept(portfolio_table):
|
|
|
|
| 3185 |
0, # compare_bear_confidence
|
| 3186 |
"", # compare_consensus
|
| 3187 |
"", # compare_stance
|
| 3188 |
+
[], # compare_debate_transcript
|
| 3189 |
+
gr.update(visible=False) # compare_audio_btn (hide on error)
|
| 3190 |
)
|
| 3191 |
return
|
| 3192 |
|
|
|
|
| 3198 |
gr.update(value=[], visible=False), # compare_debate_chat (empty, hidden)
|
| 3199 |
gr.update(visible=True), # compare_results_container
|
| 3200 |
"Could not parse portfolio. Please check format.", # compare_status
|
| 3201 |
+
"", 0, "", 0, "", "", [],
|
| 3202 |
+
gr.update(visible=False) # compare_audio_btn (hide on error)
|
| 3203 |
)
|
| 3204 |
return
|
| 3205 |
|
|
|
|
| 3232 |
0, # compare_bear_confidence
|
| 3233 |
"", # compare_consensus
|
| 3234 |
"", # compare_stance
|
| 3235 |
+
[], # compare_debate_transcript
|
| 3236 |
+
gr.update(visible=False) # compare_audio_btn (hide during streaming)
|
| 3237 |
)
|
| 3238 |
|
| 3239 |
# Extract data from final consensus message
|
|
|
|
| 3292 |
gr.update(value=[], visible=False), # compare_debate_chat (empty, hidden on error)
|
| 3293 |
gr.update(visible=True), # compare_results_container
|
| 3294 |
f"Error: {str(e)}", # compare_status
|
| 3295 |
+
"", 0, "", 0, "", "", [],
|
| 3296 |
+
gr.update(visible=False) # compare_audio_btn (hide on error)
|
| 3297 |
)
|
| 3298 |
|
| 3299 |
async def handle_test_changes(portfolio_text, changes_text, portfolio_value, session_state):
|
|
|
|
| 4097 |
}
|
| 4098 |
|
| 4099 |
# Run analysis with progress updates
|
| 4100 |
+
page, analysis, perf_metrics, alloc, risk, perf, corr, opt, audio_btn = await run_analysis_with_ui_update(
|
| 4101 |
session_state, portfolio_text, roast_mode, persona, progress
|
| 4102 |
)
|
| 4103 |
|
|
|
|
| 4116 |
performance_plot: perf,
|
| 4117 |
correlation_plot: corr,
|
| 4118 |
optimization_plot: opt,
|
| 4119 |
+
analysis_audio_btn: audio_btn,
|
| 4120 |
load_past_portfolio_dropdown: gr.update(choices=dropdown_choices) if dropdown_choices else gr.skip(),
|
| 4121 |
export_pdf_btn: LAST_EXPORT_PDF_PATH,
|
| 4122 |
export_csv_btn: LAST_EXPORT_CSV_PATH
|
|
|
|
| 4134 |
performance_plot: None,
|
| 4135 |
correlation_plot: None,
|
| 4136 |
optimization_plot: None,
|
| 4137 |
+
analysis_audio_btn: audio_btn,
|
| 4138 |
load_past_portfolio_dropdown: gr.skip(),
|
| 4139 |
export_pdf_btn: gr.skip(),
|
| 4140 |
export_csv_btn: gr.skip()
|
|
@@ -63,7 +63,7 @@ class TTSService:
|
|
| 63 |
logger.info(f"Generating audio: {len(text)} characters")
|
| 64 |
|
| 65 |
try:
|
| 66 |
-
audio_generator =
|
| 67 |
text=text,
|
| 68 |
voice_id=voice_id or self.default_voice_id,
|
| 69 |
model_id=model,
|
|
@@ -277,7 +277,7 @@ class DebateAudioGenerator:
|
|
| 277 |
Returns:
|
| 278 |
Audio data as bytes
|
| 279 |
"""
|
| 280 |
-
audio_generator =
|
| 281 |
text=text,
|
| 282 |
voice_id=voice_id,
|
| 283 |
model_id="eleven_multilingual_v2",
|
|
|
|
| 63 |
logger.info(f"Generating audio: {len(text)} characters")
|
| 64 |
|
| 65 |
try:
|
| 66 |
+
audio_generator = self.client.text_to_speech.convert(
|
| 67 |
text=text,
|
| 68 |
voice_id=voice_id or self.default_voice_id,
|
| 69 |
model_id=model,
|
|
|
|
| 277 |
Returns:
|
| 278 |
Audio data as bytes
|
| 279 |
"""
|
| 280 |
+
audio_generator = self.client.text_to_speech.convert(
|
| 281 |
text=text,
|
| 282 |
voice_id=voice_id,
|
| 283 |
model_id="eleven_multilingual_v2",
|