| |
| """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) |
| T2M_BG = RGBColor(0xE8, 0xF0, 0xF8) |
| NSL = RGBColor(0x8E, 0x44, 0xAD) |
| NSL_BG = RGBColor(0xF3, 0xEB, 0xF7) |
| ACC = RGBColor(0xC0, 0x39, 0x2B) |
| 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 |
| 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] |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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() |
|
|