Download scripts/build_degradation_slides.py from InternsProgressUpdate/Venket: direct link, hf CLI and curl.
- Browser
- Download file 12.2 kB
-
https://huggingface.co/InternsProgressUpdate/Venket/resolve/main/scripts/build_degradation_slides.py
- Command line
-
hf download hf://InternsProgressUpdate/Venket/scripts/build_degradation_slides.py
-
curl -L -o build_degradation_slides.py https://huggingface.co/InternsProgressUpdate/Venket/resolve/main/scripts/build_degradation_slides.py
12.2 kB
| #!/usr/bin/env python | |
| """Append a before/after slide per augmentation effect, plus normal- and | |
| extreme-case multi-degradation slides, to Metrics_Thresholds_Formulas.pptx. | |
| Reuses the assets written by generate_before_after_assets.py and | |
| generate_case_assets.py (run those first). New slides replicate the deck's | |
| existing visual language exactly (navy header bar, orange accent line, teal | |
| section label, Calibri/Consolas body text, bottom-right page number) so they | |
| read as part of the same deck rather than a bolt-on. | |
| The new slides are inserted as a new section 8 ("Degradation gallery") and | |
| section 9 ("Multi-degradation cases"), placed right before the closing "Key | |
| takeaways" slide, and the Agenda slide (slide 2) is extended with the two new | |
| entries. A one-time backup of the original file is kept alongside it. | |
| Usage: | |
| python scripts/build_degradation_slides.py | |
| """ | |
| from __future__ import annotations | |
| import copy | |
| import json | |
| import shutil | |
| import sys | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) | |
| from pptx import Presentation | |
| from pptx.dml.color import RGBColor | |
| from pptx.enum.shapes import MSO_SHAPE | |
| from pptx.enum.text import MSO_ANCHOR, PP_ALIGN | |
| from pptx.util import Emu, Pt | |
| from PIL import Image | |
| from scripts.generate_before_after_assets import ( | |
| ALL_EFFECTS, DISPLAY_NAME, ZOOM_EFFECTS, | |
| ) | |
| from scripts.generate_multiple_degradation_extreme_gallery import _fmt_effect | |
| ROOT = Path(__file__).resolve().parents[1] | |
| PPTX_PATH = ROOT / "Metrics_Thresholds_Formulas.pptx" | |
| BACKUP_PATH = ROOT / "Metrics_Thresholds_Formulas.backup.pptx" | |
| DEG_ROOT = ROOT / "degradation" | |
| # ---------------------------------------------------------------- palette -- | |
| NAVY, TEAL, ORANGE = "0F2A43", "1B7A7A", "E07A2C" | |
| LIGHT_TEAL, GRAY, LIGHT_BG, TEXT_DARK, WHITE = "9BC5C5", "6B767C", "F4F7F8", "222A30", "FFFFFF" | |
| def rgb(hexstr: str) -> RGBColor: | |
| return RGBColor.from_string(hexstr) | |
| # ------------------------------------------------------------- geometry --- | |
| SLIDE_W, SLIDE_H = Emu(12191695), Emu(6858000) | |
| HEADER_H, ACCENT_H = Emu(1051560), Emu(38100) | |
| TITLE_X = Emu(502920) | |
| CONTENT_X, CONTENT_W = Emu(640080), Emu(10881360) | |
| FOOTER_X, FOOTER_Y = Emu(11475720), Emu(6510528) | |
| FOOTER_W, FOOTER_H = Emu(640080), Emu(274320) | |
| LEFT_X, RIGHT_X = Emu(640080), Emu(6263640) | |
| COL_W = Emu(5257800) | |
| LABEL_Y, LABEL_H = Emu(1554480), Emu(365760) | |
| IMG_Y = Emu(LABEL_Y + LABEL_H + Emu(91440)) | |
| CAPTION_Y = Emu(1150000) | |
| CAPTION_H = Emu(320040) | |
| # ------------------------------------------------------ formula captions -- | |
| FORMULAS = { | |
| "rotation": "extra_deg = U(-1,1) × strength × 8.0", | |
| "stroke_dilation": "ksize = round(1 + strength×5) [odd]", | |
| "ink_fading": "mult = clip(1 − strength×(1−field)×0.9, .05, 1)", | |
| "ink_bleed": "ksize = round(1+strength×4); halo floor = .35+.35×strength", | |
| "motion_blur": "length = round(3 + strength×16)", | |
| "gaussian_blur": "sigma = 0.2 + strength×2.2", | |
| "perspective": "scale = 0.01 + strength×0.09", | |
| "contrast": "contrast_limit = ±(strength×0.45)", | |
| "gamma": "delta = round(strength×45); gamma_limit = (100−delta, 100+delta), lo≥40", | |
| "coffee_stain": "n_blobs = 1+randint(0,1+round(strength×3)); opacity = .10+.30×strength", | |
| "stain": "radius = (.06+.16×strength)×min(w,h); opacity scales with strength", | |
| "shadow": "darkness = 0.15+0.55×strength; band/radius/fold-width scale with strength", | |
| "crumple": "amplitude = strength×min(w,h)×0.015", | |
| "discoloration": "tint strength scales with strength per tone", | |
| "page_tilt": "angle = U(-1,1) × (1.5 + strength×6.5)", | |
| "dust": "n_specks = (h×w)×0.00003×(1+strength×6)", | |
| "scanner_streaks": "n_streaks = 1+int(strength×6)", | |
| "scanner_noise": "GaussNoise std = 0.01 + strength×0.12", | |
| "salt_pepper": "amount = 0.002 + strength×0.09", | |
| "jpeg_compression": "quality = randint(min_quality, max_quality) — NOT driven by strength", | |
| } | |
| def short_list(names: list[str], limit: int = 8) -> str: | |
| if len(names) <= limit: | |
| return ", ".join(names) if names else "none" | |
| return ", ".join(names[:limit]) + f", +{len(names) - limit} more" | |
| # ----------------------------------------------------------- primitives --- | |
| def add_rect(slide, x, y, w, h, color: str): | |
| shp = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, w, h) | |
| shp.fill.solid() | |
| shp.fill.fore_color.rgb = rgb(color) | |
| shp.line.fill.background() | |
| return shp | |
| def add_text( | |
| slide, x, y, w, h, text, size, color, *, bold=False, font="Calibri", | |
| align=PP_ALIGN.LEFT, anchor=None, word_wrap=True, | |
| ): | |
| tb = slide.shapes.add_textbox(x, y, w, h) | |
| tf = tb.text_frame | |
| tf.margin_left = tf.margin_right = tf.margin_top = tf.margin_bottom = 0 | |
| tf.word_wrap = word_wrap | |
| if anchor is not None: | |
| tf.vertical_anchor = anchor | |
| lines = text.split("\n") | |
| 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 | |
| run.font.size = Pt(size) | |
| run.font.bold = bold | |
| run.font.name = font | |
| run.font.color.rgb = rgb(color) | |
| return tb | |
| def add_bg(slide): | |
| add_rect(slide, Emu(0), Emu(0), SLIDE_W, SLIDE_H, WHITE) | |
| def add_header(slide, section_label: str, title: str, page_num: int): | |
| add_rect(slide, Emu(0), Emu(0), SLIDE_W, HEADER_H, NAVY) | |
| add_rect(slide, Emu(0), HEADER_H, SLIDE_W, ACCENT_H, ORANGE) | |
| add_text(slide, TITLE_X, Emu(128016), Emu(9144000), Emu(274320), section_label, 12, LIGHT_TEAL, bold=True) | |
| add_text(slide, TITLE_X, Emu(384048), Emu(10515600), Emu(594360), title, 28, WHITE, bold=True) | |
| add_text( | |
| slide, FOOTER_X, FOOTER_Y, FOOTER_W, FOOTER_H, str(page_num), 11, GRAY, | |
| align=PP_ALIGN.RIGHT, | |
| ) | |
| def fit_picture(slide, img_path: Path, box_x, box_y, box_w, box_h): | |
| iw, ih = Image.open(img_path).size | |
| scale = min(box_w / iw, box_h / ih) | |
| disp_w, disp_h = int(iw * scale), int(ih * scale) | |
| left = int(box_x + (box_w - disp_w) / 2) | |
| top = int(box_y + (box_h - disp_h) / 2) | |
| slide.shapes.add_picture(str(img_path), left, top, width=disp_w, height=disp_h) | |
| def add_panel_label(slide, x, color, text): | |
| add_rect(slide, x, LABEL_Y, COL_W, LABEL_H, color) | |
| add_text( | |
| slide, x, LABEL_Y, COL_W, LABEL_H, text, 14, WHITE, bold=True, | |
| align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, word_wrap=False, | |
| ) | |
| # -------------------------------------------------------------- slides ---- | |
| def build_before_after_slide(prs, layout, effect: str, page_num: int, manifest: dict): | |
| slide = prs.slides.add_slide(layout) | |
| add_bg(slide) | |
| add_header(slide, "8. DEGRADATION GALLERY", DISPLAY_NAME[effect], page_num) | |
| rec = manifest[effect] | |
| resolved = _fmt_effect(effect, {"strength": rec["strength"], "params": rec["params"] or {}}) | |
| caption = ( | |
| f"{FORMULAS[effect]} | resolved: {resolved} | " | |
| f"difficulty after = {rec['difficulty_score']:.3f} ({rec['difficulty_tier']})" | |
| ) | |
| add_text(slide, CONTENT_X, CAPTION_Y, CONTENT_W, CAPTION_H, caption, 12, GRAY, font="Consolas") | |
| add_panel_label(slide, LEFT_X, TEAL, "BEFORE - clean render") | |
| add_panel_label(slide, RIGHT_X, ORANGE, f"AFTER - {DISPLAY_NAME[effect]} (strength={rec['strength']:.2f})") | |
| img_bottom = Emu(6100000) | |
| img_h = Emu(img_bottom - IMG_Y) | |
| before_name = "before_zoom.png" if effect in ZOOM_EFFECTS else "before.png" | |
| after_name = "after_zoom.png" if effect in ZOOM_EFFECTS else "after.png" | |
| fit_picture(slide, DEG_ROOT / effect / before_name, LEFT_X, IMG_Y, COL_W, img_h) | |
| fit_picture(slide, DEG_ROOT / effect / after_name, RIGHT_X, IMG_Y, COL_W, img_h) | |
| return slide | |
| def build_case_slide(prs, layout, kind: str, page_num: int): | |
| slide = prs.slides.add_slide(layout) | |
| add_bg(slide) | |
| title = "Normal-severity combinations" if kind == "normal" else "Extreme-severity combinations" | |
| add_header(slide, "9. MULTI-DEGRADATION CASES", title, page_num) | |
| if kind == "normal": | |
| folder = ROOT / "multiple_degradation" / "case_assets" | |
| subtitle = ( | |
| "configs/generate.yaml -- every effect independently rolls its own realistic " | |
| "probability per sample (mixed, moderate combinations)" | |
| ) | |
| color = TEAL | |
| else: | |
| folder = ROOT / "multiple_degradation_extreme" / "case_assets" | |
| subtitle = ( | |
| "worst case -- every effect probability=0.85, strength 0.7-1.0, plus heavy " | |
| "misplacement / faint+blurred ink (see scripts/generate_multiple_degradation_extreme_gallery.py)" | |
| ) | |
| color = ORANGE | |
| manifest = json.loads((folder / "case_manifest.json").read_text()) | |
| add_text(slide, CONTENT_X, CAPTION_Y, CONTENT_W, CAPTION_H, subtitle, 12, GRAY, font="Consolas") | |
| img_bottom = Emu(5850000) | |
| img_h = Emu(img_bottom - IMG_Y) | |
| cap_y = Emu(img_bottom + Emu(45720)) | |
| cap_h = Emu(600000) | |
| for i, x in enumerate((LEFT_X, RIGHT_X)): | |
| key = f"case_{i + 1}" | |
| rec = manifest[key] | |
| add_panel_label(slide, x, color, f"Sample {i + 1} - {rec['n_applied']}/{rec['n_total']} effects applied") | |
| fit_picture(slide, folder / f"{key}.png", x, IMG_Y, COL_W, img_h) | |
| cap = ( | |
| f"difficulty = {rec['difficulty_score']:.3f} ({rec['difficulty_tier']}) | " | |
| f"placement: rot={rec['placement']['rotation_deg']:.1f} deg " | |
| f"scale={rec['placement']['scale_factor']:.2f} misplaced={rec['placement']['misplaced']}\n" | |
| f"applied: {short_list(rec['applied_effects'])}" | |
| ) | |
| add_text(slide, x, cap_y, COL_W, cap_h, cap, 10, TEXT_DARK, font="Consolas") | |
| return slide | |
| # ---------------------------------------------------------- agenda edit --- | |
| def extend_agenda(prs): | |
| slide = prs.slides[1] | |
| target = None | |
| for shape in slide.shapes: | |
| if shape.name == "TextBox 7": | |
| target = shape | |
| break | |
| if target is None: | |
| print("WARNING: could not find agenda TextBox 7 -- skipping agenda update") | |
| return | |
| tf = target.text_frame | |
| n_original = len(tf.paragraphs) | |
| last_p = tf.paragraphs[-1]._p | |
| new_texts = ( | |
| "8. Degradation gallery — before & after, all 20 effects", | |
| "9. Multi-degradation cases — normal vs. extreme severity", | |
| ) | |
| for text in new_texts: | |
| new_p = copy.deepcopy(last_p) | |
| run_elm = new_p.findall(".//{http://schemas.openxmlformats.org/drawingml/2006/main}t") | |
| run_elm[0].text = text | |
| last_p.addnext(new_p) | |
| last_p = new_p | |
| # Shape height doesn't clip a top-anchored textbox -- text simply overflows past | |
| # it -- so what actually matters is total rendered text height staying inside the | |
| # slide. 9 lines at the original 22pt ran ~24pt past the bottom edge, so shrink | |
| # every run down to 20pt (measured to comfortably clear the slide bottom). | |
| for para in tf.paragraphs: | |
| for run in para.runs: | |
| run.font.size = Pt(20) | |
| # ---------------------------------------------------------------- main ---- | |
| def main(): | |
| if not BACKUP_PATH.exists(): | |
| shutil.copy2(PPTX_PATH, BACKUP_PATH) | |
| print(f"Backed up original deck -> {BACKUP_PATH}") | |
| manifest = json.loads((DEG_ROOT / "before_after_manifest.json").read_text()) | |
| prs = Presentation(str(PPTX_PATH)) | |
| layout = prs.slides[2].slide_layout # "Blank" layout used by all content slides | |
| sld_id_lst = prs.slides._sldIdLst | |
| closing_sld_id = sld_id_lst[-1] # "Key takeaways" slide, must stay last | |
| extend_agenda(prs) | |
| page_num = 19 | |
| for effect in ALL_EFFECTS: | |
| build_before_after_slide(prs, layout, effect, page_num, manifest) | |
| page_num += 1 | |
| build_case_slide(prs, layout, "normal", page_num) | |
| page_num += 1 | |
| build_case_slide(prs, layout, "extreme", page_num) | |
| page_num += 1 | |
| sld_id_lst.remove(closing_sld_id) | |
| sld_id_lst.append(closing_sld_id) | |
| prs.save(str(PPTX_PATH)) | |
| print(f"Saved {PPTX_PATH} -- {len(prs.slides.__iter__.__self__._sldIdLst)} slides total") | |
| if __name__ == "__main__": | |
| main() | |