200 lines
9.8 KiB
JavaScript
200 lines
9.8 KiB
JavaScript
|
|
import fs from 'node:fs'
|
|||
|
|
import { resolve } from 'node:path'
|
|||
|
|
import { parser } from '@slidev/cli'
|
|||
|
|
|
|||
|
|
const TOTAL_SECONDS = 30 * 60
|
|||
|
|
|
|||
|
|
const data = await parser.load(resolve('.'), resolve('slides.md'))
|
|||
|
|
const slides = data.slides.map((s, i) => ({
|
|||
|
|
n: i + 1,
|
|||
|
|
name: s.frontmatter?.name || null,
|
|||
|
|
raw: s.content || '',
|
|||
|
|
}))
|
|||
|
|
|
|||
|
|
function stripText(raw) {
|
|||
|
|
let t = raw
|
|||
|
|
t = t.replace(/<!--[\s\S]*?-->/g, ' ')
|
|||
|
|
t = t.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|||
|
|
t = t.replace(/```[\s\S]*?```/g, ' [code example] ')
|
|||
|
|
t = t.replace(/<[^>]+>/g, ' ')
|
|||
|
|
t = t.replace(/^\s*::.*::\s*$/gm, ' ')
|
|||
|
|
t = t.replace(/^\s*#\s*/gm, '')
|
|||
|
|
t = t.replace(/^\s*[-*]\s+/gm, '')
|
|||
|
|
t = t.replace(/^\s*\d+\.\s+/gm, '')
|
|||
|
|
t = t.replace(/\[(.*?)\]\((.*?)\)/g, '$1')
|
|||
|
|
t = t.replace(/[`*_~]/g, '')
|
|||
|
|
t = t.replace(/\|/g, ' ')
|
|||
|
|
t = t.replace(/\n{3,}/g, '\n\n')
|
|||
|
|
const lines = t.split(/\r?\n/).map(l => l.trim()).filter(Boolean)
|
|||
|
|
return lines
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function simplify(line) {
|
|||
|
|
let t = line
|
|||
|
|
const repl = [
|
|||
|
|
[/\binfrastructure\b/gi, 'infra'],
|
|||
|
|
[/\bconfiguration\b/gi, 'config'],
|
|||
|
|
[/\bvalidation\b/gi, 'check'],
|
|||
|
|
[/\breliability\b/gi, 'stable work'],
|
|||
|
|
[/\bdeclarative\b/gi, 'declared'],
|
|||
|
|
[/\borchestrator\b/gi, 'controller'],
|
|||
|
|
[/\bdistributed\b/gi, 'spread out'],
|
|||
|
|
[/\bparadigm\b/gi, 'approach'],
|
|||
|
|
[/\bpragmatism\b/gi, 'practical work'],
|
|||
|
|
[/\bguarantees\b/gi, 'safety rules'],
|
|||
|
|
[/\bprovisioning\b/gi, 'setup'],
|
|||
|
|
[/\bcompiler\b/gi, 'compiler check'],
|
|||
|
|
]
|
|||
|
|
for (const [re, to] of repl) t = t.replace(re, to)
|
|||
|
|
t = t.replace(/\bIaC\b/g, 'I A C').replace(/\bCI\/CD\b/g, 'C I slash C D').replace(/24×7×365/g, 'all day, every day')
|
|||
|
|
t = t.replace(/\s+—\s+/g, '. ').replace(/\s+->\s+/g, '. then ').replace(/,\s*/g, '. ')
|
|||
|
|
t = t.replace(/\s{2,}/g, ' ').trim()
|
|||
|
|
return t
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function say(line) {
|
|||
|
|
const rules = [
|
|||
|
|
[/\bInfrastructure\b/gi, 'Infrastructure (IN-fruh-STRUHK-cher)'],
|
|||
|
|
[/\bautomation\b/gi, 'automation (aw-tuh-MAY-shun)'],
|
|||
|
|
[/\bconfiguration\b/gi, 'configuration (kun-fig-yuh-RAY-shun)'],
|
|||
|
|
[/\bprovisioning\b/gi, 'provisioning (pruh-VIH-zhuh-ning)'],
|
|||
|
|
[/\bcompiler\b/gi, 'compiler (kum-PIE-ler)'],
|
|||
|
|
[/\breliability\b/gi, 'reliability (rih-lai-uh-BIH-luh-tee)'],
|
|||
|
|
[/\bvalidation\b/gi, 'validation (val-ih-DAY-shun)'],
|
|||
|
|
[/\bdeclarative\b/gi, 'declarative (dih-KLAR-uh-tiv)'],
|
|||
|
|
[/\borchestrator\b/gi, 'orchestrator (OR-kes-tray-ter)'],
|
|||
|
|
[/\bparadigm\b/gi, 'paradigm (PAIR-uh-dym)'],
|
|||
|
|
[/\bRust\b/g, 'Rust (rahst)'],
|
|||
|
|
[/\bSerde\b/g, 'Serde (SER-dee)'],
|
|||
|
|
[/\bOption\b/g, 'Option (OP-shun)'],
|
|||
|
|
[/\benum\b/gi, 'enum (EE-num)'],
|
|||
|
|
[/\bKubernetes\b/gi, 'Kubernetes (koo-ber-NET-eez)'],
|
|||
|
|
[/\bAnsible\b/gi, 'Ansible (AN-suh-buhl)'],
|
|||
|
|
[/\bTerraform\b/gi, 'Terraform (TEH-ruh-form)'],
|
|||
|
|
[/\bCI\/CD\b/g, 'CI/CD (see-eye / see-dee)'],
|
|||
|
|
[/\bIaC\b/g, 'IaC (eye-ay-see)'],
|
|||
|
|
]
|
|||
|
|
let t = line
|
|||
|
|
for (const [re, rep] of rules) t = t.replace(re, rep)
|
|||
|
|
t = t.replace(/\s+—\s+/g, ' / ').replace(/\.\s+/g, '. / ').replace(/:\s+/g, ': / ')
|
|||
|
|
return t
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function phase(n) {
|
|||
|
|
if (n <= 3) return ['Act 1 - Hook & Promise', 'Low -> Medium', 'Credibilidad y dolor real', 'Calma, pausas limpias']
|
|||
|
|
if (n <= 7) return ['Act 2 - Escalation', 'Medium -> High', 'Crecimiento de complejidad sin control', 'Ritmo creciente']
|
|||
|
|
if (n <= 14) return ['Act 3 - Problem Anatomy', 'High', 'Causa raiz y costo de fallar tarde', 'Didactico, frases cortas']
|
|||
|
|
if (n <= 18) return ['Act 4 - Turning Point', 'Peak', 'No faltan tools, falta paradigma', 'Silencios intencionales']
|
|||
|
|
if (n <= 23) return ['Act 5 - Resolution', 'High -> Medium', 'Types y compiler como respuesta', 'Claro y tecnico']
|
|||
|
|
if (n <= 31) return ['Act 6 - Proof in Production', 'Medium -> High', 'Casos reales e impacto operativo', 'Energetico y concreto']
|
|||
|
|
return ['Act 7 - Close & CTA', 'Medium -> Low', 'Cierre emocional con accion', 'Lento y memorable']
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const slideTexts = slides.map(s => ({ ...s, lines: stripText(s.raw) }))
|
|||
|
|
|
|||
|
|
// 1) reader-script-en.md
|
|||
|
|
let out = '# Rustikon 2026 - Slide Text (Reader Version)\n\n'
|
|||
|
|
out += 'One section per real Slidev slide (parser-based count).\n\n'
|
|||
|
|
for (const s of slideTexts) {
|
|||
|
|
out += `## Slide ${s.n}${s.name ? ` (name: ${s.name})` : ''}\n\n`
|
|||
|
|
const [story, tension, emphasis, delivery] = phase(s.n)
|
|||
|
|
out += `STORY: ${story}\nTENSION: ${tension}\nEMPHASIS: ${emphasis}\nDELIVERY: ${delivery}\n\n`
|
|||
|
|
if (!s.lines.length) out += '[No readable text]\n\n'
|
|||
|
|
else out += s.lines.join('\n') + '\n\n'
|
|||
|
|
}
|
|||
|
|
fs.writeFileSync('reader-script-en.md', out)
|
|||
|
|
|
|||
|
|
// 2) pronunciation
|
|||
|
|
out = '# Rustikon 2026 - Reader Script with Pronunciation Guide\n\n'
|
|||
|
|
out += 'Use this for practice. Read the SAY line.\n\n'
|
|||
|
|
for (const s of slideTexts) {
|
|||
|
|
out += `## Slide ${s.n}${s.name ? ` (name: ${s.name})` : ''}\n\n`
|
|||
|
|
const [story, tension, emphasis, delivery] = phase(s.n)
|
|||
|
|
out += `STORY: ${story}\nTENSION: ${tension}\nEMPHASIS: ${emphasis}\nDELIVERY: ${delivery}\n\n`
|
|||
|
|
if (!s.lines.length) {
|
|||
|
|
out += 'EN: [No readable text]\nSAY: [No readable text]\n\n'
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
for (const l of s.lines) out += `EN: ${l}\nSAY: ${say(l)}\n\n`
|
|||
|
|
}
|
|||
|
|
fs.writeFileSync('reader-script-en-pronunciation.md', out)
|
|||
|
|
|
|||
|
|
// 3) simple
|
|||
|
|
out = '# Rustikon 2026 - Simple Reader Script (Easy English)\n\n'
|
|||
|
|
out += 'Short and clear lines for easy pronunciation.\n\n'
|
|||
|
|
for (const s of slideTexts) {
|
|||
|
|
out += `## Slide ${s.n}${s.name ? ` (name: ${s.name})` : ''}\n\n`
|
|||
|
|
const [story, tension, emphasis, delivery] = phase(s.n)
|
|||
|
|
out += `STORY: ${story}\nTENSION: ${tension}\nEMPHASIS: ${emphasis}\nDELIVERY: ${delivery}\n\n`
|
|||
|
|
if (!s.lines.length) {
|
|||
|
|
out += '[No readable text]\n\n'
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
for (const l of s.lines) out += `${simplify(l)}\n\n`
|
|||
|
|
}
|
|||
|
|
fs.writeFileSync('reader-script-en-simple.md', out)
|
|||
|
|
|
|||
|
|
// 4) live
|
|||
|
|
out = '# Rustikon 2026 - Live Reader Script (Speech Only)\n\n'
|
|||
|
|
out += 'Only lines to speak. Legend: `/` short pause, `//` long pause.\n\n'
|
|||
|
|
for (const s of slideTexts) {
|
|||
|
|
out += `## Slide ${s.n}${s.name ? ` (name: ${s.name})` : ''}\n\n`
|
|||
|
|
const [story, tension, emphasis, delivery] = phase(s.n)
|
|||
|
|
out += `STORY: ${story}\nTENSION: ${tension}\nEMPHASIS: ${emphasis}\nDELIVERY: ${delivery}\n\n`
|
|||
|
|
const spoken = s.lines.map(simplify).filter(l => /[A-Za-z]/.test(l) && !/^[-=]+$/.test(l))
|
|||
|
|
if (!spoken.length) {
|
|||
|
|
out += '(No spoken text. Move forward.)\n\n'
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
for (const l of spoken) out += `${l.replace(/\.\s+/g, '. / ').replace(/\s+—\s+/g, ' / ')}\n\n`
|
|||
|
|
}
|
|||
|
|
fs.writeFileSync('reader-script-en-live.md', out)
|
|||
|
|
|
|||
|
|
// 5) live 30-min
|
|||
|
|
const weights = slideTexts.map(s => {
|
|||
|
|
const words = s.lines.join(' ').split(/\s+/).filter(Boolean).length
|
|||
|
|
return 6 + words
|
|||
|
|
})
|
|||
|
|
let secs = weights.map(w => Math.round((w / weights.reduce((a,b)=>a+b,0)) * TOTAL_SECONDS))
|
|||
|
|
const minSec=15,maxSec=90
|
|||
|
|
secs = secs.map(v => Math.max(minSec, Math.min(maxSec, v)))
|
|||
|
|
let diff = TOTAL_SECONDS - secs.reduce((a,b)=>a+b,0)
|
|||
|
|
while (diff !== 0) {
|
|||
|
|
let changed = false
|
|||
|
|
for (let i=0; i<secs.length && diff!==0; i++) {
|
|||
|
|
if (diff > 0 && secs[i] < maxSec) { secs[i]++; diff--; changed = true }
|
|||
|
|
else if (diff < 0 && secs[i] > minSec) { secs[i]--; diff++; changed = true }
|
|||
|
|
}
|
|||
|
|
if (!changed) break
|
|||
|
|
}
|
|||
|
|
const mmss = (t)=>`${String(Math.floor(t/60)).padStart(2,'0')}:${String(t%60).padStart(2,'0')}`
|
|||
|
|
out = '# Rustikon 2026 - Live Reader (30-Min Timing)\n\n'
|
|||
|
|
out += 'Total target: 30:00 (1800s)\n\n'
|
|||
|
|
out += 'How to use:\n- Keep your pace near each slide target.\n- If you are late, cut one example line and move on.\n- If you are early, add one short emphasis line.\n\n'
|
|||
|
|
let cum = 0
|
|||
|
|
for (let i=0;i<slideTexts.length;i++) {
|
|||
|
|
const s = slideTexts[i]
|
|||
|
|
cum += secs[i]
|
|||
|
|
out += `## Slide ${s.n}${s.name ? ` (name: ${s.name})` : ''} [target: ${secs[i]}s | cumulative: ${mmss(cum)}]\n\n`
|
|||
|
|
const [story, tension, emphasis, delivery] = phase(s.n)
|
|||
|
|
out += `STORY: ${story}\nTENSION: ${tension}\nEMPHASIS: ${emphasis}\nDELIVERY: ${delivery}\n\n`
|
|||
|
|
const spoken = s.lines.map(simplify).filter(l => /[A-Za-z]/.test(l) && !/^[-=]+$/.test(l))
|
|||
|
|
if (!spoken.length) out += '(No spoken text. Move forward.)\n\n'
|
|||
|
|
else for (const l of spoken) out += `${l.replace(/\.\s+/g, '. / ')}\n\n`
|
|||
|
|
}
|
|||
|
|
out += '## Checkpoints\n\n'
|
|||
|
|
for (const mark of [5,10,15,20,25,30]) {
|
|||
|
|
const target = mark * 60
|
|||
|
|
let c = 0, slideN = 1
|
|||
|
|
for (let i=0;i<secs.length;i++) { c += secs[i]; if (c >= target) { slideN = i + 1; break } }
|
|||
|
|
out += `- ${String(mark).padStart(2,'0')}:00 -> around Slide ${slideN}\n`
|
|||
|
|
}
|
|||
|
|
fs.writeFileSync('reader-script-en-live-30min.md', out)
|
|||
|
|
|
|||
|
|
// 6) key moments for 35 slides
|
|||
|
|
const key = `# Rustikon 2026 - Key Story Moments\n\nUse this as your minimal narrative map for a 30-minute talk.\n\n## Moment 1 - Hook (Slides 1-3)\nPurpose: Win attention and establish credibility.\n\nAnchor line:\n"I have lived this problem for decades, and I wanted one thing: to sleep."\n\n## Moment 2 - Rising Tension (Slides 4-7)\nPurpose: Show complexity growing faster than control.\n\nAnchor line:\n"We changed tools many times, but not the model."\n\n## Moment 3 - Crisis / Root Cause (Slides 8-14)\nPurpose: Make the cost of late failure undeniable.\n\nAnchor line:\n"Fail late is expensive. Fail early is cheap."\n\n## Moment 4 - Turning Point (Slides 15-18)\nPurpose: Reframe the problem.\n\nAnchor line:\n"The problem was not the tools. The problem was the paradigm."\n\n## Moment 5 - Proof in Production (Slides 24-31)\nPurpose: Show real operational evidence.\n\nAnchor line:\n"At 3 AM, the system responds first. Not me."\n\n## Moment 6 - Emotional Close + CTA (Slides 33-35)\nPurpose: Close with trust and clear action.\n\nAnchor line:\n"Rust gave me deterministic systems and better sleep."\n\n## One-Line Backup Version\n1. I lived this problem for decades.\n2. Complexity grew; certainty did not.\n3. Late failures are the real tax.\n4. Types changed the paradigm.\n5. Production proof: lower MTTR and deterministic workflows.\n6. Start small: model infrastructure as types.\n`
|
|||
|
|
fs.writeFileSync('key-moments-storytelling.md', key)
|
|||
|
|
|
|||
|
|
console.log('Real Slidev slides:', slideTexts.length)
|
|||
|
|
console.log('Docs regenerated with parser-based slide count.')
|