File size: 10,250 Bytes
7bc796e
aa815df
 
 
7bc796e
 
aa815df
 
 
 
 
7bc796e
 
 
 
 
aa815df
 
7bc796e
aa815df
7bc796e
 
aa815df
 
7bc796e
aa815df
 
 
 
 
 
7bc796e
aa815df
 
 
 
 
 
 
7bc796e
aa815df
 
 
 
 
 
 
 
7d3a75b
aa815df
 
 
 
 
 
 
 
 
7bc796e
aa815df
 
 
 
 
 
 
 
0cc968c
aa815df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7bc796e
 
 
aa815df
 
7bc796e
aa815df
 
 
7bc796e
 
aa815df
 
7d3a75b
aa815df
 
7d3a75b
aa815df
 
7d3a75b
aa815df
 
7d3a75b
 
aa815df
 
 
 
 
7bc796e
aa815df
 
7bc796e
aa815df
 
 
7bc796e
aa815df
 
7d3a75b
aa815df
 
7d3a75b
aa815df
7d3a75b
7bc796e
aa815df
 
7bc796e
aa815df
 
 
7bc796e
aa815df
 
 
 
 
 
 
 
 
 
 
0cc968c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aa815df
 
 
 
 
 
0cc968c
aa815df
 
 
 
 
 
 
 
 
 
 
 
 
0cc968c
 
 
 
 
 
 
aa815df
 
7d3a75b
aa815df
 
7d3a75b
aa815df
 
 
 
 
7d3a75b
aa815df
 
0cc968c
 
7d3a75b
0cc968c
7d3a75b
7bc796e
0cc968c
aa815df
0cc968c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7bc796e
aa815df
 
 
 
 
 
 
 
7bc796e
aa815df
 
7d3a75b
aa815df
 
7d3a75b
aa815df
 
7d3a75b
aa815df
 
7d3a75b
7bc796e
aa815df
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
import os
import asyncio
import wave
import tempfile
import logging
import json
import time
from flask import Flask, render_template, request, jsonify, send_file, stream_with_context, Response
from google import genai
import aiohttp
from pydub import AudioSegment

# Configure logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

app = Flask(__name__)
app.secret_key = os.environ.get("SESSION_SECRET", "default-secret-key")

# Configure Gemini API
api_key = os.environ.get("GEMINI_API_KEY")
if not api_key:
    logger.warning("GEMINI_API_KEY not found in environment variables. Using default value for development.")
    api_key = "YOUR_API_KEY"  # This will be replaced with env var in production

# Define available voices
AVAILABLE_VOICES = [
    "Puck", "Charon", "Kore", "Fenrir", 
    "Aoede", "Leda", "Orus", "Zephyr"
]
language_code="fr-FR"

# Global variable to track generation progress
generation_progress = {
    "status": "idle",
    "current": 0,
    "total": 0,
    "message": ""
}

def update_progress(current, total, message):
    """Update the global progress tracker."""
    global generation_progress
    generation_progress = {
        "status": "in_progress" if current < total else "complete",
        "current": current,
        "total": total,
        "message": message
    }
def create_async_enumerate(async_iterator):
    """Create an async enumerate function since it's not built-in."""
    i = 0
    async def async_iter():
        nonlocal i
        async for item in async_iterator:
            yield i, item
            i += 1
    return async_iter()

async def generate_speech(text, selected_voice):
    """Generate speech from text using Gemini AI."""
    try:
        client = genai.Client(api_key=api_key)
        model = "gemini-2.0-flash-live-001"
        
        # Configure the voice settings
        speech_config = genai.types.SpeechConfig(
            language_code=language_code,
            voice_config=genai.types.VoiceConfig(
                prebuilt_voice_config=genai.types.PrebuiltVoiceConfig(
                    voice_name=selected_voice
                )
            )
        )
        
        config = genai.types.LiveConnectConfig(
            response_modalities=["AUDIO"],
            speech_config=speech_config
        )
        
        # Create a temporary file to store the audio
        with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
            temp_filename = tmp_file.name
            
            async with client.aio.live.connect(model=model, config=config) as session:
                # Open the WAV file for writing
                wf = wave.open(temp_filename, "wb")
                wf.setnchannels(1)
                wf.setsampwidth(2)
                wf.setframerate(24000)
                
                # Send the text to Gemini
                await session.send_client_content(
                    turns={"role": "user", "parts": [{"text": text}]}, 
                    turn_complete=True
                )
                
                # Receive the audio data and write it to the file
                async for idx, response in create_async_enumerate(session.receive()):
                    if response.data is not None:
                        wf.writeframes(response.data)
                
                wf.close()
            
            return temp_filename
    
    except Exception as e:
        logger.error(f"Error generating speech: {str(e)}")
        raise e

@app.route('/')
def index():
    """Render the main page."""
    return render_template('index.html', voices=AVAILABLE_VOICES)

