Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,17 +1,27 @@
|
|
1 |
import streamlit as st
|
2 |
-
|
3 |
-
import openpyxl
|
4 |
-
from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
|
5 |
import os
|
6 |
-
import
|
7 |
-
import
|
8 |
-
import sys
|
9 |
from dotenv import load_dotenv
|
10 |
import json
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
-
st.set_page_config(
|
14 |
-
st.title("
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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 |
-
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 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
206 |
try:
|
207 |
-
|
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.
|
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 |
-
st.session_state.
|
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 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
)
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
with st.spinner("
|
316 |
-
|
317 |
-
if
|
318 |
-
|
319 |
-
|
320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
321 |
else:
|
322 |
-
|
323 |
-
|
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 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
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 |
-
|
506 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.")
|