#!/usr/bin/env python3 """Build a PowerPoint figure of the two-stage architectures: T2M-GPT vs NSLP-G. Both are two-stage sign-language-production systems, and the contrast between them is exactly what the evaluation turned on: stage 1 learns a pose representation from SKELETONS ALONE (no text) stage 2 learns to emit that representation FROM TEXT T2M-GPT discrete: 512-entry VQ codebook, 4x temporal downsample, autoregressive GPT + sampling -> ceiling 0.098, DTW 0.576 NSLP-G continuous: per-frame Gaussian latent, NO temporal compression, non-autoregressive regression + length head -> ceiling 0.031, DTW 0.497 Everything editable: real shapes and text, no rasterised image. Slide 1 = the diagram, slide 2 = the difference table. """ import argparse from pptx import Presentation from pptx.dml.color import RGBColor from pptx.enum.shapes import MSO_CONNECTOR, MSO_SHAPE from pptx.enum.text import MSO_ANCHOR, PP_ALIGN from pptx.util import Emu, Inches, Pt INK = RGBColor(0x1A, 0x1A, 0x1A) MUTED = RGBColor(0x6B, 0x6B, 0x6B) RULE = RGBColor(0xC8, 0xC8, 0xC8) T2M = RGBColor(0x1A, 0x4F, 0x8A) # blue - T2M-GPT T2M_BG = RGBColor(0xE8, 0xF0, 0xF8) NSL = RGBColor(0x8E, 0x44, 0xAD) # purple - NSLP-G NSL_BG = RGBColor(0xF3, 0xEB, 0xF7) ACC = RGBColor(0xC0, 0x39, 0x2B) # red - the bottleneck GREY_BG = RGBColor(0xF2, 0xF2, 0xF2) def box(sl, x, y, w, h, text, fill, line, *, bold=False, size=10.5, shape=MSO_SHAPE.ROUNDED_RECTANGLE, fg=INK, dash=False): s = sl.shapes.add_shape(shape, Inches(x), Inches(y), Inches(w), Inches(h)) s.fill.solid() s.fill.fore_color.rgb = fill s.line.color.rgb = line s.line.width = Pt(1.25) if dash: from pptx.enum.dml import MSO_LINE_DASH_STYLE s.line.dash_style = MSO_LINE_DASH_STYLE.DASH s.shadow.inherit = False tf = s.text_frame tf.word_wrap = True tf.vertical_anchor = MSO_ANCHOR.MIDDLE tf.margin_left = tf.margin_right = Emu(36000) tf.margin_top = tf.margin_bottom = 0 lines = text.split('\n') for i, ln in enumerate(lines): p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() p.alignment = PP_ALIGN.CENTER r = p.add_run(); r.text = ln r.font.size = Pt(size if i == 0 else size - 1.5) r.font.bold = bold and i == 0 r.font.color.rgb = fg if i == 0 else MUTED r.font.name = 'Calibri' return s def label(sl, x, y, w, text, *, size=11, bold=False, color=INK, align=PP_ALIGN.LEFT, italic=False): tb = sl.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(0.3)) tf = tb.text_frame tf.word_wrap = True p = tf.paragraphs[0] p.alignment = align for i, ln in enumerate(text.split('\n')): pp = p if i == 0 else tf.add_paragraph() pp.alignment = align r = pp.add_run(); r.text = ln r.font.size = Pt(size); r.font.bold = bold; r.font.italic = italic r.font.color.rgb = color; r.font.name = 'Calibri' return tb def arrow(sl, x1, y1, x2, y2, color=MUTED, width=1.5): c = sl.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, Inches(x1), Inches(y1), Inches(x2), Inches(y2)) c.line.color.rgb = color c.line.width = Pt(width) c.line.end_arrowhead = True # not exposed by python-pptx; patched via XML below return c def set_arrowhead(conn): """python-pptx has no arrowhead API, so set it on the line XML directly.""" ln = conn.line._get_or_add_ln() from pptx.oxml.ns import qn tail = ln.find(qn('a:tailEnd')) if tail is None: tail = ln.makeelement(qn('a:tailEnd'), {}) ln.append(tail) tail.set('type', 'triangle'); tail.set('w', 'med'); tail.set('len', 'med') def flow(sl, y, boxes, color, bg, *, h=0.62, gap=0.30, x0=0.55): """Lay a row of boxes left to right with arrows between them.""" shapes = [] x = x0 for (w, txt, kind) in boxes: f, l, fg, bold = bg, color, INK, False if kind == 'io': f, l = GREY_BG, RULE elif kind == 'key': f, l, bold = bg, ACC, True elif kind == 'main': bold = True shapes.append(box(sl, x, y, w, h, txt, f, l, bold=bold)) x += w + gap for a, b in zip(shapes, shapes[1:]): c = arrow(sl, a.left / 914400 + a.width / 914400, y + h / 2, b.left / 914400, y + h / 2, color=MUTED) set_arrowhead(c) return shapes, x - gap def main(): ap = argparse.ArgumentParser() ap.add_argument('--out', default='figs/two_stage_architectures.pptx') args = ap.parse_args() prs = Presentation() prs.slide_width = Inches(13.333) prs.slide_height = Inches(7.5) blank = prs.slide_layouts[6] # ============================================================ slide 1: diagram sl = prs.slides.add_slide(blank) label(sl, 0.5, 0.22, 12.4, 'Two-stage sign language production: T2M-GPT vs NSLP-G', size=22, bold=True) label(sl, 0.5, 0.68, 12.4, 'Stage 1 learns a pose representation from skeletons alone (no text). ' 'Stage 2 learns to emit that representation from text.', size=11.5, color=MUTED) # ---------------- T2M-GPT ---------------- hdr = box(sl, 0.5, 1.15, 12.35, 0.34, 'T2M-GPT — discrete tokens, autoregressive', T2M_BG, T2M, bold=True, size=12.5, shape=MSO_SHAPE.RECTANGLE) label(sl, 0.62, 1.60, 6.0, 'Stage 1 pose VQ-VAE (poses only)', size=10.5, bold=True, color=T2M) _, ex = flow(sl, 1.92, [ (1.18, 'Pose\n[T, 248]', 'io'), (1.30, 'Conv1D\nencoder', 'main'), (1.42, 'VQ codebook\n512 entries', 'key'), (1.12, 'tokens\n[T/4]', 'io'), (1.30, 'Conv1D\ndecoder', 'main'), (1.18, 'Pose\n[T, 248]', 'io'), ], T2M, T2M_BG, gap=0.24) label(sl, ex + 0.16, 1.94, 13.15 - ex, '4× temporal downsample\n1 token per 4 frames\nceiling 0.098', size=9.5, color=ACC) label(sl, 0.62, 2.75, 6.0, 'Stage 2 text → tokens (frozen stage 1)', size=10.5, bold=True, color=T2M) _, ex = flow(sl, 3.07, [ (1.18, 'Gloss /\nsentence', 'io'), (1.30, 'PhoBERT\nfrozen', 'main'), (1.42, 'one pooled\nvector [768]', 'key'), (1.38, 'causal GPT\n9+9 blocks', 'main'), (1.12, 'tokens\nsampled', 'io'), (1.26, 'stage-1\ndecoder', 'main'), ], T2M, T2M_BG, gap=0.24) label(sl, ex + 0.16, 3.02, 13.15 - ex, 'autoregressive, one token\nat a time; CE loss;\ncategorial sampling', size=9.5, color=MUTED) # ---------------- NSLP-G ---------------- box(sl, 0.5, 4.10, 12.35, 0.34, 'NSLP-G — continuous Gaussian space, non-autoregressive', NSL_BG, NSL, bold=True, size=12.5, shape=MSO_SHAPE.RECTANGLE) label(sl, 0.62, 4.55, 6.0, 'Stage 1 spatial VAE (poses only)', size=10.5, bold=True, color=NSL) _, ex = flow(sl, 4.87, [ (1.18, 'Pose\n[T, 100]', 'io'), (1.30, 'per-frame\nencoder', 'main'), (1.58, 'Gaussian latent\nz[T, d] (μ, σ)', 'key'), (1.30, 'per-frame\ndecoder', 'main'), (1.18, 'Pose\n[T, 100]', 'io'), ], NSL, NSL_BG, gap=0.24) label(sl, ex + 0.16, 4.88, 13.15 - ex, 'NO temporal compression —\none latent per frame\nnear-lossless: ceiling 0.031', size=9.5, color=ACC) label(sl, 0.62, 5.70, 6.0, 'Stage 2 text → latents (frozen stage 1)', size=10.5, bold=True, color=NSL) _, ex = flow(sl, 6.02, [ (1.18, 'Gloss', 'io'), (1.30, 'text\nencoder', 'main'), (1.72, 'non-AR transformer\n+ length head', 'key'), (1.38, 'all latents\nat once', 'io'), (1.26, 'stage-1\ndecoder', 'main'), ], NSL, NSL_BG, gap=0.24) label(sl, ex + 0.16, 5.97, 13.15 - ex, 'every frame emitted in\nparallel; MSE regression —\nits optimum is a mean', size=9.5, color=MUTED) label(sl, 0.5, 7.02, 12.4, 'Numbers: DTW-MJE hands / stage-1 ceiling, Full_TriVis, 200 clips, 50 joints, ' 'per-clip shoulder-width.', size=9, color=MUTED) # ============================================================ slide 2: contrast sl2 = prs.slides.add_slide(blank) label(sl2, 0.5, 0.25, 12.4, 'Where the two designs differ — and what it costs', size=22, bold=True) rows = [ ('', 'T2M-GPT', 'NSLP-G'), ('Stage-1 representation', 'discrete: 512-entry VQ codebook', 'continuous: per-frame Gaussian latent'), ('Temporal compression', '4× (1 token / 4 frames)', 'none (1 latent / frame)'), ('Stage-1 ceiling (hands)', '0.098', '0.031 ← 3.2× better'), ('Stage-2 decoding', 'autoregressive + sampling', 'non-autoregressive, all at once'), ('Stage-2 loss', 'cross-entropy over 513 classes', 'MSE on continuous latents'), ('Duration', 'learned end-of-sequence token', 'explicit length head'), ('DTW hands (TriVis)', '0.5763', '0.4969 ← wins DTW'), ('FGD (TriVis)', '6.09 ← wins FGD', '7.30'), ('Ratio to own ceiling', '5.9× ← closer to its limit', '16.3×'), ('Wrong-text penalty (FGD)', '+60% ← reads the text', '+26%'), ] x0, y0, wl, w1, w2, hh = 0.6, 0.95, 3.5, 4.3, 4.3, 0.44 for i, (a, b, c) in enumerate(rows): y = y0 + i * hh head = (i == 0) for (xx, ww, txt, al) in ((x0, wl, a, PP_ALIGN.LEFT), (x0 + wl, w1, b, PP_ALIGN.CENTER), (x0 + wl + w1, w2, c, PP_ALIGN.CENTER)): fill = GREY_BG if head else RGBColor(0xFF, 0xFF, 0xFF) col = T2M if (head and txt == 'T2M-GPT') else NSL if (head and txt == 'NSLP-G') else INK s = box(sl2, xx, y, ww, hh, txt, fill, RULE, bold=head, size=11, shape=MSO_SHAPE.RECTANGLE, fg=col) s.text_frame.paragraphs[0].alignment = al label(sl2, 0.6, y0 + len(rows) * hh + 0.18, 12.2, 'Both stage-2 models are trained on the identical 19,253 TriVis clips; only the ' 'representation and the decoding differ.\nDTW picks NSLP-G, FGD picks T2M-GPT — ' 'and NSLP-G\'s shuffled-text score (0.5882) is worse than T2M-GPT\'s correct-text ' 'score (0.5763).', size=10.5, color=MUTED) import os os.makedirs(os.path.dirname(args.out) or '.', exist_ok=True) prs.save(args.out) print(f'wrote {args.out} ({len(prs.slides.__iter__.__self__._sldIdLst)} slides)') if __name__ == '__main__': main()