""" 🎨 Studio Artistico PRO - Streamlit Version Tab 1: Video Sketch Converter PRO (con opzione 9:16 Shorts) Tab 2: Matita & Carboncino (20 effetti foto) Tab 3: Video Canny Edge Detection Compatible with Python 3.13 """ import streamlit as st import cv2 import numpy as np import tempfile import time import os import io from PIL import Image from scipy import ndimage st.set_page_config( page_title="🎨 Studio Artistico PRO", page_icon="🎨", layout="wide" ) # ═══════════════════════════════════════════════════════════════ # VIDEO SKETCH CONVERTER # ═══════════════════════════════════════════════════════════════ class VideoSketchConverter: STILI = { "sketch": "✏️ Sketch Classico - Linee pulite e definite", "pencil": "πŸ“ Pencil Drawing - Effetto matita con texture", "ink": "πŸ–ŠοΈ Ink Drawing - Linee nere marcate", "charcoal": "🎨 Charcoal - Effetto carboncino morbido", "comic": "πŸ’₯ Comic Style - Stile fumetto" } @staticmethod def applica_effetto_sketch(frame, style, intensity, detail, contrast): try: if detail % 2 == 0: detail += 1 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) if style == "sketch": inverted = cv2.bitwise_not(gray) blurred = cv2.GaussianBlur(inverted, (detail, detail), 0) sketch = cv2.divide(gray, 255 - blurred, scale=256.0) sketch = cv2.convertScaleAbs(sketch, alpha=contrast, beta=0) _, sketch = cv2.threshold(sketch, int(intensity), 255, cv2.THRESH_BINARY) return sketch elif style == "pencil": sketch_gray, _ = cv2.pencilSketch(frame, sigma_s=60, sigma_r=0.07, shade_factor=0.05) sketch_gray = cv2.convertScaleAbs(sketch_gray, alpha=contrast, beta=0) _, sketch_gray = cv2.threshold(sketch_gray, int(intensity), 255, cv2.THRESH_BINARY) return sketch_gray elif style == "ink": bilateral = cv2.bilateralFilter(gray, 9, 75, 75) edges = cv2.Canny(bilateral, 50, 150) ink = cv2.bitwise_not(edges) kernel = np.ones((2, 2), np.uint8) ink = cv2.erode(ink, kernel, iterations=1) return ink elif style == "charcoal": inverted = cv2.bitwise_not(gray) blur_size = max(3, detail * 2 + 1) if blur_size % 2 == 0: blur_size += 1 blurred = cv2.GaussianBlur(inverted, (blur_size, blur_size), 0) charcoal = cv2.divide(gray, 255 - blurred, scale=256.0) charcoal = cv2.GaussianBlur(charcoal, (3, 3), 0) return charcoal elif style == "comic": bilateral = cv2.bilateralFilter(gray, 9, 75, 75) edges = cv2.adaptiveThreshold( bilateral, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 9, 2) return edges return gray except Exception as e: st.error(f"Errore: {e}") return gray # ═══════════════════════════════════════════════════════════════ # CONVERSIONE 9:16 β€” zoom + crop centrale # Input: frame orizzontale 16:9 (es. 1920x1080) # Output: frame verticale 9:16 (es. 1080x1920) # Come: scala l'altezza a out_h, poi ritaglia il centro # β†’ niente bande nere, immagine piΓΉ stretta e alta # ═══════════════════════════════════════════════════════════════ def converti_9_16(frame_bgr, out_w=1080, out_h=1920): src_h, src_w = frame_bgr.shape[:2] # Scala in modo che l'altezza riempia out_h esattamente scale = out_h / src_h new_w = int(src_w * scale) # per un 16:9 β†’ molto piΓΉ largo di out_w resized = cv2.resize(frame_bgr, (new_w, out_h), interpolation=cv2.INTER_LANCZOS4) # Ritaglia al centro orizzontalmente if new_w >= out_w: x = (new_w - out_w) // 2 return resized[:, x:x + out_w] else: # Caso raro (video giΓ  molto stretto): pad nero laterale pad = (out_w - new_w) // 2 return cv2.copyMakeBorder(resized, 0, 0, pad, out_w - new_w - pad, cv2.BORDER_CONSTANT, value=[0, 0, 0]) # ═══════════════════════════════════════════════════════════════ # MATITA & CARBONCINO β€” 20 effetti su foto # ═══════════════════════════════════════════════════════════════ def _gray(img): return cv2.cvtColor(np.array(img.convert("RGB")), cv2.COLOR_RGB2GRAY) def _to_cv(img): return cv2.cvtColor(np.array(img.convert("RGB")), cv2.COLOR_RGB2BGR) def matita_classica(img, v=21): v = max(3, int(v) | 1); g = _gray(img) b = cv2.GaussianBlur(255 - g, (v, v), 0) return Image.fromarray(cv2.divide(g, 255 - b, scale=256)) def matita_morbida(img, v=51): v = max(3, int(v) | 1); g = _gray(img) b = cv2.GaussianBlur(255 - g, (v, v), 0) return Image.fromarray(cv2.GaussianBlur(cv2.divide(g, 255 - b, scale=256), (3,3), 0)) def matita_dura(img, v=7): v = max(3, int(v) | 1); g = _gray(img) b = cv2.GaussianBlur(255 - g, (v, v), 0) s = cv2.divide(g, 255 - b, scale=256) _, s = cv2.threshold(s, 200, 255, cv2.THRESH_BINARY) return Image.fromarray(s) def linee_canny(img, v=150): g = _gray(img) return Image.fromarray(255 - cv2.Canny(cv2.GaussianBlur(g,(5,5),0), int(v)//3, int(v))) def linee_laplacian(img, v=3): g = _gray(img) return Image.fromarray(255 - np.uint8(np.clip(np.abs(cv2.Laplacian(g, cv2.CV_64F, ksize=3)), 0, 255))) def carboncino_leggero(img, v=21): v = max(3, int(v) | 1); g = _gray(img) b = cv2.GaussianBlur(255 - g, (v, v), 0) s = cv2.equalizeHist(cv2.divide(g, 255 - b, scale=256)) return Image.fromarray(np.clip(s.astype(np.int32) - 30, 0, 255).astype(np.uint8)) def carboncino_intenso(img, v=15): v = max(3, int(v) | 1); g = _gray(img) b = cv2.GaussianBlur(255 - g, (v, v), 0) s = np.clip(cv2.divide(g, 255 - b, scale=256).astype(np.int32) - 60, 0, 255).astype(np.uint8) return Image.fromarray(cv2.createCLAHE(3.0, (8,8)).apply(s)) def carboncino_sfumato(img, v=31): v = max(3, int(v) | 1); g = _gray(img) b = cv2.GaussianBlur(255 - g, (v, v), 0) s = cv2.GaussianBlur(cv2.divide(g, 255 - b, scale=256), (7,7), 0) return Image.fromarray(np.clip(s.astype(np.int32) - 20, 0, 255).astype(np.uint8)) def effetto_grafite(img, v=25): v = max(3, int(v) | 1); g = _gray(img) b = cv2.GaussianBlur(255 - g, (v, v), 0) s = cv2.divide(g, 255 - b, scale=256) return Image.fromarray(np.clip(s.astype(np.int16) + np.random.normal(0,5,s.shape).astype(np.int16), 0, 255).astype(np.uint8)) def cross_hatching(img, v=21): v = max(3, int(v) | 1); g = _gray(img) b1 = cv2.GaussianBlur(255 - g, (v, v), 0); s1 = cv2.divide(g, 255 - b1, scale=256) rot = ndimage.rotate(g, 45, reshape=False, mode='nearest') b2 = cv2.GaussianBlur(255 - rot, (v, v), 0) s2 = ndimage.rotate(cv2.divide(rot, 255 - b2, scale=256), -45, reshape=False, mode='nearest').astype(np.uint8) return Image.fromarray((s1.astype(np.float32)/255 * s2.astype(np.float32)/255 * 255).astype(np.uint8)) def schizzo_rapido(img, v=8): g = _gray(img) return Image.fromarray(255 - np.clip(cv2.filter2D(g, -1, np.array([[-1,-1,-1],[-1,8,-1],[-1,-1,-1]])), 0, 255).astype(np.uint8)) def matita_artistica(img, v=21): v = max(3, int(v) | 1) g = cv2.cvtColor(cv2.bilateralFilter(_to_cv(img), 9, 75, 75), cv2.COLOR_BGR2GRAY) b = cv2.GaussianBlur(255 - g, (v, v), 0) return Image.fromarray(cv2.divide(g, 255 - b, scale=256)) def matita_carta(img, v=21): v = max(3, int(v) | 1); g = _gray(img) b = cv2.GaussianBlur(255 - g, (v, v), 0); s = cv2.divide(g, 255 - b, scale=256) h, w = s.shape; noise = np.random.randint(200, 256, (h, w), dtype=np.uint8) return Image.fromarray((s.astype(np.float32)/255 * noise.astype(np.float32)/255 * 255).astype(np.uint8)) def matita_colorata(img, v=21): v = max(3, int(v) | 1); cv_img = _to_cv(img); sketches = [] for ch in cv2.split(cv_img): b = cv2.GaussianBlur(255 - ch, (v, v), 0); sketches.append(cv2.divide(ch, 255 - b, scale=256)) return Image.fromarray(cv2.cvtColor(cv2.merge(sketches), cv2.COLOR_BGR2RGB)) def effetto_inchiostro(img, v=127): g = _gray(img); blur = cv2.GaussianBlur(g, (5,5), 0) _, binary = cv2.threshold(blur, int(v), 255, cv2.THRESH_BINARY) return Image.fromarray(cv2.morphologyEx(binary, cv2.MORPH_CLOSE, np.ones((2,2), np.uint8))) def matita_xdog(img, v=10): g = _gray(img).astype(np.float32) / 255.0 diff = cv2.GaussianBlur(g,(0,0),0.4) - 0.99 * cv2.GaussianBlur(g,(0,0),80.0) xdog = np.where(diff > -0.1, 1.0, 1.0 + np.tanh(float(v) * diff)) return Image.fromarray((np.clip(xdog, 0, 1) * 255).astype(np.uint8)) def carboncino_vignetta(img, v=21): v = max(3, int(v) | 1); g = _gray(img) b = cv2.GaussianBlur(255 - g, (v, v), 0) s = np.clip(cv2.divide(g, 255 - b, scale=256).astype(np.int32) - 40, 0, 255).astype(np.uint8) h, w = s.shape; Y, X = np.ogrid[:h, :w]; cx, cy = w/2, h/2 mask = 1 - np.clip(((X-cx)**2/cx**2 + (Y-cy)**2/cy**2), 0, 1) * 0.5 return Image.fromarray((s * mask).astype(np.uint8)) def schizzo_sobel(img, v=1): g = _gray(img) sx = cv2.Sobel(g, cv2.CV_64F, 1, 0, ksize=3); sy = cv2.Sobel(g, cv2.CV_64F, 0, 1, ksize=3) return Image.fromarray(255 - np.clip(np.sqrt(sx**2+sy**2), 0, 255).astype(np.uint8)) def matita_stilizzata(img, v=5): shade = max(1, min(20, int(v))) / 100 gray_sketch, _ = cv2.pencilSketch(_to_cv(img), sigma_s=60, sigma_r=0.07, shade_factor=shade) return Image.fromarray(gray_sketch) def carboncino_drammatico(img, v=15): v = max(3, int(v) | 1) g = cv2.createCLAHE(4.0, (8,8)).apply(_gray(img)) b = cv2.GaussianBlur(255 - g, (v, v), 0) s = np.clip(cv2.divide(g, 255 - b, scale=256).astype(np.int32) - 80, 0, 255).astype(np.uint8) _, s = cv2.threshold(s, 10, 255, cv2.THRESH_TOZERO) return Image.fromarray(s) EFFETTI = { "1. Matita Classica": (matita_classica, 3, 101, 21), "2. Matita Morbida": (matita_morbida, 3, 101, 51), "3. Matita Dura / Tecnica": (matita_dura, 3, 51, 7), "4. Linee Pulite (Canny)": (linee_canny, 10, 300,150), "5. Linee Fini (Laplacian)": (linee_laplacian, 1, 10, 3), "6. Carboncino Leggero": (carboncino_leggero, 3, 101, 21), "7. Carboncino Intenso": (carboncino_intenso, 3, 51, 15), "8. Carboncino Sfumato": (carboncino_sfumato, 3, 101, 31), "9. Effetto Grafite": (effetto_grafite, 3, 101, 25), "10. Cross-Hatching": (cross_hatching, 3, 51, 21), "11. Schizzo Rapido": (schizzo_rapido, 1, 20, 8), "12. Matita Artistica": (matita_artistica, 3, 101, 21), "13. Matita + Texture Carta": (matita_carta, 3, 101, 21), "14. Matita Colorata": (matita_colorata, 3, 101, 21), "15. Effetto Inchiostro": (effetto_inchiostro,50, 250,127), "16. Matita XDoG (Fumetto)": (matita_xdog, 1, 50, 10), "17. Carboncino + Vignetta": (carboncino_vignetta,3, 101, 21), "18. Schizzo Sobel": (schizzo_sobel, 1, 5, 1), "19. Matita Stilizzata (CV2)": (matita_stilizzata, 1, 20, 5), "20. Carboncino Drammatico": (carboncino_drammatico,3, 51, 15), } # ═══════════════════════════════════════════════════════════════ # VIDEO CANNY β€” funzioni core # ═══════════════════════════════════════════════════════════════ def canny_applica_frame(frame, low, high, blur_k): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) return cv2.Canny(cv2.GaussianBlur(gray, (blur_k, blur_k), 0), low, high) def canny_genera_anteprima(video_path, low, high, blur_k, posizione=0.3): cap = cv2.VideoCapture(video_path) total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) cap.set(cv2.CAP_PROP_POS_FRAMES, int(total * posizione)) ret, frame = cap.read() cap.release() if not ret: return None return Image.fromarray(canny_applica_frame(frame, low, high, blur_k)) def canny_processa_video(video_path, low, high, blur_k, converti_916, progress_bar, status_text): cap = cv2.VideoCapture(video_path) fps = cap.get(cv2.CAP_PROP_FPS) or 25.0 total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) orig_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) orig_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) if converti_916: out_w, out_h, suffix = 1080, 1920, "canny_916" else: out_w, out_h, suffix = orig_w, orig_h, "canny" with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp_out: tmp_out_path = tmp_out.name out = cv2.VideoWriter(tmp_out_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (out_w, out_h), isColor=False) frame_count = 0 status_text.text("⏳ Elaborazione in corso…") while True: ret, frame = cap.read() if not ret: break if converti_916: frame = converti_9_16(frame, out_w, out_h) out.write(canny_applica_frame(frame, low, high, blur_k)) frame_count += 1 if total_frames > 0: progress_bar.progress(min(frame_count / total_frames, 1.0)) cap.release(); out.release() with open(tmp_out_path, "rb") as f: video_bytes = f.read() os.unlink(tmp_out_path) status_text.text("βœ… Completato!") return video_bytes, f"{suffix}_output.mp4" # ═══════════════════════════════════════════════════════════════ # MAIN β€” tre tabs # ═══════════════════════════════════════════════════════════════ def main(): st.markdown("# 🎨 Studio Artistico PRO") tab1, tab2, tab3 = st.tabs([ "🎬 Video Sketch Converter", "✏️ Matita & Carboncino (Foto)", "⚑ Video Canny Edge" ]) # ── TAB 1: Video Sketch Converter ─────────────────────────────────── with tab1: st.markdown("### Trasforma i tuoi video in opere d'arte") col1, col2 = st.columns(2) with col1: st.subheader("🎬 Input") uploaded_file = st.file_uploader("Carica Video", type=['mp4', 'avi', 'mov', 'mkv']) style = st.selectbox( "🎨 Stile Artistico", options=list(VideoSketchConverter.STILI.keys()), format_func=lambda x: VideoSketchConverter.STILI[x] ) # ── FORMATO OUTPUT ────────────────────────────────────────── st.markdown("#### πŸ“ Formato output") formato = st.radio( "Scegli il formato", ["originale", "9:16"], format_func=lambda x: { "originale": "πŸ”² Originale (mantieni dimensioni)", "9:16": "πŸ“± 9:16 verticale β€” Shorts / Reels / TikTok", }[x], horizontal=False, key="fmt_sketch" ) if formato == "9:16": st.info("βœ‚οΈ Zoom + crop centrale: l'altezza riempie lo schermo, i lati vengono tagliati. Niente bande nere.") res_916 = st.radio("Risoluzione verticale", ["1080x1920", "720x1280"], horizontal=True, key="res_sketch_916") out_w_sk, out_h_sk = map(int, res_916.split("x")) else: out_w_sk, out_h_sk = None, None with st.expander("βš™οΈ Parametri Avanzati"): intensity = st.slider("IntensitΓ ", 50, 255, 150) detail = st.slider("Dettaglio", 1, 15, 5, step=2) contrast = st.slider("Contrasto", 1.0, 3.0, 1.5, step=0.1) process_btn = st.button("πŸš€ CONVERTI VIDEO", type="primary", use_container_width=True, key="btn_video") with col2: st.subheader("πŸŽ₯ Output") st.empty() if uploaded_file and process_btn: with st.spinner("Elaborazione in corso..."): with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_input: tmp_input.write(uploaded_file.read()) input_path = tmp_input.name cap = cv2.VideoCapture(input_path) orig_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) orig_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) fps = cap.get(cv2.CAP_PROP_FPS) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) width_out = out_w_sk if formato == "9:16" else orig_width height_out = out_h_sk if formato == "9:16" else orig_height output_path = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4').name out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width_out, height_out), False) progress_bar = st.progress(0) status_text = st.empty() frame_count = 0 start_time = time.time() while True: ret, frame = cap.read() if not ret: break # Conversione 9:16 prima dello sketch if formato == "9:16": frame = converti_9_16(frame, width_out, height_out) sketch = VideoSketchConverter.applica_effetto_sketch( frame, style, intensity, detail, contrast) out.write(sketch) frame_count += 1 if frame_count % 10 == 0: progress = frame_count / total_frames progress_bar.progress(progress) elapsed = time.time() - start_time fps_proc = frame_count / elapsed if elapsed > 0 else 0 eta = (total_frames - frame_count) / fps_proc if fps_proc > 0 else 0 status_text.text( f"Frame {frame_count}/{total_frames} | " f"{fps_proc:.1f} fps | ETA: {int(eta)}s") cap.release() out.release() elapsed_total = time.time() - start_time progress_bar.progress(1.0) st.success(f"βœ… Completato in {elapsed_total:.1f}s! ({width_out}Γ—{height_out}px)") suffix_nome = "_shorts" if formato == "9:16" else "" with open(output_path, 'rb') as f: st.download_button( label="πŸ“₯ SCARICA VIDEO", data=f.read(), file_name=f"sketch_{style}{suffix_nome}_{uploaded_file.name}", mime="video/mp4", type="primary", use_container_width=True ) try: os.unlink(input_path) os.unlink(output_path) except Exception: pass # ── TAB 2: Matita & Carboncino (foto) ─────────────────────────────── with tab2: st.markdown("### Trasforma le tue foto in disegni a matita o carboncino") col_a, col_b = st.columns(2) with col_a: st.subheader("πŸ“· Input") foto = st.file_uploader("Carica una foto", type=['jpg', 'jpeg', 'png', 'bmp', 'webp'], key="foto_uploader") nome_eff = st.selectbox("🎨 Effetto", options=list(EFFETTI.keys())) fn, mn, mx, default = EFFETTI[nome_eff] intensita = st.slider("πŸ”§ IntensitΓ  / Parametro", min_value=mn, max_value=mx, value=default) col_btn1, col_btn2 = st.columns(2) with col_btn1: btn_applica = st.button("✨ Applica", type="primary", use_container_width=True, key="btn_foto") with col_btn2: btn_batch = st.button("πŸ” Tutti e 20", use_container_width=True, key="btn_batch") with col_b: st.subheader("🎨 Risultato") risultato_placeholder = st.empty() if foto and btn_applica: pil_img = Image.open(foto).convert("RGB") with st.spinner(f"Applicando {nome_eff}…"): risultato = fn(pil_img, v=intensita) risultato_placeholder.image(risultato, use_container_width=True) buf = io.BytesIO() risultato.save(buf, format="PNG") st.download_button("πŸ’Ύ Scarica", data=buf.getvalue(), file_name=f"matita_{nome_eff[:20].strip()}.png", mime="image/png") if foto and btn_batch: pil_img = Image.open(foto).convert("RGB") st.markdown("#### πŸ–ΌοΈ Galleria β€” Tutti e 20 gli effetti") cols = st.columns(4) with st.spinner("Elaborazione tutti gli effetti…"): for i, (nome, (f, mn, mx, default)) in enumerate(EFFETTI.items()): try: res = f(pil_img, v=default) with cols[i % 4]: st.image(res, caption=nome, use_container_width=True) except Exception as e: st.warning(f"{nome}: {e}") # ── TAB 3: Video Canny Edge Detection ─────────────────────────────── with tab3: st.markdown("### ⚑ Trasforma il tuo video in Canny Edge Detection") uploaded_canny = st.file_uploader( "πŸ“‚ Carica il tuo video", type=["mp4", "avi", "mov", "mkv", "flv", "wmv", "webm"], key="canny_uploader" ) if not uploaded_canny: st.info("πŸ‘† Carica un video per iniziare.") else: with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp_c: tmp_c.write(uploaded_canny.read()) tmp_canny_path = tmp_c.name cap_info = cv2.VideoCapture(tmp_canny_path) total_f = int(cap_info.get(cv2.CAP_PROP_FRAME_COUNT)) fps_i = cap_info.get(cv2.CAP_PROP_FPS) or 25.0 w_i = int(cap_info.get(cv2.CAP_PROP_FRAME_WIDTH)) h_i = int(cap_info.get(cv2.CAP_PROP_FRAME_HEIGHT)) cap_info.release() ci1, ci2, ci3, ci4 = st.columns(4) ci1.metric("πŸ“ Larghezza", f"{w_i}px") ci2.metric("πŸ“ Altezza", f"{h_i}px") ci3.metric("🎞️ FPS", f"{fps_i:.1f}") ci4.metric("⏱️ Durata", f"{total_f / fps_i:.1f}s") st.divider() st.markdown("#### βš™οΈ Parametri Canny") cp1, cp2, cp3 = st.columns(3) with cp1: c_low = st.slider("Soglia Bassa", 0, 200, 50, key="c_low") with cp2: c_high = st.slider("Soglia Alta", 0, 300, 150, key="c_high") with cp3: c_blur = st.slider("Sfocatura", 1, 15, 5, step=2, key="c_blur") c_916 = st.checkbox("πŸ“± Converti in 9:16 verticale (1080Γ—1920)", value=False, key="c_916") blur_k = c_blur if c_blur % 2 == 1 else c_blur + 1 st.divider() st.markdown("#### πŸ‘οΈ Anteprima") pos_col, _ = st.columns([2, 3]) with pos_col: c_pos = st.slider("Posizione nel video", 0, 100, 30, format="%d%%", key="c_pos") preview = canny_genera_anteprima(tmp_canny_path, c_low, c_high, blur_k, c_pos/100.0) if preview: st.image(preview, caption=f"Anteprima β€” Low={c_low}, High={c_high}, Blur={blur_k}", use_container_width=True) else: st.warning("⚠️ Impossibile generare l'anteprima.") st.divider() st.markdown("#### πŸš€ Elabora il Video Completo") if st.button("▢️ AVVIA ELABORAZIONE", type="primary", use_container_width=True, key="btn_canny"): c_progress = st.progress(0.0) c_status = st.empty() try: video_bytes, nome_file = canny_processa_video( tmp_canny_path, c_low, c_high, blur_k, c_916, c_progress, c_status) st.success(f"βœ… Video elaborato! ({len(video_bytes)/1024/1024:.1f} MB)") st.download_button("⬇️ Scarica Video Canny", data=video_bytes, file_name=nome_file, mime="video/mp4", use_container_width=True, key="dl_canny") except Exception as e: st.error(f"❌ Errore: {e}") import traceback; st.code(traceback.format_exc()) try: os.unlink(tmp_canny_path) except Exception: pass if __name__ == "__main__": main()