@app.route('/generate', methods=['POST'])
async def generate():
    """Generate speech from text."""
    try:
        data = request.json
        text = data.get('text', '')
        voice = data.get('voice', 'Kore')  # Default voice
        
        if not text:
            return jsonify({"error": "Text is required"}), 400
        
        if voice not in AVAILABLE_VOICES:
            return jsonify({"error": "Invalid voice selection"}), 400
        
        # Generate the speech
        audio_file = await generate_speech(text, voice)
        
        return jsonify({
            "status": "success",
            "message": "Audio generated successfully",
            "audioUrl": f"/audio/{os.path.basename(audio_file)}"
        })
    
    except Exception as e:
        logger.error(f"Error in generate endpoint: {str(e)}")
        return jsonify({"error": str(e)}), 500

@app.route('/audio/<filename>')
def get_audio(filename):
    """Serve the generated audio file."""
    try:
        temp_dir = tempfile.gettempdir()
        file_path = os.path.join(temp_dir, filename)
        
        if not os.path.exists(file_path):
            return jsonify({"error": "Audio file not found"}), 404
        
        return send_file(file_path, mimetype="audio/wav", as_attachment=False)
    
    except Exception as e:
        logger.error(f"Error serving audio file: {str(e)}")
        return jsonify({"error": str(e)}), 500

@app.route('/generate-podcast', methods=['POST'])
async def generate_podcast_route():
    """Generate a podcast from a scenario."""
    try:
        scenario = request.json
        
        # Reset progress tracker
        global generation_progress
        generation_progress = {
            "status": "in_progress",
            "current": 0,
            "total": len(scenario.get('characters', [])),
            "message": "Démarrage de la génération..."
        }
        
        # Start the background task for podcast generation
        # We'll return immediately and let the client poll for progress
        asyncio.create_task(generate_podcast_background(scenario))
        
        return jsonify({
            "status": "started",
            "message": "Génération du podcast commencée. Suivez la progression sur l'interface."
        })
    
    except Exception as e:
        logger.error(f"Error in generate-podcast endpoint: {str(e)}")
        update_progress(0, 0, f"Erreur: {str(e)}")
        return jsonify({"error": str(e)}), 500

async def generate_podcast_background(scenario):
    """Generate a podcast in the background."""
    try:
        # Generate audio for each character
        characters = scenario.get('characters', [])
        total_characters = len(characters)
        update_progress(0, total_characters, f"Préparation du podcast avec {total_characters} personnages...")
        
        audio_segments = []
        podcast_filename = None
        
        for idx, character in enumerate(characters):
            character_name = character.get('name', 'Unknown')
            voice = character.get('voice', 'Kore')
            text = character.get('text', '')
            
            update_progress(idx, total_characters, f"Génération de l'audio pour {character_name} ({idx+1}/{total_characters})...")
            
            if voice not in AVAILABLE_VOICES:
                logger.warning(f"Voice {voice} not available. Using default voice Kore for {character_name}.")
                voice = 'Kore'
            
            # Generate speech for this character
            try:
                audio_file = await generate_speech(text, voice)
                audio_segments.append(audio_file)
            except Exception as e:
                logger.error(f"Error generating speech for {character_name}: {str(e)}")
                update_progress(0, 0, f"Erreur lors de la génération pour {character_name}: {str(e)}")
                return
        
        update_progress(total_characters, total_characters, "Assemblage des segments audio...")
        
        # Combine all audio segments into one file
        combined = AudioSegment.empty()
        
        for audio_file in audio_segments:
            segment = AudioSegment.from_wav(audio_file)
            combined += segment
            # Add a short silence between segments (500ms)
            combined += AudioSegment.silent(duration=500)
        
        # Export the combined audio
        with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as output_file:
            podcast_filename = output_file.name
            combined.export(podcast_filename, format="wav")
        
        update_progress(total_characters + 1, total_characters + 1, f"Podcast généré avec succès! audio:{os.path.basename(podcast_filename)}")
        
    except Exception as e:
        logger.error(f"Error in podcast background task: {str(e)}")
        update_progress(0, 0, f"Erreur: {str(e)}")

@app.route('/podcast-status')
def podcast_status():
    """Get the current status of the podcast generation."""
    global generation_progress
    
    # If status is complete and contains an audioUrl in the message, extract it
    if generation_progress["status"] == "complete" and "audio:" in generation_progress["message"]:
        message_parts = generation_progress["message"].split("audio:")
        if len(message_parts) > 1:
            audio_filename = message_parts[1].strip()
            return jsonify({
                "status": "complete",
                "message": message_parts[0].strip(),
                "audioUrl": f"/audio/{audio_filename}"
            })
    
    # Otherwise just return the current progress
    return jsonify(generation_progress)

@app.route('/generation-progress')
def get_generation_progress():
    """Get the current progress of podcast generation."""
    return jsonify(generation_progress)

@app.route('/download/<filename>')
def download_audio(filename):
    """Download the generated audio file."""
    try:
        temp_dir = tempfile.gettempdir()
        file_path = os.path.join(temp_dir, filename)
        
        if not os.path.exists(file_path):
            return jsonify({"error": "Audio file not found"}), 404
        
        # Check if this is a podcast or simple speech
        download_name = "gemini_podcast.wav"
        
        return send_file(file_path, mimetype="audio/wav", as_attachment=True, 
                         download_name=download_name)
    
    except Exception as e:
        logger.error(f"Error downloading audio file: {str(e)}")
        return jsonify({"error": str(e)}), 500