MatteoScript commited on
Commit
4a42996
·
verified ·
1 Parent(s): 6fd36c6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +367 -476
app.py CHANGED
@@ -1,17 +1,27 @@
1
  import streamlit as st
2
- from openai import OpenAI
3
- import openpyxl
4
- from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
5
  import os
6
- import io
7
- import traceback
8
- import sys
9
  from dotenv import load_dotenv
10
  import json
11
- load_dotenv()
 
 
 
 
 
 
 
12
 
13
- st.set_page_config(layout="wide", page_title="Generatore Excel AI")
14
- st.title("📊 Generatore Excel AI")
 
 
 
 
 
 
15
 
16
  if "logged" not in st.session_state:
17
  st.session_state.logged = False
@@ -29,478 +39,359 @@ if st.session_state.logged == False:
29
  st.session_state.logged = True
30
  login_placeholder.empty()
31
 
32
- API_KEY = os.getenv("API_HUGGINGFACE")
33
- BASE_URL = "https://matteoscript-ai.hf.space/v1/"
34
- MODEL_NAME = "gemini-2.5-flash-preview-05-20" # Sostituisci con il tuo modello preferito
35
-
36
- if not API_KEY:
37
- st.error("API Key Hugging Face non trovata. Assicurati che il file .env sia configurato correttamente con API_HUGGINGFACE.")
38
- st.stop()
39
-
40
- client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
41
-
42
- DEFAULT_EXCEL_FILENAME = "excel_ai.xlsx"
43
- MAX_CORRECTION_ATTEMPTS = 5
44
- PROMPT_FOR_SHEET_PLANNER = """
45
- Sei un assistente AI specializzato nella pianificazione di documenti Excel multi-foglio.
46
- Data una richiesta generale dell'utente e un numero di fogli desiderato, il tuo compito è proporre:
47
- 1. Un nome univoco e descrittivo per ciascun foglio (massimo 30 caratteri, evita caratteri speciali non ammessi nei nomi dei fogli Excel come / ? * [ ]).
48
- 2. Una breve descrizione (1-3 frasi) dello scopo, del contenuto principale di ciascun foglio e delle possibili interazioni/collegamenti con altri fogli. Questa descrizione verrà poi usata come base per generare il contenuto dettagliato del foglio.
49
- La tua risposta DEVE essere un array JSON di oggetti. Ogni oggetto rappresenta un foglio e DEVE avere i campi "sheet_name" (stringa) e "sheet_purpose" (stringa).
50
- Il numero di oggetti nell'array DEVE corrispondere esattamente al numero di fogli specificato dall'utente.
51
- Esempio di richiesta utente: "Report vendite trimestrale (3 fogli): 1. Input dati grezzi vendite. 2. Riepilogo vendite per prodotto, che legga da input. 3. Riepilogo vendite per regione, che legga anch'esso da input."
52
- Esempio di TUA risposta JSON:
53
- [
54
- {"sheet_name": "Input Vendite", "sheet_purpose": "Foglio per l'inserimento manuale o l'importazione dei dati grezzi di vendita, includendo data, ID prodotto, quantità, prezzo, regione."},
55
- {"sheet_name": "Riep Prodotti", "sheet_purpose": "Analizza i dati dal foglio 'Input Vendite' per mostrare un riepilogo delle vendite per prodotto. Include totali, medie e forse grafici. Utilizza formule per aggregare i dati da 'Input Vendite'."},
56
- {"sheet_name": "Riep Regioni", "sheet_purpose": "Analizza i dati dal foglio 'Input Vendite' per visualizzare le performance di vendita per regione. Potrebbe includere somme condizionali basate sui dati di 'Input Vendite'."}
57
- ]
58
- Non includere ```json o ``` all'inizio o alla fine della tua risposta. Fornisci solo l'array JSON.
59
- """
60
-
61
- BASE_PROMPT_NO_BACKTICKS = """
62
- Sei un esperto generatore di SCRIPT PYTHON per MODIFICARE un oggetto Workbook openpyxl esistente (fornito in una variabile chiamata wb). Il tuo compito è aggiungere e popolare UN SINGOLO FOGLIO in questo Workbook, usando la libreria openpyxl.
63
-
64
- IL TUO SCRIPT NON DEVE ASSOLUTAMENTE:
65
- - Creare un nuovo Workbook (NON usare openpyxl.Workbook()). L'oggetto Workbook ti viene fornito e si chiama wb.
66
- - Salvare il Workbook (NON usare wb.save()). Il salvataggio sarà gestito esternamente.
67
- - Includere istruzioni di import globali. Le funzioni e classi necessarie (wb, Font, PatternFill, Border, Side, Alignment) saranno già disponibili nello scope.
68
-
69
- IL TUO SCRIPT PYTHON DEVE:
70
- - Aspettarsi che un oggetto openpyxl.Workbook (wb) sia già definito.
71
- - Creare un nuovo foglio o accedere a uno esistente usando il TITOLO_FOGLIO_RICHIESTO:
72
- sheet_title = "TITOLO_FOGLIO_RICHIESTO" # Questo sarà il nome fornito
73
- if sheet_title in wb.sheetnames:
74
- sheet = wb[sheet_title]
75
- # Pulisci il foglio se deve essere sovrascritto (valori e stili)
76
- for row_idx in range(1, sheet.max_row + 1):
77
- for col_idx in range(1, sheet.max_column + 1):
78
- cell = sheet.cell(row=row_idx, column=col_idx)
79
- cell.value = None
80
- cell.style = openpyxl.styles.Style() # Reset stile base
81
- else:
82
- sheet = wb.create_sheet(title=sheet_title)
83
- - Popolare l'oggetto sheet con dati, header, formule e formattazione come da richiesta.
84
- - Usare stili Material Design e font moderni.
85
- - Includere FORMULE Excel, specialmente se la richiesta implica calcoli o aggregazioni.
86
-
87
- CONTESTO DEI FOGLI PRECEDENTI (SE FORNITO):
88
- Se nella richiesta utente qui sotto trovi una sezione "CONTESTO DEI FOGLI PRECEDENTI", essa conterrà gli script Python usati per generare i fogli precedenti.
89
- Usa questo per creare FORMULE EXCEL CHE COLLEGANO IL FOGLIO CORRENTE AI FOGLI PRECEDENTI.
90
- Esempio: sheet['B1'].value = "='DatiVendite'!E10"
91
- Analizza gli script precedenti per nomi dei fogli e struttura dati. I nomi dei fogli nelle formule DEVONO CORRISPONDERE ESATTAMENTE.
92
-
93
- REQUISITI CODICE GENERATO:
94
- - SENZA commenti python (#).
95
- - Massimo un ritorno a capo vuoto consecutivo.
96
- - Gestione errori interni al try/except (usare 'pass', non 'raise e').
97
- - NON impostare sola lettura.
98
- - NON usare Alignment se non strettamente necessario.
99
- - Genera dati di esempio realistici (5-10 righe) se non diversamente specificato.
100
- - Applica formattazione ad header e celle chiave.
101
- """
102
-
103
- PROMPT_FOR_SCRIPT_CORRECTION = """
104
- Sei un esperto programmatore Python specializzato nella libreria openpyxl e nel debugging di script che manipolano file Excel.
105
- Ti è stato fornito uno script Python che ha fallito durante l'esecuzione mentre tentava di aggiungere e popolare un foglio in un workbook openpyxl esistente ('wb').
106
- Ti sono stati forniti anche l'errore esatto e il traceback.
107
-
108
- IL TUO COMPITO È:
109
- 1. Analizzare attentamente lo script originale, l'errore e il traceback.
110
- 2. Identificare la causa dell'errore.
111
- 3. Correggere lo script Python. Lo script corretto deve ancora rispettare TUTTE le seguenti regole originali:
112
- - DEVE operare su un oggetto Workbook openpyxl esistente chiamato 'wb'.
113
- - NON DEVE creare un nuovo Workbook (NON usare openpyxl.Workbook()).
114
- - NON DEVE salvare il Workbook (NON usare wb.save()).
115
- - NON DEVE includere istruzioni di import globali (es. 'import openpyxl'). Le classi necessarie (wb, Font, PatternFill, Border, Side, Alignment) sono già nello scope.
116
- - DEVE creare/accedere al foglio specificato da TITOLO_FOGLIO_RICHIESTO. Il nome del foglio è critico, non alterarlo a meno che l'errore non sia specificamente legato ad esso e la correzione sia ovvia.
117
- - DEVE popolare il foglio come descritto in DESCRIZIONE_ORIGINALE_FOGLIO.
118
- - Se era presente un CONTESTO_FOGLI_PRECEDENTI, lo script corretto deve ancora poter utilizzare quel contesto per eventuali formule inter-foglio.
119
- - DEVE essere SENZA commenti python (#).
120
- - DEVE avere al massimo un ritorno a capo vuoto consecutivo.
121
- - DEVE gestire gli errori specifici del foglio internamente con un blocco try/except che termina con 'pass' (non 'raise e'). L'intero script dovrebbe essere avvolto in un try/except.
122
- - NON DEVE usare la classe Alignment esplicitamente a meno che non sia fondamentale e parte della descrizione originale.
123
- - DEVE mantenere l'obiettivo originale del foglio. Non rimuovere funzionalità a meno che non siano la causa diretta dell'errore e non ci sia una correzione ovvia.
124
- 4. Restituisci SOLO lo script Python CORRETTO E COMPLETO, senza alcuna spiegazione aggiuntiva, commenti o ```python.
125
-
126
- INFORMAZIONI FORNITE:
127
- - TITOLO_FOGLIO_RICHIESTO: "{sheet_name}"
128
- - DESCRIZIONE_ORIGINALE_FOGLIO: "{sheet_purpose}"
129
- - CONTESTO_FOGLI_PRECEDENTI (se applicabile):
130
- {previous_scripts_context}
131
- - SCRIPT_ORIGINALE_FALLITO:
132
- ---
133
- {original_script}
134
- ---
135
- - ERRORE_E_TRACEBACK:
136
- ---
137
- {error_traceback}
138
- ---
139
-
140
- Restituisci solo il codice Python corretto. Non aggiungere spiegazioni prima o dopo il codice. Assicurati che lo script sia completo e pronto per essere eseguito.
141
- """
142
-
143
- def initialize_session_state():
144
- defaults = {
145
- "initial_excel_request": "Business Plan di lancio prodotto per un'azienda Loyalty specializzata nella GDO",
146
- "num_sheets_requested": 1,
147
- "ai_sheet_plan": None,
148
- "workbook_object": None,
149
- "generated_scripts_for_sheets": [],
150
- "final_excel_file_path": None,
151
- "final_excel_filename": DEFAULT_EXCEL_FILENAME,
152
- "process_log": [],
153
- "error_message": "",
154
- "warning_message": "",
155
- "generation_started": False
156
- }
157
- for key, value in defaults.items():
158
- if key not in st.session_state:
159
- st.session_state[key] = value
160
-
161
- def log_message(message, level="info"):
162
- log_entry = f"[{level.upper()}] {message}\n"
163
- st.session_state.process_log.append(log_entry)
164
- if level == "error":
165
- st.session_state.error_message += message + "\n"
166
- elif level == "warning":
167
- st.session_state.warning_message += message + "\n"
168
-
169
- def call_openai_for_sheet_plan(overall_request, num_sheets):
170
- st.session_state.error_message = ""
171
- st.session_state.warning_message = ""
172
- try:
173
- user_content = f"Richiesta generale dell'utente: \"{overall_request}\"\nNumero di fogli desiderato: {num_sheets}"
174
- completion = client.chat.completions.create(
175
- model=MODEL_NAME,
176
- messages=[
177
- {"role": "system", "content": PROMPT_FOR_SHEET_PLANNER},
178
- {"role": "user", "content": user_content}
179
- ],
180
- temperature=0.3,
181
- response_format={"type": "json_object"}
182
- )
183
- response_content = completion.choices[0].message.content.strip()
184
- log_message(f"Risposta grezza AI (piano fogli): {response_content}")
185
- planned_sheets = json.loads(response_content)
186
- if isinstance(planned_sheets, list) and all(isinstance(item, dict) and "sheet_name" in item and "sheet_purpose" in item for item in planned_sheets):
187
- if len(planned_sheets) == num_sheets: return planned_sheets
188
- else:
189
- msg = f"AI ha proposto {len(planned_sheets)} fogli, richiesti {num_sheets}. Verifica."
190
- log_message(msg, level="warning"); st.session_state.warning_message = msg
191
- return planned_sheets
192
  else:
