|
3 | 3 | from utils import sanitize_text, get_asset_path, get_app_data_dir |
4 | 4 | from config import AVAILABLE_VOICES, DEFAULT_APP_SETTINGS, DEMO_AVAILABLE |
5 | 5 | from create_demo import create_html_demo_whisperx |
| 6 | +from transcript_analyzer import generate_analysis_docx, get_analysis_prompt_path |
6 | 7 | import os |
7 | 8 | import tempfile |
8 | 9 | import json |
@@ -59,6 +60,69 @@ def save_settings(settings): |
59 | 60 | with open(get_settings_path(), 'w') as f: |
60 | 61 | json.dump(settings, f, indent=4) |
61 | 62 |
|
| 63 | +def extract_filename_from_script(script_text, extension, max_length=50): |
| 64 | + """ |
| 65 | + Extracts a safe filename from the beginning of the first sentence in the script. |
| 66 | +
|
| 67 | + Args: |
| 68 | + script_text: The script content |
| 69 | + extension: File extension (e.g., 'mp3', 'docx') |
| 70 | + max_length: Maximum length for the filename (default 50) |
| 71 | +
|
| 72 | + Returns: |
| 73 | + A sanitized filename with the given extension |
| 74 | + """ |
| 75 | + # Remove speaker labels and get the first sentence |
| 76 | + lines = script_text.strip().split('\n') |
| 77 | + first_dialogue = "" |
| 78 | + |
| 79 | + for line in lines: |
| 80 | + # Skip empty lines |
| 81 | + if not line.strip(): |
| 82 | + continue |
| 83 | + # Check if line has speaker format (Speaker: text) |
| 84 | + match = re.match(r'^\s*([^:]+?)\s*:\s*(.+)$', line) |
| 85 | + if match: |
| 86 | + first_dialogue = match.group(2).strip() |
| 87 | + break |
| 88 | + else: |
| 89 | + # If no speaker format, use the line as-is |
| 90 | + first_dialogue = line.strip() |
| 91 | + break |
| 92 | + |
| 93 | + if not first_dialogue: |
| 94 | + # Fallback to UUID if no content found |
| 95 | + return f"podcast_{os.urandom(4).hex()}.{extension}" |
| 96 | + |
| 97 | + # Remove any bracketed annotations like [playful], [laughing], etc. |
| 98 | + first_dialogue = re.sub(r'\[.*?\]', '', first_dialogue).strip() |
| 99 | + |
| 100 | + # Extract the beginning (up to first sentence or max_length) |
| 101 | + # Split by sentence-ending punctuation |
| 102 | + sentence_match = re.match(r'^([^.!?]+)', first_dialogue) |
| 103 | + if sentence_match: |
| 104 | + first_sentence = sentence_match.group(1).strip() |
| 105 | + else: |
| 106 | + first_sentence = first_dialogue |
| 107 | + |
| 108 | + # Limit length |
| 109 | + if len(first_sentence) > max_length: |
| 110 | + first_sentence = first_sentence[:max_length].strip() |
| 111 | + |
| 112 | + # Remove or replace characters that are unsafe for filenames |
| 113 | + # Keep alphanumeric, spaces, hyphens, and underscores |
| 114 | + safe_name = re.sub(r'[^\w\s\-]', '', first_sentence) |
| 115 | + # Replace multiple spaces/hyphens with single underscore |
| 116 | + safe_name = re.sub(r'[\s\-]+', '_', safe_name) |
| 117 | + # Remove leading/trailing underscores |
| 118 | + safe_name = safe_name.strip('_') |
| 119 | + |
| 120 | + # If we ended up with an empty name, use fallback |
| 121 | + if not safe_name: |
| 122 | + return f"podcast_{os.urandom(4).hex()}.{extension}" |
| 123 | + |
| 124 | + return f"{safe_name}.{extension}" |
| 125 | + |
62 | 126 | # --- Routes --- |
63 | 127 | @app.route('/') |
64 | 128 | def index(): |
@@ -112,6 +176,7 @@ def get_settings(): |
112 | 176 | settings = load_settings() |
113 | 177 | settings['has_elevenlabs_key'] = bool(os.environ.get("ELEVENLABS_API_KEY")) |
114 | 178 | settings['has_gemini_key'] = bool(os.environ.get("GEMINI_API_KEY")) |
| 179 | + settings['has_analysis_prompt'] = bool(get_analysis_prompt_path()) |
115 | 180 | return jsonify(settings) |
116 | 181 |
|
117 | 182 | @app.route('/api/settings', methods=['POST']) |
@@ -254,7 +319,7 @@ def handle_generate(): |
254 | 319 | app_settings_clean = sanitize_app_settings_for_backend(app_settings) |
255 | 320 |
|
256 | 321 | task_id = str(uuid.uuid4()) |
257 | | - output_filename = f"{task_id}.mp3" |
| 322 | + output_filename = extract_filename_from_script(sanitized_script, 'mp3') |
258 | 323 | output_filepath = os.path.join(app.config['TEMP_DIR'], output_filename) |
259 | 324 |
|
260 | 325 | stop_event = threading.Event() |
@@ -380,6 +445,43 @@ def download_demo_zip(demo_id): |
380 | 445 | def get_temp_file(filename): |
381 | 446 | return send_from_directory(app.config['TEMP_DIR'], filename) |
382 | 447 |
|
| 448 | +@app.route('/api/generate_analysis', methods=['POST']) |
| 449 | +def handle_generate_analysis(): |
| 450 | + """Generates a DOCX analysis document from a transcript using Gemini API.""" |
| 451 | + data = request.json |
| 452 | + transcript = data.get('transcript', '') |
| 453 | + |
| 454 | + if not transcript: |
| 455 | + return jsonify({'error': 'Transcript is required.'}), 400 |
| 456 | + |
| 457 | + # Check if Gemini API key is available |
| 458 | + api_key = os.environ.get("GEMINI_API_KEY") |
| 459 | + if not api_key: |
| 460 | + return jsonify({'error': 'Gemini API key not configured.'}), 403 |
| 461 | + |
| 462 | + try: |
| 463 | + # Generate the analysis DOCX |
| 464 | + docx_filename = extract_filename_from_script(transcript, 'docx') |
| 465 | + docx_path = os.path.join(app.config['TEMP_DIR'], docx_filename) |
| 466 | + |
| 467 | + generate_analysis_docx( |
| 468 | + transcript=transcript, |
| 469 | + output_path=docx_path, |
| 470 | + api_key=api_key |
| 471 | + ) |
| 472 | + |
| 473 | + return jsonify({ |
| 474 | + 'download_url': f'/temp/{docx_filename}', |
| 475 | + 'filename': docx_filename |
| 476 | + }) |
| 477 | + |
| 478 | + except ValueError as e: |
| 479 | + logger.error(f"Validation error during analysis generation: {e}") |
| 480 | + return jsonify({'error': str(e)}), 400 |
| 481 | + except Exception as e: |
| 482 | + logger.error(f"Error during analysis generation: {e}", exc_info=True) |
| 483 | + return jsonify({'error': 'An unexpected error occurred during analysis generation.'}), 500 |
| 484 | + |
383 | 485 | if __name__ == '__main__': |
384 | 486 | from dotenv import load_dotenv |
385 | 487 | load_dotenv() |
|
0 commit comments