Download make_arch_pptx.py from Tri1/t2m-gpt-vsl-code: direct link, hf CLI and curl.
- Browser
- Download file 10.6 kB
-
https://huggingface.co/Tri1/t2m-gpt-vsl-code/resolve/main/make_arch_pptx.py
- Command line
-
hf download hf://Tri1/t2m-gpt-vsl-code/make_arch_pptx.py
-
curl -L -o make_arch_pptx.py https://huggingface.co/Tri1/t2m-gpt-vsl-code/resolve/main/make_arch_pptx.py
10.6 kB
| #!/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() | |