""" Generate an editable .pptx of the SADAR 5-min pitch deck. Output: docs/SADAR_pitch_5min.pptx Slides: 11 (matches docs/SADAR_pitch_5min.html numbering). Open in Google Slides via Drive -> right click -> Open with Google Slides. Design: same color tokens, fonts, layout principles as the HTML deck, adapted to the limits of python-pptx (native shapes + textboxes + images). """ from pathlib import Path from pptx import Presentation from pptx.dml.color import RGBColor from pptx.enum.shapes import MSO_SHAPE from pptx.enum.text import PP_ALIGN, MSO_ANCHOR from pptx.util import Inches, Pt, Emu REPO = Path(__file__).resolve().parent.parent OUT = REPO / "docs" / "SADAR_pitch_5min.pptx" ASSETS = REPO / "docs" / "assets" FRONTEND = REPO / "frontend" / "public" SLIDE_W = Inches(13.333) SLIDE_H = Inches(7.5) C_BG = RGBColor(0x05, 0x08, 0x0D) C_BG_DEEP = RGBColor(0x0A, 0x10, 0x14) C_PANEL = RGBColor(0x0E, 0x1A, 0x1F) C_EDGE = RGBColor(0x1A, 0x2F, 0x36) C_TEXT = RGBColor(0xC1, 0xDA, 0xCD) C_MUTED = RGBColor(0x5E, 0x7A, 0x78) C_LABEL = RGBColor(0x7A, 0x9E, 0x99) C_INFO = RGBColor(0x7F, 0xD1, 0xC6) C_WARN = RGBColor(0xE6, 0xA2, 0x3C) C_ALERT = RGBColor(0xE0, 0x80, 0x80) C_WHITE = RGBColor(0xFF, 0xFF, 0xFF) FONT_MONO = "Roboto Mono" FONT_FALLBACK = "Consolas" def in_(v): return Inches(v) def pt(v): return Pt(v) def make_prs(): prs = Presentation() prs.slide_width = SLIDE_W prs.slide_height = SLIDE_H return prs def blank_slide(prs, bg=C_BG): blank_layout = prs.slide_layouts[6] slide = prs.slides.add_slide(blank_layout) bg_rect = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_W, SLIDE_H) bg_rect.line.fill.background() bg_rect.fill.solid() bg_rect.fill.fore_color.rgb = bg return slide def add_rect(slide, x, y, w, h, fill, line=None): rect = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, w, h) rect.fill.solid() rect.fill.fore_color.rgb = fill if line is None: rect.line.fill.background() else: rect.line.color.rgb = line rect.line.width = Emu(6350) return rect def add_text( slide, x, y, w, h, text, size=18, color=C_TEXT, bold=False, italic=False, font=FONT_MONO, align=PP_ALIGN.LEFT, spacing=0.02, anchor=MSO_ANCHOR.TOP, uppercase=False, ): tb = slide.shapes.add_textbox(x, y, w, h) tf = tb.text_frame tf.word_wrap = True tf.margin_left = tf.margin_right = Emu(0) tf.margin_top = tf.margin_bottom = Emu(0) tf.vertical_anchor = anchor lines = text.split("\n") if isinstance(text, str) else text for i, line in enumerate(lines): p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() p.alignment = align run = p.add_run() run.text = line.upper() if uppercase else line run.font.name = font run.font.size = pt(size) run.font.bold = bold run.font.italic = italic run.font.color.rgb = color return tb def add_topbar(slide, ctx_text, status_left="SADAR", right_pairs=None, status_color=None): add_rect(slide, 0, 0, SLIDE_W, in_(0.42), C_BG_DEEP) add_rect(slide, 0, in_(0.42), SLIDE_W, Emu(12700), C_EDGE) left_y = in_(0.10) left_text = f"{status_left} // {ctx_text}" add_text( slide, in_(0.5), left_y, in_(8.0), in_(0.30), left_text, size=10, color=C_MUTED, uppercase=True, spacing=0.22, ) if right_pairs: right_str = " // ".join(right_pairs) tb = add_text( slide, in_(5.0), left_y, in_(7.8), in_(0.30), right_str, size=10, color=status_color or C_INFO, uppercase=True, spacing=0.22, align=PP_ALIGN.RIGHT, ) return tb def add_footer(slide, crumbs, controls=False): add_rect(slide, 0, SLIDE_H - in_(0.55), SLIDE_W, Emu(12700), C_EDGE) add_rect(slide, 0, SLIDE_H - in_(0.54), SLIDE_W, in_(0.54), C_BG_DEEP) logo_path = FRONTEND / "sadar-mark.png" if logo_path.exists(): try: slide.shapes.add_picture( str(logo_path), in_(0.4), SLIDE_H - in_(0.45), height=in_(0.35), ) except Exception: pass if controls: ctrl_x = in_(5.4) ctrl_y = SLIDE_H - in_(0.42) ctrl_w = in_(0.55) ctrl_h = in_(0.30) ctrls = [("PAUSE", False), ("1X", True), ("10X", False), ("30X", False), ("60X", False)] for i, (lbl, active) in enumerate(ctrls): rect = add_rect( slide, ctrl_x + i * (ctrl_w + in_(0.08)), ctrl_y, ctrl_w, ctrl_h, C_BG_DEEP, line=(C_INFO if active else C_EDGE), ) tb = slide.shapes.add_textbox(rect.left, rect.top, rect.width, rect.height) tf = tb.text_frame tf.margin_left = tf.margin_right = Emu(0) tf.margin_top = tf.margin_bottom = Emu(0) tf.vertical_anchor = MSO_ANCHOR.MIDDLE p = tf.paragraphs[0] p.alignment = PP_ALIGN.CENTER run = p.add_run() run.text = lbl run.font.name = FONT_MONO run.font.size = pt(8) run.font.color.rgb = C_INFO if active else C_MUTED add_text( slide, SLIDE_W - in_(4.8), SLIDE_H - in_(0.42), in_(4.4), in_(0.30), crumbs, size=10, color=C_INFO, uppercase=True, spacing=0.22, align=PP_ALIGN.RIGHT, ) def add_kicker(slide, x, y, text, color=C_INFO): add_text( slide, x, y, in_(8.0), in_(0.3), f"// {text}", size=11, color=color, uppercase=True, spacing=0.30, ) def add_bg_image(slide, path, opacity_hint=0.4): if not path.exists(): return try: pic = slide.shapes.add_picture(str(path), 0, in_(0.45), width=SLIDE_W) if pic.height > SLIDE_H - in_(1.0): pic.height = SLIDE_H - in_(1.0) pic.width = int(pic.height * (16 / 9)) pic.left = int((SLIDE_W - pic.width) / 2) pic.top = int((SLIDE_H - pic.height) / 2) veil = add_rect(slide, 0, 0, SLIDE_W, SLIDE_H, C_BG) veil.fill.transparency = 0.35 except Exception: pass # ---------------- SLIDES ---------------- def slide_01_portada(prs): slide = blank_slide(prs) add_topbar( slide, "SYSTEM ONLINE // LEMD ACC", right_pairs=["v1.0", "LIVE"], status_color=C_INFO, ) add_text( slide, 0, in_(2.3), SLIDE_W, in_(1.4), "SADAR", size=110, color=C_TEXT, bold=True, font=FONT_MONO, align=PP_ALIGN.CENTER, spacing=0.04, ) add_text( slide, 0, in_(3.9), SLIDE_W, in_(0.5), "Smart Anomaly Detection for Aviation Routes", size=22, color=C_INFO, font=FONT_MONO, align=PP_ALIGN.CENTER, spacing=0.18, uppercase=False, ) add_text( slide, 0, in_(4.5), SLIDE_W, in_(0.5), "Vigilancia inteligente del espacio aereo", size=18, color=C_TEXT, font=FONT_MONO, align=PP_ALIGN.CENTER, spacing=0.04, ) add_text( slide, 0, in_(6.0), SLIDE_W, in_(0.4), "aviacion comercial // datos abiertos // open-source", size=11, color=C_MUTED, uppercase=True, font=FONT_MONO, align=PP_ALIGN.CENTER, spacing=0.22, ) add_footer(slide, "SADAR // 01 / 11", controls=True) def slide_02_hook(prs): slide = blank_slide(prs) add_bg_image(slide, FRONTEND / "bg-mid.jpg") add_topbar( slide, "CASO HISTORICO // 31 JUL 2017 // LEMD ACC", right_pairs=["ALERT", "SQUAWK 7500"], status_color=C_ALERT, ) add_text( slide, in_(0.7), in_(1.1), in_(7.2), in_(3.6), "Un avion transmite el\ncodigo de secuestro\nsobre Madrid durante\ntreinta segundos.\nY aterriza como si nada.", size=38, color=C_TEXT, font=FONT_MONO, bold=True, spacing=0.02, ) add_text( slide, in_(0.7), in_(5.0), in_(7.2), in_(1.2), "¿Real o glitch?\nEsa pregunta la responde SADAR.", size=20, color=C_INFO, font=FONT_MONO, ) panel_x = in_(8.4) panel_y = in_(0.9) panel_w = in_(4.4) panel_h = in_(5.8) add_rect(slide, panel_x, panel_y, panel_w, panel_h, C_PANEL, line=C_EDGE) add_rect(slide, panel_x, panel_y, panel_w, in_(0.5), C_BG_DEEP, line=C_EDGE) add_text( slide, panel_x + in_(0.2), panel_y + in_(0.1), in_(3.0), in_(0.3), "SELECTED FLIGHT", size=10, color=C_LABEL, uppercase=True, spacing=0.24, ) add_text( slide, panel_x + panel_w - in_(1.4), panel_y + in_(0.1), in_(1.2), in_(0.3), "TRACK", size=10, color=C_INFO, uppercase=True, spacing=0.24, align=PP_ALIGN.RIGHT, ) rows = [ ("CALLSIGN", "IBE2845", C_TEXT, True), ("OPERATOR", "Iberia", C_TEXT, False), ("DATE", "31 JUL 2017", C_TEXT, False), ("SQUAWK", "7500", C_ALERT, True), ("MEANING", "unlawful interference", C_ALERT, False), ("DURATION", "~ 30 s", C_WARN, False), ("DIST LEMD", "0.8 NM", C_TEXT, False), ("OUTCOME", "landed nominally", C_INFO, False), ] row_y = panel_y + in_(0.6) row_h = in_(0.62) for (lbl, val, val_color, big) in rows: add_text( slide, panel_x + in_(0.25), row_y + in_(0.18), in_(2.0), in_(0.3), lbl, size=10, color=C_LABEL, uppercase=True, spacing=0.18, ) add_text( slide, panel_x + in_(2.2), row_y + in_(0.10), in_(2.0), in_(0.4), val, size=20 if big else 14, color=val_color, bold=big, font=FONT_MONO, align=PP_ALIGN.RIGHT, ) row_y += row_h add_text( slide, panel_x, panel_y + panel_h + in_(0.15), panel_w, in_(0.6), "La alerta salto en torre. Las bocinas sonaron.\nTreinta segundos despues, todo en silencio.", size=11, color=C_MUTED, italic=True, font=FONT_MONO, spacing=0.04, ) add_footer(slide, "SADAR // 02 / 11 // EL GLITCH") def slide_03_problema(prs): slide = blank_slide(prs) add_topbar( slide, "PROBLEM SPACE // ONE-CLASS LEARNING", right_pairs=["LABELED DATA", "4 / 19.000"], status_color=C_INFO, ) add_kicker(slide, in_(0.7), in_(0.95), "el problema") add_text( slide, in_(0.7), in_(1.4), in_(7.0), in_(1.6), "¿Como se detecta lo raro\ncuando no hay ejemplos de lo raro?", size=36, color=C_TEXT, bold=True, font=FONT_MONO, spacing=0.02, ) bullets = [ "Los secuestros no se repiten miles de veces.\nNo hay catalogo para ensenarle a la IA.", "SADAR no aprende que es malo.\nAprende que es normal.", "19.000 vuelos sobre Madrid le ensenan\ncomo es uno bueno. El resto lo deduce.", ] y = in_(3.5) for b in bullets: add_text( slide, in_(0.7), y, in_(0.4), in_(0.6), ">", size=22, color=C_INFO, bold=True, font=FONT_MONO, ) add_text( slide, in_(1.1), y, in_(6.8), in_(1.2), b, size=17, color=C_TEXT, font=FONT_MONO, spacing=0.04, ) y += in_(1.05) map_path = ASSETS / "eda" / "trajectories_map.png" if map_path.exists(): try: pic = slide.shapes.add_picture( str(map_path), in_(8.4), in_(1.3), width=in_(4.5), ) if pic.height > in_(5.0): pic.height = in_(5.0) pic.width = int(pic.height * (pic.width / pic.height)) add_text( slide, in_(8.4), in_(1.3) + pic.height + in_(0.1), in_(4.5), in_(0.4), "Trayectorias normales y eventos de emergencia sobre LEMD", size=10, color=C_MUTED, font=FONT_MONO, italic=True, align=PP_ALIGN.CENTER, spacing=0.04, ) except Exception: pass add_footer(slide, "SADAR // 03 / 11 // PROBLEMA") def slide_04_idea(prs): slide = blank_slide(prs) add_topbar( slide, "SYSTEM DESIGN // CORE PRINCIPLE", right_pairs=["MODE", "UNSUPERVISED"], status_color=C_INFO, ) add_kicker(slide, in_(0.7), in_(0.95), "la idea SADAR") add_text( slide, in_(0.7), in_(1.5), in_(12.0), in_(1.0), "Aprender lo normal. Alertar lo demas.", size=44, color=C_TEXT, bold=True, font=FONT_MONO, spacing=0.02, ) box_y = in_(3.2) box_w = in_(3.6) box_h = in_(2.2) gap = in_(0.4) start_x = (SLIDE_W - (3 * box_w + 2 * gap)) / 2 boxes = [ ("APRENDE DE", "19.000 vuelos reales\nde Madrid-Barajas"), ("MODELO", "Aprende como es\nun vuelo normal"), ("SALIDA", "Alerta si algo\nno encaja"), ] for i, (head, body) in enumerate(boxes): x = start_x + i * (box_w + gap) add_rect(slide, x, box_y, box_w, box_h, C_PANEL, line=C_EDGE) add_rect(slide, x, box_y, in_(0.06), box_h, C_INFO) add_text( slide, x + in_(0.3), box_y + in_(0.4), box_w - in_(0.6), in_(0.4), head, size=12, color=C_INFO, uppercase=True, spacing=0.24, font=FONT_MONO, ) add_text( slide, x + in_(0.3), box_y + in_(1.0), box_w - in_(0.6), in_(1.1), body, size=18, color=C_TEXT, font=FONT_MONO, spacing=0.02, ) add_text( slide, in_(0.7), box_y + box_h + in_(0.5), in_(12.0), in_(0.8), "Como un controlador con 19.000 aproximaciones en la cabeza:\ncuando una no encaja, se la nota al instante.", size=16, color=C_MUTED, italic=True, font=FONT_MONO, align=PP_ALIGN.CENTER, spacing=0.04, ) add_footer(slide, "SADAR // 04 / 11 // IDEA") def slide_screenshot(prs, idx, img_path, ctx_text, right_pairs, right_color, tag_text, tag_alert, caption_lines, crumbs, controls=True): slide = blank_slide(prs) add_bg_image(slide, FRONTEND / "bg-mid.jpg") add_topbar(slide, ctx_text, right_pairs=right_pairs, status_color=right_color) if img_path.exists(): try: pic = slide.shapes.add_picture( str(img_path), 0, 0, width=in_(11.5), ) max_h = in_(5.2) if pic.height > max_h: ratio = max_h / pic.height pic.height = max_h pic.width = int(pic.width * ratio) pic.left = int((SLIDE_W - pic.width) / 2) pic.top = in_(1.05) except Exception: pic = None else: pic = None tag_color = C_ALERT if tag_alert else C_INFO tag_x = in_(0.5) tag_y = in_(0.85) add_rect(slide, tag_x, tag_y, in_(0.06), in_(0.6), tag_color) add_rect(slide, tag_x + in_(0.06), tag_y, in_(5.5), in_(0.6), C_PANEL, line=C_EDGE) add_text( slide, tag_x + in_(0.25), tag_y + in_(0.13), in_(5.2), in_(0.4), tag_text, size=13, color=tag_color if tag_alert else C_TEXT, uppercase=True, spacing=0.20, font=FONT_MONO, bold=True, ) cap_w = in_(5.2) cap_h = in_(1.2) cap_x = SLIDE_W - cap_w - in_(0.5) cap_y = SLIDE_H - in_(1.95) add_rect(slide, cap_x, cap_y, cap_w, cap_h, C_PANEL, line=C_EDGE) add_rect(slide, cap_x + cap_w - in_(0.06), cap_y, in_(0.06), cap_h, tag_color) tb = slide.shapes.add_textbox(cap_x + in_(0.3), cap_y + in_(0.15), cap_w - in_(0.6), cap_h - in_(0.3)) tf = tb.text_frame tf.word_wrap = True tf.margin_left = tf.margin_right = Emu(0) tf.margin_top = tf.margin_bottom = Emu(0) for i, (txt, color) in enumerate(caption_lines): p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() p.alignment = PP_ALIGN.RIGHT run = p.add_run() run.text = txt run.font.name = FONT_MONO run.font.size = pt(15) run.font.color.rgb = color add_footer(slide, crumbs, controls=controls) def slide_05_radar(prs): slide_screenshot( prs, 5, ASSETS / "img1.png", "RANGE 40 NM // LEMD ACC // TRACKS 25", right_pairs=["STATUS", "NOMINAL"], right_color=C_INFO, tag_text="OPERACION NORMAL", tag_alert=False, caption_lines=[ ("Lo que ve un operador en torre:", C_TEXT), ("veinticinco vuelos vigilados a la vez", C_INFO), ], crumbs="SADAR // 05 / 11 // RADAR", controls=True, ) def slide_06_simulador(prs): slide_screenshot( prs, 6, ASSETS / "img2.png", "SIMULATOR // LEMD ACC // SDR001 INJECTED", right_pairs=["ALERT", "LATENCY 0s"], right_color=C_ALERT, tag_text="ANOMALIA INYECTADA // < 2 MIN", tag_alert=True, caption_lines=[ ("Inyectamos una maniobra rara", C_TEXT), ("y el sistema avisa antes de los 2 minutos", C_INFO), ], crumbs="SADAR // 06 / 11 // SIMULADOR", controls=True, ) def slide_07_resultados(prs): slide = blank_slide(prs) add_topbar( slide, "SYSTEM METRICS // VALIDATION", right_pairs=["LATENCY", "< 2 MIN"], status_color=C_INFO, ) add_kicker(slide, in_(0.7), in_(0.95), "resultados") add_text( slide, in_(0.7), in_(1.45), in_(12.0), in_(0.8), "El sistema funciona y avisa pronto.", size=36, color=C_TEXT, bold=True, font=FONT_MONO, ) kpis = [ ("LATENCIA HASTA LA ALERTA", "< 2 min", "desde que algo se sale del patron\nhasta que SADAR lo dice"), ("VUELOS REALES APRENDIDOS", "19.000", "trayectorias completas de\nMadrid-Barajas (OpenSky)"), ("EMERGENCIAS REALES DETECTADAS", "4", "emergencias del historico\ndetectadas correctamente"), ] y = in_(2.8) box_h = in_(2.4) box_w = in_(3.9) gap = in_(0.3) start_x = (SLIDE_W - (3 * box_w + 2 * gap)) / 2 for i, (lbl, val, cap) in enumerate(kpis): x = start_x + i * (box_w + gap) add_rect(slide, x, y, box_w, box_h, C_PANEL, line=C_EDGE) add_rect(slide, x, y, in_(0.06), box_h, C_INFO) add_text( slide, x + in_(0.3), y + in_(0.3), box_w - in_(0.6), in_(0.4), lbl, size=10, color=C_LABEL, uppercase=True, spacing=0.22, font=FONT_MONO, ) add_text( slide, x + in_(0.3), y + in_(0.9), box_w - in_(0.6), in_(1.0), val, size=44, color=C_INFO, bold=True, font=FONT_MONO, ) add_text( slide, x + in_(0.3), y + in_(1.7), box_w - in_(0.6), in_(0.8), cap, size=11, color=C_MUTED, font=FONT_MONO, spacing=0.02, ) add_text( slide, in_(0.7), in_(5.5), in_(12.0), in_(0.8), "En pruebas con maniobras anomalas inyectadas, el modelo\nlevanta la alerta antes de que pase nada.", size=15, color=C_TEXT, font=FONT_MONO, spacing=0.02, align=PP_ALIGN.CENTER, ) add_text( slide, in_(0.7), in_(6.3), in_(12.0), in_(0.4), "En la linea de iniciativas espanolas como FARO o ISOBAR (CRIDA-ENAIRE, programa europeo SESAR).", size=10, color=C_MUTED, italic=True, font=FONT_MONO, align=PP_ALIGN.CENTER, spacing=0.04, ) add_footer(slide, "SADAR // 07 / 11 // RESULTADOS") def slide_08_casos(prs): slide = blank_slide(prs) add_topbar( slide, "DEPLOYMENT SCENARIOS // 04 USE CASES", right_pairs=["STATUS", "PRODUCTION READY"], status_color=C_INFO, ) add_kicker(slide, in_(0.7), in_(0.95), "casos de uso") add_text( slide, in_(0.7), in_(1.4), in_(12.0), in_(0.8), "Donde SADAR aporta valor hoy.", size=32, color=C_TEXT, bold=True, font=FONT_MONO, ) cases = [ ("01", "Vigilancia ADS-B publica", "Redes abiertas (OpenSky, FlightAware) sin acceso a planes de vuelo. SADAR anade una capa de deteccion automatica sobre datos publicos.", "periodismo · ONGs · transparencia"), ("02", "Aeropuertos sin ATM propietario", "Aeropuertos secundarios y regiones sin sistemas avanzados de vigilancia. Open-source y replicable con datos ADS-B locales.", "aeropuertos regionales · mercados emergentes"), ("03", "Observatorios del espacio aereo", "Reguladores y organismos independientes. Vision transversal sin necesidad de acceso a datos privados de cada operador.", "AESA · observatorios · auditoria"), ("04", "Investigacion y academia", "Plataforma base reproducible y abierta para investigar nuevos metodos de deteccion de anomalias en trayectorias.", "universidades · centros de I+D"), ] grid_x = in_(0.7) grid_y = in_(2.5) cell_w = in_(6.0) cell_h = in_(1.95) gap = in_(0.3) for i, (num, title, body, who) in enumerate(cases): col, row = i % 2, i // 2 x = grid_x + col * (cell_w + gap) y = grid_y + row * (cell_h + in_(0.25)) add_rect(slide, x, y, cell_w, cell_h, C_PANEL, line=C_EDGE) add_rect(slide, x, y, in_(0.06), cell_h, C_INFO) add_text( slide, x + in_(0.3), y + in_(0.2), in_(1.0), in_(0.3), num, size=10, color=C_INFO, uppercase=True, spacing=0.24, font=FONT_MONO, ) add_text( slide, x + in_(0.3), y + in_(0.55), cell_w - in_(0.6), in_(0.5), title, size=18, color=C_TEXT, bold=True, font=FONT_MONO, ) add_text( slide, x + in_(0.3), y + in_(1.05), cell_w - in_(0.6), in_(0.7), body, size=11, color=C_TEXT, font=FONT_MONO, spacing=0.02, ) add_text( slide, x + in_(0.3), y + cell_h - in_(0.35), cell_w - in_(0.6), in_(0.3), who, size=9, color=C_LABEL, uppercase=True, spacing=0.18, font=FONT_MONO, ) add_text( slide, in_(0.7), SLIDE_H - in_(1.0), in_(12.0), in_(0.4), "SADAR no compite con la torre. Vigila lo que la torre no mira: el despues, lo publico, lo pequeno.", size=12, color=C_MUTED, italic=True, font=FONT_MONO, align=PP_ALIGN.CENTER, spacing=0.04, ) add_footer(slide, "SADAR // 08 / 11 // CASOS DE USO") def slide_09_como(prs): slide = blank_slide(prs) add_topbar( slide, "ARCHITECTURE // MODEL ANATOMY", right_pairs=["SIZE", "222 KB"], status_color=C_INFO, ) add_kicker(slide, in_(0.7), in_(0.95), "como esta hecho") add_text( slide, in_(0.7), in_(1.4), in_(12.0), in_(1.5), "Un modelo. Entrenado solo con vuelos normales.\nAprende como es un vuelo normal;\nsi algo no encaja, lo dice.", size=24, color=C_TEXT, font=FONT_MONO, spacing=0.02, ) nodes = [ ("01 · DATOS", "3,4 millones de puntos GPS\nde 19.000 vuelos reales"), ("02 · MODELO", "Aprende lo normal\nsin ver una sola anomalia"), ("03 · ALERTA", "Aviso en\nmenos de 2 minutos"), ] nx = in_(0.7) ny = in_(4.0) nw = (SLIDE_W - in_(1.4) - in_(0.6)) / 3 nh = in_(1.4) for i, (lbl, body) in enumerate(nodes): x = nx + i * (nw + in_(0.3)) add_rect(slide, x, ny, nw, nh, C_PANEL, line=C_EDGE) add_rect(slide, x, ny, in_(0.06), nh, C_INFO) add_text( slide, x + in_(0.3), ny + in_(0.2), nw - in_(0.6), in_(0.3), lbl, size=11, color=C_LABEL, uppercase=True, spacing=0.22, font=FONT_MONO, ) add_text( slide, x + in_(0.3), ny + in_(0.65), nw - in_(0.6), in_(0.7), body, size=14, color=C_TEXT, font=FONT_MONO, spacing=0.02, ) bullets = [ "Aprende sin etiquetas: nadie le dice que es un secuestro.", "Mira 7 senales por punto: posicion, altitud, velocidad, rumbo, ascenso o descenso.", "El modelo entero ocupa 222 KB. Cabe en un correo electronico.", ] by = in_(5.6) for b in bullets: add_text( slide, in_(0.7), by, in_(0.4), in_(0.4), ">", size=14, color=C_INFO, bold=True, font=FONT_MONO, ) add_text( slide, in_(1.1), by, in_(11.5), in_(0.4), b, size=13, color=C_TEXT, font=FONT_MONO, spacing=0.02, ) by += in_(0.42) add_footer(slide, "SADAR // 09 / 11 // COMO") def slide_10_cierre(prs): slide = blank_slide(prs) add_bg_image(slide, FRONTEND / "bg-mid.jpg") add_topbar( slide, "SESSION END // SUMMARY", right_pairs=["RECORDING STOPPED"], status_color=C_MUTED, ) add_kicker(slide, in_(0.7), in_(0.95), "cierre") add_text( slide, in_(0.7), in_(1.8), in_(12.0), in_(2.4), "SADAR vigila lo que\nnadie esta vigilando.", size=64, color=C_TEXT, bold=True, font=FONT_MONO, spacing=0.02, ) add_text( slide, in_(0.7), in_(4.6), in_(12.0), in_(1.2), "Donde no llega la torre. Donde no hay plan de vuelo.\nDonde no hay licencia de software.\nY avisa antes de los dos minutos.", size=22, color=C_TEXT, font=FONT_MONO, spacing=0.02, ) add_text( slide, in_(0.7), SLIDE_H - in_(1.6), in_(12.0), in_(0.8), "Honestidad tecnica: sin planes de vuelo oficiales, sin eventos catastroficos reales.\nLos 4 vuelos con codigo de emergencia se usaron para validar, nunca para entrenar.", size=11, color=C_MUTED, italic=True, font=FONT_MONO, spacing=0.02, ) add_footer(slide, "SADAR // 10 / 11 // CIERRE") def slide_11_qr(prs): slide = blank_slide(prs) add_topbar( slide, "DEMO ACCESS // SCAN TO CONNECT", right_pairs=["ONLINE"], status_color=C_INFO, ) add_kicker(slide, in_(0.7), in_(0.95), "acceso") add_text( slide, in_(0.7), in_(1.5), in_(7.0), in_(2.5), "Escanea\npara probar el\nsimulador en vivo.", size=46, color=C_TEXT, bold=True, font=FONT_MONO, spacing=0.02, ) panel_x = in_(0.7) panel_y = in_(4.3) panel_w = in_(6.6) panel_h = in_(2.4) add_rect(slide, panel_x, panel_y, panel_w, panel_h, C_PANEL, line=C_EDGE) add_rect(slide, panel_x, panel_y, panel_w, in_(0.5), C_BG_DEEP, line=C_EDGE) add_text( slide, panel_x + in_(0.25), panel_y + in_(0.1), in_(3.0), in_(0.3), "RECURSOS", size=10, color=C_LABEL, uppercase=True, spacing=0.24, ) add_text( slide, panel_x + panel_w - in_(1.4), panel_y + in_(0.1), in_(1.2), in_(0.3), "PUBLIC", size=10, color=C_INFO, uppercase=True, spacing=0.24, align=PP_ALIGN.RIGHT, ) rows = [ ("DEMO", "sadar.demo"), ("CODIGO", "github.com/sadar"), ("ARTICULO", "medium / sadar"), ("DATOS", "opensky-network.org"), ] ry = panel_y + in_(0.65) for lbl, val in rows: add_text( slide, panel_x + in_(0.3), ry, in_(2.0), in_(0.35), lbl, size=11, color=C_LABEL, uppercase=True, spacing=0.18, ) add_text( slide, panel_x + in_(2.5), ry - in_(0.02), in_(4.0), in_(0.4), val, size=14, color=C_INFO, font=FONT_MONO, align=PP_ALIGN.RIGHT, ) ry += in_(0.40) qr_path = ASSETS / "qr-placeholder.png" if qr_path.exists(): try: qr_w = in_(4.0) qr_x = SLIDE_W - qr_w - in_(0.7) qr_y = in_(1.5) add_rect(slide, qr_x - in_(0.15), qr_y - in_(0.15), qr_w + in_(0.3), qr_w + in_(0.3), C_PANEL, line=C_EDGE) add_rect(slide, qr_x - in_(0.15), qr_y - in_(0.15), in_(0.06), qr_w + in_(0.3), C_INFO) slide.shapes.add_picture( str(qr_path), qr_x, qr_y, width=qr_w, height=qr_w, ) add_text( slide, qr_x, qr_y + qr_w + in_(0.2), qr_w, in_(0.4), "scan · play · inject", size=12, color=C_INFO, uppercase=True, spacing=0.28, font=FONT_MONO, align=PP_ALIGN.CENTER, ) add_text( slide, qr_x - in_(0.5), qr_y + qr_w + in_(0.9), qr_w + in_(1.0), in_(0.6), "· Gracias ·", size=24, color=C_INFO, uppercase=True, spacing=0.32, font=FONT_MONO, align=PP_ALIGN.CENTER, ) except Exception: pass add_footer(slide, "SADAR // 11 / 11 // FIN") def main(): prs = make_prs() slide_01_portada(prs) slide_02_hook(prs) slide_03_problema(prs) slide_04_idea(prs) slide_05_radar(prs) slide_06_simulador(prs) slide_07_resultados(prs) slide_08_casos(prs) slide_09_como(prs) slide_10_cierre(prs) slide_11_qr(prs) OUT.parent.mkdir(parents=True, exist_ok=True) prs.save(OUT) print(f"OK -> {OUT}") if __name__ == "__main__": main()