ZeideI survived only to tell
Browse the Archives

Build assets — o pipeline completo (v0.2, decisão 33)

Build assets — o pipeline completo (v0.2, decisão 33)

This notebook is in Portuguese, as written during the research. Translation follows in stages, after the first published version.

File
site/docs/11-build-assets.md
Updated
2026-09-01
Terms of use
Written by the family with AI assistance, August–September 2026. The site's source of truth.

O COMO do 10-design-avancado.md. Todo asset derivado é gerado por comando reproduzível a partir das fontes (vídeo/áudio/scans/.md) — nada de asset editado à mão. Ferramentas: ffmpeg (já usado no projeto), Node (sharp, @hebcal/core, resvg) no script de build, Python 3 para utilitários curtos. Fontes dos comandos: vídeo 19971125 - Survivors of the Shoah - Mojsie Stobiecki.mp4 (768×576, 4:3, 25fps, 8.237s = 2h17min14s, raiz do projeto); áudio audio_raw.wav (regenerável — comando no BRIEFING §5).

1 · Visão do pipeline#

fontes .md do projeto ──┐
                        ├─► build/compile.mjs ──► dist/data/*.json  (§2)
transcricao/*.srt ──────┘
vídeo MP4 ──► ffmpeg ──► poster · fundos de capítulo · posters de evento · sprites+VTT  (§3)
audio_raw.wav ──► ffmpeg ──► chips A1–A12 (spec no 04) · waveform.json  (§4)
scans/fotos ──► sharp ──► AVIF/WebP/JPEG responsivos + LQIP  (§5)
templates ──► favicon (§6) · OG images (§7) · fio-da-vida.json (§8) · legendas VTT (§9)
           ──► calendario.ics (§10) · zeide-offline.zip (§11)

Ordem de execução e verificações finais: §12.

2 · Os JSONs (fonte da verdade = os .md; o site nunca lê .md direto)#

ArquivoFonteConteúdo
timeline.jsoncronologia/linha-do-tempo.mdeventos 2 níveis, capítulos (com bgFrame: timestamp do fundo §3.2), taxonomia, graus //, timestamps do vídeo
transcricao.jsontranscricao/01-verbatim.md + 02-revisada.md487 turnos, dois tracks alinhados por âncora, marcações [?] preservadas
catalogo.jsonCATALOGO-DOS-ACHADOS.mdD01D19, P01P11, H01H09, C01C06 com IDs estáveis (âncoras de URL)
pessoas.jsonfontes/dados-biograficos.mdpessoas, datas, fontes; flag living: true ⇒ só no build da área família
busca-index.jsontodos acimaíndice minisearch (B2)
fio-da-vida.jsontabela do 10 §A1ver §8

Regra de manutenção: correção factual entra primeiro no .md-fonte (hierarquia de autoridade do projeto) e o build recompila. A tabela do fio (10 §A1) é a única duplicação tolerada — o compilador imprime um aviso lembrando de conferi-la quando dados-biograficos.md mudar.

3 · Derivados do vídeo (ffmpeg)#

Definir V="19971125 - Survivors of the Shoah - Mojsie Stobiecki.mp4" (raiz).

3.1 Poster do player#

Frame nativo 768×576 (sem upscale — linha ética C1), momento a escolher no código (sugestão inicial: ~00:01:30, plano estável dele; validar a olho):

ffmpeg -ss 00:01:30 -i "$V" -frames:v 1 -vf "scale=768:576" -q:v 3 dist/media/poster.jpg

3.2 Fundos de capítulo (blur pesado — textura, não documento)#

Para cada capítulo, bgFrame vem do timeline.json. Upscale aqui é aceitável: o blur de sigma 40 destrói qualquer detalhe — é cor e luz da fita, não imagem:

ffmpeg -ss <bgFrame> -i "$V" -frames:v 1 \
  -vf "scale=1280:960:flags=lanczos,gblur=sigma=40,eq=saturation=0.55:brightness=-0.03" \
  -q:v 6 dist/media/cap-<id>.jpg

O duotone final é CSS por cima (10 §C1) — um asset serve os dois temas.

3.3 Posters de evento (hover do dot + OG de momento)#

Loop sobre eventos com timestamp no timeline.json:

ffmpeg -ss <t> -i "$V" -frames:v 1 -vf "scale=640:480" -q:v 4 dist/media/ev-<eventId>.jpg

3.4 Sprite de scrubbing + thumbnails.vtt#

824 miniaturas (1/10s), 160×120, paginadas 10×10 (9 sprites; a última página completa com preto, inofensivo). Duas etapas para ficar simples e auditável:

mkdir -p tmp/thumbs
ffmpeg -i "$V" -vf "fps=1/10,scale=160:120" -q:v 4 tmp/thumbs/f%04d.jpg
for p in 0 1 2 3 4 5 6 7 8; do
  ffmpeg -start_number $((p*100+1)) -i tmp/thumbs/f%04d.jpg -frames:v 100 \
    -vf tile=10x10 -q:v 5 dist/media/sprite-0$p.jpg
done

Gerador do VTT (build, Python inline):

# scripts/thumbs_vtt.py — WEBVTT com xywh, 1 cue/10s
import sys
n, out = 824, ["WEBVTT", ""]
def ts(s): return f"{s//3600:02d}:{s%3600//60:02d}:{s%60:02d}.000"
for i in range(n):
    page, k = i // 100, i % 100
    x, y = (k % 10) * 160, (k // 10) * 120
    out += [f"{ts(i*10)} --> {ts(min((i+1)*10, 8237))}",
            f"sprite-0{page}.jpg#xywh={x},{y},160,120", ""]
print("\n".join(out))

Sprites e VTT só carregam na primeira interação com a barra (10 §D-D).

4 · Áudio#

4.1 Chips A1–A12#

Spec completa (timestamps, fades, loudness) já aprovada em 04-curadoria-audio.md. Comando genérico do build (S/E/duração vêm do 04):

ffmpeg -ss <S> -to <E> -i audio_raw.wav \
  -af "afade=t=in:d=0.3,afade=t=out:st=<dur-0.3>:d=0.3,loudnorm=I=-16:TP=-1.5:LRA=11" \
  -ac 1 -c:a aac -b:a 96k dist/media/a<N>.m4a

4.2 waveform.json (2.000 picos, 0–1)#

ffmpeg -i audio_raw.wav -ac 1 -ar 8000 -f s16le - | python3 scripts/peaks.py 2000 > dist/data/waveform.json
# scripts/peaks.py — reduz PCM s16le a N picos normalizados
import sys, struct
n = int(sys.argv[1]); data = sys.stdin.buffer.read()
samples = struct.unpack(f"<{len(data)//2}h", data)
bucket = max(1, len(samples) // n)
peaks = [max(abs(s) for s in samples[i*bucket:(i+1)*bucket] or [0]) for i in range(n)]
top = max(peaks) or 1
print("[" + ",".join(f"{p/top:.3f}" for p in peaks) + "]")

Mini-ondas dos chips: fatia do mesmo array pelos timestamps (no front — sem asset extra).

5 · Imagens (scans e fotos) — script build/imagens.mjs (sharp)#

Para cada imagem publicada (respeitando a linha ética C1 — originais intocados no repo):

  1. Larguras 480/960/1600 (nunca acima do nativo) em AVIF (q50) + WebP (q75) + JPEG q80;
  2. LQIP: 24px de largura, JPEG q40, base64 inline no JSON da imagem (~300 bytes);
  3. width/height reais gravados no JSON (CLS 0);
  4. Saída: dist/media/img/<id>-<w>.<ext> + entrada em imagens.json. Esboço:
import sharp from "sharp";
const meta = await sharp(src).metadata();
for (const w of [480, 960, 1600].filter(w => w <= meta.width)) {
  await sharp(src).resize(w).avif({quality:50}).toFile(out(`avif`,w));
  await sharp(src).resize(w).webp({quality:75}).toFile(out(`webp`,w));
  await sharp(src).resize(w).jpeg({quality:80}).toFile(out(`jpg`,w));
}
const lqip = (await sharp(src).resize(24).jpeg({quality:40}).toBuffer()).toString("base64");

6 · Favicon — o "Z" (asset definitivo, materializar como dist/favicon.svg)#

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
  <style>
    rect { fill:#FAF6EF; } text { fill:#9A5B1F; }
    @media (prefers-color-scheme: dark) { rect { fill:#14120F; } text { fill:#D79A4E; } }
  </style>
  <rect width="64" height="64" rx="12"/>
  <text x="32" y="47" text-anchor="middle"
        font-family="'Source Serif 4', Georgia, serif" font-size="44"
        font-weight="600">Z</text>
</svg>
  • apple-touch-icon.png 180×180 (mesmo desenho, fundo claro fixo, gerado com resvg/sharp). O "Z" do header usa a mesma serif/peso — uma identidade só (10 §D-E).

7 · OG images (1200×630, uma por página/evento/pessoa/documento)#

Template SVG com 3 placeholders ({img} {titulo} {subtitulo}), renderizado a PNG no build (@resvg/resvg-js ou sharp). Layout: foto à esquerda (40%, com a vinheta), fundo #FAF6EF, título em Source Serif 4 600 (máx. 2 linhas, corte com reticências), rodapé fixo: "Zeide — eu sobrevivi só para contar" + o "Z". Páginas de momento (?t=) usam o poster de evento (§3.3) como {img}. Saída dist/media/og/<pageId>.png (~60–90 KB). Meta tags: og:title · og:description · og:image (+og:image:width/height) · og:locale (pt_BR/en_US) + twitter:card=summary_large_image. Fecha o B3 do backlog — o link no WhatsApp da família abre com a foto certa.

8 · fio-da-vida.json#

Materializa a tabela do 10 §A1 (formato lá definido). Gerado por build/fio.mjs de uma constante literal no próprio script — com um teste que confere 6 âncoras contra pessoas.json (nascimentos de Mojsie/Majer, †Frania, casamento 1950, †2000, contagem 12 em 1987) e falha o build em divergência (é o guarda da duplicação tolerada do §2).

9 · Legendas e transcrição sincronizada#

  • mojsie-1997.srt (1.237 blocos) → dist/media/legendas-pt.vtt: ffmpeg -i mojsie-1997.srt dist/media/legendas-pt.vtt (conversão direta).
  • legendas-en.vtt: quando a tradução EN dos blocos existir (decisão 22, por etapas).
  • thumbnails.vtt: §3.4.
  • A transcrição sincronizada da página Entrevista NÃO usa VTT — usa transcricao.json (turnos + âncoras), que preserva verbatim/revisada e as marcações [?].

10 · calendario.ics (B7)#

VCALENDAR gerado no build com @hebcal/core:

  • Yahrzeit: 20 Tishrei, convertido ano a ano 2026–2075 (VEVENTs individuais — datas hebraicas não têm RRULE gregoriana). Âncora de validação: 20 Tishrei 5787 = 01/10/2026; o build falha se a conversão não bater com a âncora.
  • Datas fixas com RRULE anual: 12/12 (nascimento, 1914) · 03/06 (casamento, 1950) · 06/12 (chegada ao Brasil — o Groix atraca em Santos, 1947) · 18/10 (falecimento, 2000).
  • Cada VEVENT com descrição de 1 linha + URL do momento correspondente no site.

11 · zeide-offline.zip (perenidade — 09)#

Build com BASE_URL=./ (caminhos relativos, navegável via file://). Duas variantes:

  • leve (~60 MB): site completo + imagens + áudios A1–A12 + poster — sem o MP4; o player offline mostra o poster com aviso "vídeo na cópia completa";
  • completa (~1,4 GB): idem + MP4 + sprites — a cópia "um por ramo da família" do 09. Gerar a cada release: zip -r zeide-offline-<leve|completa>-vN.zip dist-offline/.

12 · Ordem do build e verificações (o build FALHA se qualquer uma reprovar)#

  1. compile .md → JSONs (§2) → 2. fio (§8, com teste de âncoras) → 3. imagens (§5) →
  2. vídeo (§3, só se mudou — cache por mtime) → 5. áudio (§4, idem) → 6. favicon/OG (§6–7) →
  3. legendas (§9) → 8. ics (§10) → 9. HTML/CSS/JS → 10. verificações → 11. offline.zip (release).

Verificações obrigatórias:

  • Denylist: pesquisa/pedidos-prontos.md e qualquer conteúdo living: true ausentes do dist público (grep de nomes-sentinela; o build da área família é um dist separado);
  • Fórmula de Majdanek presente em toda página que afirme o destino da família (grep "segundo o próprio Mojsie");
  • Verbatim byte-idêntico ao 01-verbatim.md compilado (hash);
  • Toda <img>/<video> com width/height; nenhuma URL externa no caminho crítico;
  • JS por página < 30 KB gzip; Lighthouse CI 100×4 (budget do 10 §D-D);
  • Âncora do ics (20 Tishrei 5787 = 01/10/2026).