| """ |
| 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 |
|
|
|
|
| |
|
|
| 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() |
|
|