t2m-gpt-vsl-code / make_arch_pptx.py
Tri1's picture
T2M-GPT VSL adaptation: Python sources only (82 files, no checkpoints or data)
8e5456b verified
Raw
History Blame Contribute Delete
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()