193
- msg = "Struttura JSON piano fogli AI non valida."
194
- log_message(msg, level="error"); st.session_state.error_message = msg
195
- return None
196
- except json.JSONDecodeError as e:
197
- msg = f"Errore decodifica JSON piano fogli: {e}. Risposta: {response_content}"
198
- log_message(msg, level="error"); st.session_state.error_message = msg
199
- return None
200
- except Exception as e:
201
- log_message(f"Errore OpenAI piano fogli: {e}", level="error"); st.session_state.error_message = f"Errore OpenAI pianificazione: {e}"
202
- return None
203
-
204
- def call_openai_for_sheet_script(sheet_purpose, sheet_name, prev_scripts_ctx):
205
- st.session_state.error_message = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  try:
207
- user_content = (
208
- f"TITOLO_FOGLIO_RICHIESTO: '{sheet_name}'.\n"
209
- f"Descrizione per '{sheet_name}':\n{sheet_purpose}\n\n"
210
- )
211
- if prev_scripts_ctx:
212
- user_content += f"CONTESTO_FOGLI_PRECEDENTI:\n{prev_scripts_ctx}\n"
213
-
214
- completion = client.chat.completions.create(
215
- model=MODEL_NAME,
216
- messages=[
217
- {"role": "system", "content": BASE_PROMPT_NO_BACKTICKS},
218
- {"role": "user", "content": user_content}
219
- ],
220
- temperature=0.1,
221
- )
222
- script = completion.choices[0].message.content.strip()
223
- if script.startswith("```python"): script = script[9:]
224
- if script.endswith("```"): script = script[:-3]
225
- return script.strip()
226
  except Exception as e:
227
- st.session_state.error_message = f"Errore OpenAI script foglio '{sheet_name}': {e}"
228
- log_message(st.session_state.error_message, level="error")
229
- return None
230
-
231
- def call_openai_for_script_correction(original_script, error_traceback, sheet_name, sheet_purpose, prev_scripts_ctx):
232
- st.session_state.error_message = "" # Resetta per la chiamata di correzione
233
- log_message(f"Tentativo di correzione AI per foglio '{sheet_name}'.")
234
- try:
235
- prompt = PROMPT_FOR_SCRIPT_CORRECTION.format(
236
- sheet_name=sheet_name,
237
- sheet_purpose=sheet_purpose,
238
- previous_scripts_context=prev_scripts_ctx if prev_scripts_ctx else "Nessuno.",
239
- original_script=original_script,
240
- error_traceback=error_traceback
241
- )
242
- completion = client.chat.completions.create(
243
- model=MODEL_NAME,
244
- messages=[{"role": "user", "content": prompt}], # Il system prompt è inglobato per semplicità qui
245
- temperature=0.2, # Leggermente più creativo per trovare soluzioni
246
- )
247
- corrected_script = completion.choices[0].message.content.strip()
248
- if corrected_script.startswith("```python"): corrected_script = corrected_script[9:]
249
- if corrected_script.endswith("```"): corrected_script = corrected_script[:-3]
250
- log_message(f"Script corretto proposto dall'AI per '{sheet_name}'.")
251
- return corrected_script.strip()
252
- except Exception as e:
253
- st.session_state.error_message = f"Errore OpenAI correzione script per '{sheet_name}': {e}"
254
- log_message(st.session_state.error_message, level="error")
255
- return None
256
-
257
- def execute_single_sheet_script(script_code, workbook_obj, sheet_name_being_processed):
258
- #st.session_state.error_message = "" # Non resettare qui, l'errore può venire da OpenAI
259
- current_error_message_for_exec = ""
260
- capture_out = io.StringIO()
261
- original_stdout = sys.stdout
262
- sys.stdout = capture_out
263
- execution_success = False
264
- script_globals = {
265
- 'wb': workbook_obj, 'openpyxl': openpyxl,
266
- 'Font': Font, 'PatternFill': PatternFill,
267
- 'Border': Border, 'Side': Side, 'Alignment': Alignment
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  }
269
- detailed_error_for_correction = ""
270
- try:
271
- exec(script_code, script_globals)
272
- execution_success = True
273
- except Exception as e:
274
- tb_str = traceback.format_exc()
275
- current_error_message_for_exec = f"Errore esecuzione script per foglio '{sheet_name_being_processed}':\n{type(e).__name__}: {e}\nTraceback:\n{tb_str}"
276
- detailed_error_for_correction = f"{type(e).__name__}: {e}\n{tb_str}"
277
- # Non loggare qui come errore fatale subito, potrebbe essere corretto
278
- finally:
279
- sys.stdout = original_stdout
280
- execution_log = capture_out.getvalue()
281
- capture_out.close()
282
-
283
- if not execution_success: # Solo se fallisce, aggiorna l'errore di session_state
284
- st.session_state.error_message += current_error_message_for_exec + "\n"
285
-
286
-
287
- return execution_success, execution_log, detailed_error_for_correction
288
-
289
-
290
- def render_sidebar():
291
- with st.sidebar:
292
- st.header("🤖 Input per l'AI")
293
- st.session_state.initial_excel_request = st.text_area(
294
- "📝 Descrivi l'Excel",
295
- value=st.session_state.initial_excel_request, height=200,
296
- help="Sii descrittivo. Specifica se i fogli devono leggere dati da altri."
297
- )
298
- st.session_state.num_sheets_requested = st.number_input(
299
- "🔢 Numero di fogli:", min_value=1, max_value=20,
300
- value=st.session_state.num_sheets_requested, step=1
301
- )
302
-
303
- def render_main_content():
304
- st.markdown("Definisci richiesta e numero Fogli, poi l'AI **pianificherà, genererà, collegherà** e tenterà di auto-correggere gli script dei fogli")
305
-
306
- if st.sidebar.button("💡 Pianifica Fogli con AI", type="primary", use_container_width=True):
307
- st.session_state.ai_sheet_plan = None
308
- st.session_state.generated_scripts_for_sheets = []
309
- st.session_state.process_log = []
310
- st.session_state.error_message = ""
311
- st.session_state.warning_message = ""
312
- st.session_state.final_excel_file_path = None
313
- st.session_state.generation_started = False
314
- log_message("Avvio pianificazione fogli...")
315
- with st.spinner("L'AI sta pianificando i fogli..."):
316
- plan = call_openai_for_sheet_plan(st.session_state.initial_excel_request, st.session_state.num_sheets_requested)
317
- if plan:
318
- st.session_state.ai_sheet_plan = plan
319
- log_message(f"Piano fogli ricevuto: {len(plan)} fogli.")
320
- st.success(f"✅ L'AI ha pianificato {len(plan)} fogli! Rivedi/modifica descrizioni.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  else:
322
- log_message("Fallimento pianificazione.", level="error")
323
- st.error(f"Impossibile pianificare. {st.session_state.error_message}")
324
- st.rerun()
325
-
326
- if st.session_state.ai_sheet_plan:
327
- st.markdown("---")
328
- st.subheader("📋 Piano Fogli Proposto dall'AI (Modificabile)")
329
- st.caption("Modifica descrizioni se necessario, specialmente per chiarire collegamenti.")
330
- for i, sheet_def in enumerate(st.session_state.ai_sheet_plan):
331
- with st.expander(f"Foglio {i+1}: **{sheet_def['sheet_name']}**", expanded=True):
332
- new_purpose = st.text_area(
333
- f"Scopo/Contenuto per '{sheet_def['sheet_name']}':",
334
- value=sheet_def['sheet_purpose'], key=f"sheet_purpose_edit_{i}", height=120
335
- )
336
- if new_purpose != sheet_def['sheet_purpose']:
337
- st.session_state.ai_sheet_plan[i]['sheet_purpose'] = new_purpose
338
-
339
- if st.button("✨ Genera, Assembla e Auto-Correggi Excel ✨", type="primary", use_container_width=True, disabled=st.session_state.generation_started):
340
- st.session_state.generation_started = True
341
- # Conserva log pianificazione, pulisce log di vecchie generazioni
342
- planning_logs = [log for log in st.session_state.process_log if "pianificazione" in log.lower() or "piano fogli ricevuto" in log.lower()]
343
- st.session_state.process_log = planning_logs
344
- st.session_state.error_message = ""
345
- st.session_state.warning_message = ""
346
- st.session_state.generated_scripts_for_sheets = [] # Visualizzazione finale
347
- st.session_state.final_excel_file_path = None
348
-
349
- st.session_state.workbook_object = openpyxl.Workbook()
350
- if st.session_state.workbook_object.sheetnames:
351
- st.session_state.workbook_object.remove(st.session_state.workbook_object.active)
352
- log_message("Workbook inizializzato per generazione.")
353
-
354
- generated_scripts_history_for_context = [] # Per passare contesto all'AI
355
- all_sheets_successful = True
356
- progress_bar = st.progress(0)
357
- num_total_sheets = len(st.session_state.ai_sheet_plan)
358
-
359
- for i, sheet_def in enumerate(st.session_state.ai_sheet_plan):
360
- sheet_name = sheet_def["sheet_name"]
361
- sheet_purpose = sheet_def["sheet_purpose"]
362
- progress_text = f"Foglio {i+1}/{num_total_sheets}: '{sheet_name}'"
363
- log_message(f"--- Inizio {progress_text} ---")
364
- progress_bar.progress((i + 1) / num_total_sheets, text=f"Generando: {progress_text}")
365
-
366
- previous_scripts_context_str = ""
367
- if generated_scripts_history_for_context:
368
- parts = ["'''\nScript fogli precedenti (riferimento/collegamenti):\n"]
369
- for idx, prev_info in enumerate(generated_scripts_history_for_context):
370
- parts.append(f"--- Script Foglio '{prev_info['name']}' (Indice {idx}) ---\n{prev_info['script']}\n")
371
- parts.append("'''\n")
372
- previous_scripts_context_str = "\n".join(parts)
373
-
374
- current_script_for_sheet = None
375
- correction_attempts = 0
376
- sheet_successfully_processed = False
377
-
378
- # Tentativo iniziale di generazione script
379
- spinner_msg = f"🤖 AI genera script per '{sheet_name}' (contesto: {len(generated_scripts_history_for_context)} fogli)..."
380
- with st.spinner(spinner_msg):
381
- current_script_for_sheet = call_openai_for_sheet_script(sheet_purpose, sheet_name, previous_scripts_context_str)
382
-
383
- if current_script_for_sheet:
384
- log_message(f"Script iniziale per '{sheet_name}' generato.")
385
-
386
- # Ciclo di esecuzione e correzione
387
- while correction_attempts <= MAX_CORRECTION_ATTEMPTS and not sheet_successfully_processed:
388
- exec_spinner_msg = f"⚙️ Esecuzione script per '{sheet_name}'"
389
- if correction_attempts > 0:
390
- exec_spinner_msg += f" (tentativo correzione {correction_attempts})"
391
-
392
- with st.spinner(exec_spinner_msg):
393
- success, exec_log, error_details_for_correction = execute_single_sheet_script(
394
- current_script_for_sheet, st.session_state.workbook_object, sheet_name
395
- )
396
- if exec_log: log_message(f"Log esecuzione per '{sheet_name}':\n{exec_log}")
397
-
398
- if success:
399
- log_message(f"Foglio '{sheet_name}' aggiunto/modificato con successo.")
400
- st.session_state.generated_scripts_for_sheets.append({"name": sheet_name, "script": current_script_for_sheet, "status": "Successo"})
401
- generated_scripts_history_for_context.append({"name": sheet_name, "script": current_script_for_sheet})
402
- sheet_successfully_processed = True
403
- else:
404
- log_message(f"⚠️ Fallimento esecuzione script per '{sheet_name}'. Errore: {error_details_for_correction}", level="warning")
405
- correction_attempts += 1
406
- if correction_attempts <= MAX_CORRECTION_ATTEMPTS:
407
- log_message(f"Tentativo {correction_attempts}/{MAX_CORRECTION_ATTEMPTS} di auto-correzione AI per '{sheet_name}'.")
408
- with st.spinner(f"🤖 AI tenta correzione per '{sheet_name}' (errore: {error_details_for_correction.splitlines()[0]}..."):
409
- corrected_script_candidate = call_openai_for_script_correction(
410
- current_script_for_sheet, error_details_for_correction,
411
- sheet_name, sheet_purpose, previous_scripts_context_str
412
- )
413
- if corrected_script_candidate:
414
- log_message(f"Nuovo script corretto proposto per '{sheet_name}'.")
415
- current_script_for_sheet = corrected_script_candidate
416
- # Loop rieseguirà execute_single_sheet_script
417
- else:
418
- log_message(f"AI non ha fornito script corretto per '{sheet_name}'. Tentativo {correction_attempts} fallito.", level="error")
419
- st.session_state.generated_scripts_for_sheets.append({"name": sheet_name, "script": current_script_for_sheet, "status": f"Fallito dopo correzione (AI non ha corretto)", "error": error_details_for_correction})
420
- break # Esce dal ciclo di correzione se AI non dà nulla
421
- else:
422
- log_message(f"Limite tentativi ({MAX_CORRECTION_ATTEMPTS}) di correzione raggiunto per '{sheet_name}'. Errore finale: {error_details_for_correction}", level="error")
423
- st.session_state.generated_scripts_for_sheets.append({"name": sheet_name, "script": current_script_for_sheet, "status": f"Fallito (max tentativi)", "error": error_details_for_correction})
424
- else: # Fallimento generazione script iniziale
425
- log_message(f"⚠️ Fallimento generazione script iniziale per '{sheet_name}'. {st.session_state.error_message}", level="error")
426
- st.session_state.generated_scripts_for_sheets.append({"name": sheet_name, "script": "# ERRORE GENERAZIONE SCRIPT", "status": "Fallimento Generazione", "error": st.session_state.error_message})
427
-
428
- if not sheet_successfully_processed:
429
- all_sheets_successful = False
430
- break # Interrompe generazione se un foglio fallisce definitivamente
431
- log_message(f"--- Fine Foglio {i+1}: '{sheet_name}' ---")
432
  progress_bar.empty()
433
 
434
- if all_sheets_successful and st.session_state.workbook_object:
435
- try:
436
- if os.path.exists(st.session_state.final_excel_filename): os.remove(st.session_state.final_excel_filename)
437
- st.session_state.workbook_object.save(st.session_state.final_excel_filename)
438
- st.session_state.final_excel_file_path = st.session_state.final_excel_filename
439
- log_message(f"Workbook '{st.session_state.final_excel_filename}' salvato.")
440
- st.success(f" Excel '{st.session_state.final_excel_filename}' generato!")
441
- except Exception as e:
442
- log_message(f"❌ Errore salvataggio Workbook: {e}", level="error"); st.session_state.error_message = f"Errore salvataggio: {e}"
443
- elif not all_sheets_successful:
444
- st.error(f"Processo interrotto. Excel non salvato. {st.session_state.error_message}")
445
- else:
446
- st.warning("Nessun foglio elaborato o problema sconosciuto.")
447
- st.session_state.generation_started = False
448
- st.rerun()
449
-
450
- if st.session_state.warning_message: st.warning(st.session_state.warning_message)
451
- # Mostra errore principale solo se non c'è un file di successo e non siamo in fase di generazione attiva
452
- if st.session_state.error_message and not st.session_state.final_excel_file_path and not st.session_state.generation_started:
453
- st.error(f"Si sono verificati errori durante l'ultimo processo: {st.session_state.error_message}")
454
-
455
- if st.session_state.generated_scripts_for_sheets:
456
- st.markdown("---")
457
- with st.expander("🔍 Vedi Script Generati per i Fogli (e Stato Esecuzione)"):
458
- for item in st.session_state.generated_scripts_for_sheets:
459
- # Modifica questa riga:
460
- sheet_name_display = item.get('name', 'Nome Foglio Mancante')
461
- status_display = item.get('status', 'Stato Sconosciuto')
462
- script_display = item.get('script', '# Script non disponibile')
463
- error_display = item.get('error', None)
464
-
465
- st.subheader(f"Script per '{sheet_name_display}' - Stato: {status_display}")
466
- st.code(script_display, language="python")
467
- if error_display: # Controlla se la chiave 'error' esiste e ha un valore
468
- st.error(f"Dettaglio Errore Finale per '{sheet_name_display}':\n{error_display}")
469
-
470
- if st.session_state.process_log:
471
- st.markdown("---")
472
- with st.expander("📝 Log del Processo Dettagliato", expanded=False):
473
- st.text_area("Log:", "".join(st.session_state.process_log), height=300, disabled=True)
474
-
475
- render_download_section()
476
- render_footer()
477
-
478
- def render_download_section():
479
- if st.session_state.final_excel_file_path and os.path.exists(st.session_state.final_excel_file_path):
480
- st.markdown("---")
481
- st.header("📥 Download File Excel Finale")
482
- try:
483
- with open(st.session_state.final_excel_file_path, "rb") as fp: excel_bytes = fp.read()
484
- st.download_button(
485
- label=f"💾 Scarica {st.session_state.final_excel_filename}", data=excel_bytes,
486
- file_name=st.session_state.final_excel_filename,
487
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
488
- use_container_width=True, type="primary"
489
- )
490
- except Exception as e:
491
- st.error(f"Impossibile leggere il file per il download: {e}")
492
- log_message(f"Errore lettura file per download: {e}", level="error")
493
- elif st.session_state.ai_sheet_plan and not st.session_state.generation_started:
494
- st.info("Una volta generato l'Excel, il download apparirà qui.")
495
-
496
- def render_footer():
497
- st.markdown("---")
498
- st.markdown("💡 **Nota:** L'AI tenta di collegare i fogli e auto-correggere errori. Verifica sempre i risultati e le formule.")
499
-
500
- def main():
501
- initialize_session_state()
502
- render_sidebar()
503
- render_main_content()
504
 
505
- if __name__ == "__main__":
506
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ import base64
 
 
3
  import os
4
+ from io import BytesIO
5
+ from pdf2image import convert_from_bytes
 
6
  from dotenv import load_dotenv
7
  import json
8
+ from pydantic import BaseModel, Field, RootModel
9
+ from typing import List
10
+ import shutil
11
+ from moviepy import ImageClip, AudioFileClip, concatenate_videoclips
12
+ from openai import OpenAI
13
+ import wave, numpy as np, os
14
+ from pydub import AudioSegment
15
+ import io
16
 
17
+ st.set_page_config(page_title="Slide to Video 🎞️")
18
+ st.title("Slide to Video 🎞️")
19
+
20
+ load_dotenv()
21
+ API_KEY = os.getenv("API_HUGGINGFACE")
22
+ BASE_URL = "https://matteoscript-ai.hf.space/v1/"
23
+ MODEL_NAME = "gemini-2.5-flash"
24
+ client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
25
 
26
  if "logged" not in st.session_state:
27
  st.session_state.logged = False
 
39
  st.session_state.logged = True
40
  login_placeholder.empty()
41
 
42
+ class DialogoPagina(BaseModel):
43
+ """Contiene il dialogo per una singola pagina."""
44
+ page: int = Field(..., description="Il numero della pagina a cui si riferisce il dialogo.")
45
+ speaker: str = Field(..., description="Battuta del dialogo, pronunciata dallo Speaker.")
46
+
47
+ class DialoghiTTS(BaseModel):
48
+ """L'oggetto JSON principale che contiene tutti i dialoghi generati."""
49
+ data: List[DialogoPagina] = Field(..., description="Una lista di oggetti, ciascuno contenente il dialogo per una pagina.")
50
+
51
+ class SpeechSegment(BaseModel):
52
+ speaker: str = Field(..., description="ID dello speaker (es. SPEAKER_00)")
53
+ start_seconds: float = Field(..., ge=0, description="Secondi di inizio (comprensivi di decimali)")
54
+ end_seconds: float = Field(..., ge=0, description="Secondi di fine (comprensivi di decimali)")
55
+
56
+ class Speech(RootModel[List[SpeechSegment]]):
57
+ """ Un modello radice che rappresenta direttamente una lista di SpeechSegment."""
58
+ pass
59
+
60
+ def pdf_to_images(pdf_bytes: bytes):
61
+ """Converte il PDF in miniature PIL (per anteprima)."""
62
+ return convert_from_bytes(pdf_bytes, dpi=200)
63
+
64
+ def encode_bytes(file_bytes):
65
+ """Codifica i byte di un file in una stringa base64."""
66
+ return base64.b64encode(file_bytes).decode("utf-8")
67
+
68
+ def genera_dialoghi_tts(prompt: str, lingua: str, pdf_bytes: bytes, num_pagine: int)-> dict:
69
+ """Invia uno o più PDF a OpenAI e restituisce un riassunto."""
70
+ prompt_text = f"Per ciascuna delle {num_pagine} pagine del documento, {prompt}. Genera il testo in questa LINGUA: {lingua}. Rispondi solo con JSON conforme con un Array di {num_pagine} oggetti!!!"
71
+ content = [{"type": "text", "text": prompt_text}]
72
+ content.append(
73
+ {
74
+ "type": "image_url",
75
+ "image_url": {"url": f"data:application/pdf;base64,{encode_bytes(pdf_bytes)}"},
76
+ }
77
+ )
78
+ completion = client.beta.chat.completions.parse(
79
+ model=MODEL_NAME,
80
+ messages=[{"role": "user", "content": content}],
81
+ response_format=DialoghiTTS
82
+ )
83
+ return completion.choices[0].message.parsed.model_dump()
84
+
85
+ def generate_text(system_prompt: str, user_request: str):
86
+ """Chiama un LLM per chiamata API """
87
+ response = client.chat.completions.create(
88
+ model=MODEL_NAME,
89
+ temperature=0.2,
90
+ messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": f"Ecco il testo: {user_request}"}],
91
+ )
92
+ return response.choices[0].message.content.strip()
93
+
94
+ # Diarizzazione Audio per Speaker
95
+ def diarize_by_llm(file_name_audio: str, temp_dir: str, slides_data: list, music: str):
96
+ """Suddivide l'audio sulla base dei secondi restituiti dall'LLM."""
97
+ original_voice = AudioSegment.from_file(file_name_audio)
98
+ silence_2s = AudioSegment.silent(duration=2000) # 2000 ms = 2 s
99
+ voice_with_silence = silence_2s + original_voice + silence_2s
100
+ print(f"Durata originale: {len(original_voice)/1000:.2f}s | con silenzi: {len(voice_with_silence)/1000:.2f}s")
101
+ buffer = io.BytesIO()
102
+ voice_with_silence.export(buffer, format="wav")
103
+ segments = trascrivi_audio_openai(buffer.getvalue(), slides_data)
104
+ voice_with_silence.export(file_name_audio, format="wav")
105
+ if music:
106
+ add_music(file_name_audio, f"{music}.mp3")
107
+ full_audio = AudioSegment.from_file(file_name_audio)
108
+ segs = segments.root
109
+ for idx in range(1, len(segs)):
110
+ if idx==1:
111
+ start_ms = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  else:
113
+ start_ms = int(segs[idx - 1].start_seconds * 1000)
114
+ end_ms = int(segs[idx].start_seconds * 1000)
115
+ clip = full_audio[start_ms:end_ms]
116
+ filename = os.path.join(temp_dir, f"slide_{idx}.wav")
117
+ clip.export(filename, format="wav")
118
+ print(f"Salvato: {filename} ({start_ms / 1000:.2f}s - {end_ms / 1000:.2f}s)")
119
+ start_ms = int(segs[-1].start_seconds * 1000)
120
+ final_clip = full_audio[start_ms:]
121
+ final_filename = os.path.join(temp_dir, f"slide_{len(segs)}.wav")
122
+ final_clip.export(final_filename, format="wav")
123
+ print(f"Salvato: {final_filename} ({start_ms / 1000:.2f}s - {len(full_audio) / 1000:.2f}s)")
124
+
125
+ # Trascrive audio con SECONDI tramite LLM
126
+ def trascrivi_audio_openai(audio: bytes, slides_data: dict) -> Speech:
127
+ """ Trascrive AUDIO con secondi per la diarizzazione """
128
+ audio_b64 = base64.b64encode(audio).decode()
129
+ resp = client.beta.chat.completions.parse(
130
+ model = "gemini-2.5-flash",
131
+ response_format=Speech,
132
+ messages=[{
133
+ "role": "user",
134
+ "content": [
135
+ { "type": "text", "text": f"Restituisci un array JSON con esattamente {len(slides_data)} oggetti aventi speaker, start_seconds (decimale), end_seconds (decimale), "
136
+ f" sulla base del testo delle slides così formattate: {slides_data}"},
137
+ { "type": "input_audio", "input_audio": { "data": audio_b64, "format": "wav"}}
138
+ ]
139
+ }]
140
+ )
141
+ return resp.choices[0].message.parsed
142
+
143
+ # Aggiungi musica di sottofondo
144
+ def add_music(speech_name, music_name):
145
+ """ Aggiunge musica di sottofondo alla presentazione """
146
+ voice = AudioSegment.from_wav(speech_name)
147
+ guitar = AudioSegment.from_file(music_name)
148
+ guitar = guitar - 15
149
+ if len(guitar) < len(voice):
150
+ loops = (len(voice) // len(guitar)) + 1
151
+ guitar = guitar * loops
152
+ guitar = guitar[:len(voice)]
153
+ final = voice.overlay(guitar)
154
+ final.export(speech_name, format="wav")
155
+ print("Creato audio con sottofondo")
156
+
157
+ # Modifica Dialoghi
158
+ def modifica_dialoghi_con_llm(richiesta_utente: str, dialoghi_attuali: dict) -> DialoghiTTS:
159
+ """ Usa un LLM per modificare i dialoghi esistenti sulla base di una richiesta utente. """
160
+ dialoghi_json_str = json.dumps(dialoghi_attuali, indent=2, ensure_ascii=False)
161
+ prompt_llm = f"""
162
+ Sei un assistente editoriale per presentazioni. Il tuo compito è modificare una serie di dialoghi per delle slide in base alla richiesta dell'utente.
163
+ **Richiesta dell'Utente:**
164
+ "{richiesta_utente}"
165
+ ----------
166
+ **Dialoghi Attuali in formato JSON:**
167
+ {dialoghi_json_str}
168
+ ----------
169
+ **Istruzioni Obbligatorie:**
170
+ 1. Leggi la richiesta dell'utente e modifica i dialoghi nel JSON come richiesto.
171
+ 2. RISPONDI ESCLUSIVAMENTE CON UN OGGETTO JSON VALIDO.
172
+ 3. L'oggetto JSON di risposta deve avere ESATTAMENTE le stesse chiavi (i numeri delle pagine) dell'oggetto JSON che ti ho fornito. Non aggiungere, rimuovere o modificare le chiavi.
173
+ 4. Mantieni la struttura del JSON originale con i testi modificati!.
174
+ """
175
+ completion = client.beta.chat.completions.parse(
176
+ model=MODEL_NAME,
177
+ messages=[{"role": "user", "content": prompt_llm},],
178
+ response_format=DialoghiTTS
179
+ )
180
  try:
181
+ return completion.choices[0].message.parsed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  except Exception as e:
183
+ st.error(f"Errore durante la modifica dei dialoghi con l'LLM: {e}")
184
+ return {}
185
+
186
+ # ────────────────────────────────────────────────────────────────
187
+ # Streamlit UI
188
+ # ────────────────────────────────────────────────────────────────
189
+ # Inizializzazione dello stato della sessione
190
+ if 'dialoghi' not in st.session_state:
191
+ st.session_state.dialoghi = None
192
+ if 'pages_imgs' not in st.session_state:
193
+ st.session_state.pages_imgs = None
194
+ if 'video_path' not in st.session_state:
195
+ st.session_state.video_path = None
196
+ if 'slides_da_eliminare' not in st.session_state:
197
+ st.session_state.slides_da_eliminare = []
198
+
199
+ with st.sidebar:
200
+ st.header("🔄 Caricamento")
201
+ uploaded_file = st.file_uploader("Seleziona un file PDF", type=["pdf"])
202
+ st.divider()
203
+ st.header("✍️ Testo")
204
+ prompt_dialoghi = st.text_area("Prompt di generazione", "Genera un breve dialogo (max 15 parole) adatto ad una presentazione aziendale molto professionale", height=100)
205
+ lingua = st.selectbox("Lingua", ["Italiano", "Inglese", "Spagnolo", "Polacco", "Tedesco", "Rumeno", "Bresciano"])
206
+ base_style_prompt = "Leggi in tono AZIENDALE e professionale per una presentazione molto elegante. Deve essere VELOCE!"
207
+ if st.button("Genera Testo Dialoghi", type='primary', use_container_width=True) and uploaded_file:
208
+ st.session_state.video_path = None # Resetta il video precedente
209
+ st.session_state.slides_da_eliminare = [] # Resetta le slide eliminate
210
+ pdf_bytes = uploaded_file.getvalue()
211
+ with st.spinner("Creazione anteprime pagine..."):
212
+ st.session_state.pages_imgs = pdf_to_images(pdf_bytes)
213
+ num_pages = len(st.session_state.pages_imgs)
214
+ with st.spinner("Generazione dialoghi con AI..."):
215
+ st.session_state.dialoghi = genera_dialoghi_tts(prompt_dialoghi, lingua, pdf_bytes, num_pages)
216
+ if lingua != "Italiano":
217
+ with st.spinner("Traduzione stile audio..."):
218
+ base_style_prompt = generate_text(f"Sei un TRADUTTORE dall'ITALIANO alla lingua {lingua}. Traduci la frase che ti viene assegnata:", base_style_prompt)
219
+ st.divider()
220
+ st.subheader("🎤 Voce")
221
+
222
+ voice_info = {
223
+ "Zephyr": ("Brillante", "female"),
224
+ "Puck": ("Ritmato", "male"),
225
+ "Charon": ("Informativa", "male"),
226
+ "Kore": ("Deciso", "female"),
227
+ "Fenrir": ("Eccitabile", "male"),
228
+ "Leda": ("Giovane", "female"),
229
+ "Orus": ("Aziendale", "male"),
230
+ "Aoede": ("Arioso", "female"),
231
+ "Callirrhoe": ("Rilassato", "female"),
232
+ "Autonoe": ("Brillante", "female"),
233
+ "Enceladus": ("Respiro", "male"),
234
+ "Iapetus": ("Sussurrato", "male"),
235
+ "Umbriel": ("Rilassato", "male"),
236
+ "Algieba": ("Morbido", "male"),
237
+ "Despina": ("Morbido", "female"),
238
+ "Erinome": ("Chiara", "female"),
239
+ "Algenib": ("Rauco", "male"),
240
+ "Rasalgethi": ("Informativa", "male"),
241
+ "Laomedeia": ("Allegro", "female"),
242
+ "Achernar": ("Soffice", "female"),
243
+ "Alnilam": ("Aziendale", "male"),
244
+ "Schedar": ("Neutro", "male"),
245
+ "Gacrux": ("Per adulti", "female"),
246
+ "Pulcherrima": ("Avanzato", "female"),
247
+ "Achird": ("Amichevole", "male"),
248
+ "Zubenelgenubi": ("Casual", "male"),
249
+ "Vindemiatrix": ("Delicato", "female"),
250
+ "Sadachbia": ("Vivace", "male"),
251
+ "Sadaltager": ("Competente", "male"),
252
+ "Sulafat": ("Caldo", "female"),
253
  }
254
+ def voice_label(name: str) -> str:
255
+ style, gender = voice_info[name]
256
+ symbol = "♀️" if gender == "female" else "♂️"
257
+ return f"{symbol} {name} - {style}"
258
+
259
+ voice_names = list(voice_info.keys())
260
+
261
+ speaker1_voice = st.selectbox(
262
+ "Prima Voce",
263
+ options=voice_names,
264
+ index=voice_names.index("Kore"),
265
+ format_func=voice_label
266
+ )
267
+
268
+ speaker2_voice = st.selectbox(
269
+ "Seconda Voce",
270
+ options=voice_names,
271
+ index=voice_names.index("Schedar"),
272
+ format_func=voice_label
273
+ )
274
+ style_prompt = st.text_area("Stile", base_style_prompt, height=100)
275
+ music = st.selectbox("Musica Sottofondo", ["Modern", "Guitar", "Uplifting", "Acoustic", ""])
276
+
277
+ if st.session_state.dialoghi and st.session_state.pages_imgs:
278
+ st.subheader("Dialoghi Generati per Slide")
279
+ st.divider()
280
+ def elimina_slide(index_da_eliminare):
281
+ if index_da_eliminare not in st.session_state.slides_da_eliminare:
282
+ st.session_state.slides_da_eliminare.append(index_da_eliminare)
283
+ for i, img in enumerate(st.session_state.pages_imgs, 1):
284
+ if i in st.session_state.slides_da_eliminare:
285
+ continue
286
+ with st.container(border=False):
287
+ col1, col2 = st.columns([0.8, 0.2])
288
+ with col1:
289
+ st.write(f"#### 📄 Slide {i}")
290
+ with col2:
291
+ st.button(f"🗑️ Elimina", key=f"delete_{i}", on_click=elimina_slide, args=(i,), use_container_width=True)
292
+
293
+ st.image(img, use_container_width=True)
294
+ if "data" in st.session_state.dialoghi:
295
+ dialogo_trovato = next((item.get("speaker", "") for item in st.session_state.dialoghi["data"] if item.get("page") == i), "")
296
+ st.text_area(f"Dialogo Pagina {i}", value=dialogo_trovato, height=100, key=f"dialogo_{i}", label_visibility="collapsed")
297
+ st.divider()
298
+
299
+ if st.sidebar.button("Genera Audio & Video", use_container_width=True, type="primary"):
300
+ with st.spinner("Generazione in corso"):
301
+ temp_dir = "temp_video_files"
302
+ if os.path.exists(temp_dir):
303
+ shutil.rmtree(temp_dir)
304
+ os.makedirs(temp_dir)
305
+ video_clips = []
306
+ pagine_valide = [(i, img) for i, img in enumerate(st.session_state.pages_imgs, 1)
307
+ if i not in st.session_state.slides_da_eliminare]
308
+ num_pagine_valide = len(pagine_valide)
309
+ content_con_speaker = ""
310
+ i = 0
311
+ slides_data = []
312
+ with st.spinner("Generazione audio"):
313
+ for idx, (page_num, img) in enumerate(pagine_valide):
314
+ i+=1
315
+ dialogo_corrente = st.session_state[f"dialogo_{page_num}"]
316
+ if i % 2 != 0:
317
+ content_con_speaker+= f"Speaker 1: {dialogo_corrente}\n\n"
318
+ else:
319
+ content_con_speaker+= f"Speaker 2: {dialogo_corrente}\n\n"
320
+ slide_info = {"numero_slide": i, "testo_slide": dialogo_corrente}
321
+ slides_data.append(slide_info)
322
+ file_name_audio = os.path.join(temp_dir, "slides.wav")
323
+ response = client.audio.speech.create(
324
+ model="gemini-2.5-flash-preview-tts",
325
+ input=f"{style_prompt}\n\n{content_con_speaker}",
326
+ voice=f"{speaker1_voice},{speaker2_voice}",
327
+ response_format="wav",
328
+ )
329
+ response.write_to_file(file_name_audio)
330
+ diarize_by_llm(file_name_audio, temp_dir, slides_data, music)
331
+ progress_bar = st.sidebar.progress(0, "Inizio generazione video...")
332
+ for idx, (page_num, img) in enumerate(pagine_valide):
333
+ progress_text = f"Elaborazione slide {idx + 1}/{num_pagine_valide} (Pagina originale: {page_num})..."
334
+ progress_bar.progress(idx / num_pagine_valide, text=progress_text)
335
+ file_name_audio = os.path.join(temp_dir, f"slide_{idx + 1}.wav")
336
+ if os.path.exists(file_name_audio):
337
+ audio_clip = AudioFileClip(file_name_audio)
338
+ clip = (
339
+ ImageClip(np.array(img))
340
+ .with_duration(audio_clip.duration)
341
+ .with_fps(1)
342
+ .with_audio(audio_clip)
343
+ )
344
+ video_clips.append(clip)
345
+ else:
346
+ st.warning(f"Audio per slide {page_num} non generato correttamente.")
347
+
348
+ progress_bar.progress(0.9, text="Assemblaggio video finale...")
349
+
350
+ if video_clips:
351
+ final_video = concatenate_videoclips(video_clips, method="chain")
352
+ final_video_path = "final_video.mp4"
353
+ final_video.write_videofile(final_video_path, codec="libx264", audio_codec="aac", fps=1, preset="ultrafast", threads=os.cpu_count(), ffmpeg_params=["-tune", "stillimage", "-movflags", "+faststart"])
354
+ st.session_state.video_path = final_video_path
355
  else:
356
+ st.warning("Nessuna slide valida da elaborare. Il video non è stato creato.")
357
+ #shutil.rmtree(temp_dir)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  progress_bar.empty()
359
 
360
+ if st.session_state.video_path:
361
+ st.success("🎉 Video generato con successo!")
362
+ st.video(st.session_state.video_path)
363
+
364
+ with open(st.session_state.video_path, "rb") as file:
365
+ st.download_button(
366
+ label="📥 SCARICA VIDEO",
367
+ data=file,
368
+ file_name="presentazione_video.mp4",
369
+ mime="video/mp4",
370
+ use_container_width=True,
371
+ type='primary'
372
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
 
374
+ # CHAT INPUT
375
+ if st.session_state.dialoghi:
376
+ prompt_modifica = st.chat_input("Come vuoi modificare i dialoghi? (es. 'Rendili più brevi e professionali')")
377
+ if prompt_modifica:
378
+ with st.spinner("L'AI sta modificando i dialoghi..."):
379
+ dialoghi_attuali = {}
380
+ pagine_visibili = [
381
+ i for i in range(1, len(st.session_state.pages_imgs) + 1)
382
+ if i not in st.session_state.slides_da_eliminare
383
+ ]
384
+ for page_num in pagine_visibili:
385
+ key = f"dialogo_{page_num}"
386
+ if key in st.session_state:
387
+ dialoghi_attuali[str(page_num)] = st.session_state[key]
388
+ if not dialoghi_attuali:
389
+ st.warning("Non ci sono dialoghi da modificare.")
390
+ else:
391
+ dialoghi_modificati_obj = modifica_dialoghi_con_llm(prompt_modifica, dialoghi_attuali)
392
+ if dialoghi_modificati_obj:
393
+ st.session_state.dialoghi = dialoghi_modificati_obj.model_dump()
394
+ st.success("Dialoghi aggiornati!")
395
+ st.rerun()
396
+ else:
397
+ st.error("❌ Modifica fallita: L'AI non ha restituito un output valido.")