Script sync-campus: importa le guide del corso in campus-content/

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 15:30:59 +02:00
parent a7d918d241
commit 7baebebc09
220 changed files with 27569 additions and 1 deletions
+115
View File
@@ -0,0 +1,115 @@
// Parsing delle guide di studio (*.studio.md) del corso Biohacking Campus.
// Le intestazioni H1 sono eterogenee per tipo di guida: ogni regex qui sotto
// riconosce una famiglia e ne estrae titolo, relatore ed eventuale numero.
import { marked } from 'marked';
const H1_RE = /^#\s+Lezione\s+([0-9]+)\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
const ACCEL_RE = /^#\s+Accelerator\s+([0-9]+)\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
const LIVE_RE = /^#\s+Lezione\s+live\s+([0-9]+)\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
const SLIDE_RE = /^#\s+Slide\s+—\s+(.*?)\s+—\s+Guida/u;
const SPEAKER_RE = /^#\s+Intervento\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
const CONSUL_RE = /^#\s+Consulenza\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
const MASTER_RE = /^#\s+Masterclass\s+—\s+(.*?)\s+\(([^)]*)\)\s+—\s+Guida/u;
// I titoli Q&A sono eterogenei: cattura tutto fino a "— Guida allo studio".
const QA_RE = /^#\s+(.+?)\s+—\s+Guida allo studio\s*$/u;
const BOLD_RE = /\*\*(.+?)\*\*/u;
const PARTE_RE = /\((Parte\s+\d+)\)/u;
export function kindOf(slug) {
if (slug.startsWith('accelerator')) return 'accelerator';
if (slug.startsWith('lezione-live')) return 'live';
if (slug.startsWith('slide-')) return 'slide';
if (slug.startsWith('speaker-')) return 'speaker';
if (slug.startsWith('consulenza-')) return 'consulenza';
if (slug.startsWith('masterclass-')) return 'masterclass';
if (slug.startsWith('qa-')) return 'qa';
return 'lezione';
}
const LABEL_PREFIX = {
accelerator: 'A', live: 'LV', slide: 'SL', speaker: 'SP',
consulenza: 'C', masterclass: 'MC', qa: 'QA',
};
// Le immagini nei sorgenti puntano a "assets/..." relativo alla cartella del
// sito statico; nella sezione campus sono servite dalla route protetta.
export function rewriteAssets(html) {
return html.replaceAll('src="assets/', 'src="/campus/assets/');
}
function extractConcetti(bodyLines) {
const out = [];
let inSection = false;
for (const ln of bodyLines) {
if (ln.startsWith('## ')) {
inSection = ln.trim().toLowerCase().startsWith('## concetti chiave');
continue;
}
if (!inSection || !ln.trimStart().startsWith('- ')) continue;
const m = BOLD_RE.exec(ln);
if (m) out.push(m[1].trim().replace(/[.:;,]+$/, ''));
}
return out;
}
/**
* Parsa una guida.
* @param {string} raw contenuto del file .studio.md
* @param {string} slug nome file senza estensione
* @param {number} pos posizione della guida nel capitolo (1-based)
* @returns {{slug,kind,num,titleFull,titleShort,relatore,parte,label,concetti,html}}
*/
export function parseGuide(raw, slug, pos) {
const lines = raw.split('\n');
const h1 = lines[0];
const kind = kindOf(slug);
let num = pos, titleFull, titleShort, relatore, parte = '';
let m;
if (kind === 'qa' && (m = QA_RE.exec(h1))) {
titleFull = titleShort = m[1].trim();
relatore = 'Q&A del corso';
} else if (kind === 'masterclass' && (m = MASTER_RE.exec(h1))) {
titleFull = titleShort = m[1].trim();
relatore = m[2].trim();
} else if (kind === 'consulenza' && (m = CONSUL_RE.exec(h1))) {
titleFull = titleShort = m[1].trim();
relatore = m[2].trim();
} else if (kind === 'speaker' && (m = SPEAKER_RE.exec(h1))) {
titleFull = titleShort = m[1].trim();
relatore = m[2].trim();
} else if (kind === 'slide' && (m = SLIDE_RE.exec(h1))) {
titleFull = titleShort = m[1].trim();
relatore = 'Slide ufficiali del corso';
} else if (kind === 'live' && (m = LIVE_RE.exec(h1))) {
num = Number(m[1]);
const date = m[2].trim();
titleFull = `Lezione live ${num}${date}`;
titleShort = `Lezione live ${num}`;
relatore = `${m[3]} · ${date}`;
} else if (kind === 'accelerator' && (m = ACCEL_RE.exec(h1))) {
num = Number(m[1]);
const date = m[2].trim();
titleFull = `Accelerator ${num}${date}`;
titleShort = `Accelerator ${num}`;
relatore = `${m[3]} · ${date}`;
} else if (kind === 'lezione' && (m = H1_RE.exec(h1))) {
num = Number(m[1]);
titleFull = m[2];
relatore = m[3];
parte = PARTE_RE.exec(titleFull)?.[1] ?? '';
titleShort = titleFull.replace(/\s*\(Parte\s+\d+\)/u, '').trim();
} else {
throw new Error(`H1 non riconosciuto in ${slug}: ${JSON.stringify(h1)}`);
}
const bodyLines = lines.slice(1).filter((ln) => !ln.startsWith('> Sintesi di studio basata'));
const html = rewriteAssets(marked.parse(bodyLines.join('\n').trim(), { async: false }));
const label = kind === 'lezione'
? `L${num}${parte ? ` · ${parte}` : ''}`
: `${LABEL_PREFIX[kind]}${num}`;
return { slug, kind, num, titleFull, titleShort, relatore, parte, label, concetti: extractConcetti(bodyLines), html };
}
+197
View File
@@ -0,0 +1,197 @@
// Struttura del corso Biohacking Campus: capitoli, colori e macro-aree.
// Portata dal generatore statico del progetto sorgente (sito/genera_sito.py).
// Quando si aggiungono guide, inserirle nel capitolo corrispondente.
export const CHAPTERS = [
[1, 'Sonno e ritmi', 'Il fondamento: dormire e vivere in fase con la luce', [
'lezione-01-ottimizzazione-del-sonno',
]],
[2, 'Movimento, peso, proteine', 'Corpo in movimento e composizione corporea', [
'lezione-02-movimento-fitness-parte1',
'lezione-02-movimento-fitness-parte2',
'lezione-03-protocollo-fitness-longevita',
'lezione-04-peso-corporeo',
'lezione-05-proteine-creatina',
]],
[3, 'Freddo e respirazione', 'Stress ormetico e controllo del respiro', [
'lezione-06-il-freddo',
'lezione-07-la-respirazione',
'lezione-08-respirazione-pianificazione',
]],
[4, 'Mente e HRV', 'Allenamento mentale e variabilità cardiaca', [
'lezione-09-allenamento-mentale-parte1',
'lezione-09-allenamento-mentale-parte2',
'lezione-10-hrv',
]],
[5, 'Luce e recupero', 'Fotobiomodulazione, recupero attivo e dati', [
'lezione-11-luce-fotobiomodulazione-parte1',
'lezione-11-luce-fotobiomodulazione-parte2',
'lezione-12-recupero-attivo',
'lezione-13-monitoraggio-dati',
]],
[6, 'Consulenza e obiettivi', 'Dalla prima consulenza alla definizione degli obiettivi', [
'lezione-14-prima-consulenza-cliente',
'lezione-15-arte-obiettivo',
]],
[7, 'DNA, microbiota, epigenetica', 'Le basi molecolari della personalizzazione', [
'lezione-16-introduzione-dna',
'lezione-17-microbiota-metagenomica',
'lezione-18-epigenetica-ormoni',
]],
[8, 'Longevità: analisi e alimentazione', 'Interpretare le analisi e mangiare per durare', [
'lezione-19-analisi-longevita-parte1',
'lezione-19-analisi-longevita-parte2',
'lezione-20-alimentazione-longevita',
]],
[9, 'Performance cognitiva', 'Memoria, focus e nootropi', [
'lezione-21-performance-cognitiva-parte1',
'lezione-21-performance-cognitiva-parte2',
]],
[10, 'Funghi, integrazione, cervello', 'Micoterapia, integrazione e cervello sano', [
'lezione-22-funghi-terapeutici',
'lezione-23-integrazione-annuale-giornaliera',
'lezione-24-longevita-cervello-sano',
'lezione-25-focus-microbiota-plus',
]],
[11, 'Accelerator', 'Business e pratica professionale del biohacker (live mensili, Massimo Filippi)', [
'accelerator-1-2025-11-11',
'accelerator-2-2025-12-20',
'accelerator-3-2026-01-28',
'accelerator-4-2026-02-21',
'accelerator-5-2026-03-28',
'accelerator-6-2026-05-09',
'accelerator-7-2026-06-06',
]],
[12, 'Lezioni live', 'Incontri live di approfondimento (Massimo Filippi)', [
'lezione-live-1-2026-02-24',
'lezione-live-2-2026-03-31',
'lezione-live-3-2026-05-26',
'lezione-live-4-2026-06-30',
]],
[13, 'Slide ufficiali', 'Deck del Master Monitoraggio Biometrico e focus tematici', [
'slide-hrv',
'slide-dna',
'slide-microbiota-metagenomica',
'slide-epigenetica-ormoni',
'slide-nutrizione-precisione',
'slide-focus-ormoni',
'slide-focus-microbiota-plus',
]],
[14, 'Interventi speaker', 'Ospiti e focus tematici: genetica, ambiente, etica, consulenza, marketing, ormoni', [
'speaker-dna-focus-moduli',
'speaker-seipel-campi-elettromagnetici',
'speaker-delnero-etica-integrita',
'speaker-manfron-consulenza',
'speaker-resta-marketing',
'speaker-smeazzetto-focus-ormoni',
]],
[15, 'Consulenze', 'Casi studio reali: posizionamento e progetto professionale del biohacker (Massimo Filippi)', [
'consulenza-2025-02-04-cristina-ruggeri',
'consulenza-2025-02-10-umberto-riad',
'consulenza-2025-02-11-stefano-pignatelli',
'consulenza-2025-02-18-annalisa-di-lello',
'consulenza-2025-02-24-barbara-fila-manenti',
'consulenza-2025-02-27-eva-guerini',
'consulenza-2025-03-03-luca-patella',
'consulenza-2025-03-04-marco-gilberti',
'consulenza-2025-06-10-guglielmo-cornelli',
'consulenza-2025-06-16-liga-briviba',
'consulenza-2025-07-02-lucia-griglio',
'consulenza-2025-07-03-monia-gaudenzi',
'consulenza-2025-07-08-simone-holl',
'consulenza-2025-07-09-isabella-biagina-gioia',
]],
[16, 'Masterclass', 'Lezioni formative per professionisti: comunicazione e coaching (Luca Ruggeri), psicosomatica e benessere (Laura Pirotta)', [
'masterclass-2025-10-08-luca-ruggeri',
'masterclass-2025-11-08-laura-pirotta',
'masterclass-2025-11-12-luca-ruggeri',
'masterclass-2025-11-29-laura-pirotta',
'masterclass-2025-12-10-luca-ruggeri',
'masterclass-2026-01-10-laura-pirotta',
'masterclass-2026-01-14-luca-ruggeri',
'masterclass-2026-02-07-laura-pirotta',
'masterclass-2026-02-11-luca-ruggeri',
'masterclass-2026-03-07-laura-pirotta',
'masterclass-2026-03-25-luca-ruggeri',
'masterclass-2026-04-27-laura-pirotta',
'masterclass-2026-05-18-laura-pirotta',
'masterclass-2026-06-08-laura-pirotta',
'masterclass-2026-06-23-laura-pirotta',
]],
[17, 'Q&A', "L'appuntamento ricorrente del corso: risposte agli argomenti della University e casi studio reali (Tony Manfron, Massimo Filippi, Daniela Nuti)", [
'qa-01-tony-04',
'qa-02-tony-05',
'qa-03-tony-06',
'qa-04-tony-07',
'qa-05-tony-08',
'qa-06-tony-09',
'qa-07-tony-10',
'qa-08-tony-11',
'qa-09-tony-12',
'qa-10-tony-13',
'qa-11-tony-n14',
'qa-12-tony-n15-ketosi-e-sport',
'qa-13-massimo-27-02',
'qa-14-massimo-6-03',
'qa-15-simulazione-caso-studio-1-cliente-skinny-fat',
'qa-16-tony-n15-sonno-su-cliente-56-anni-e-lettura',
'qa-17-tony-n16-ottimizzazione-sonno-uomo-donna-ipe',
'qa-18-tony-n17-idratazione-sali-caffeina-teina',
'qa-19-tony-n-18-caso-calciatore-caso-perimenopausa',
'qa-20-tony-n19-quando-il-caso-non-e-di-nostra-comp',
'qa-21-simulazione-di-esame-caso-studio-calciatore',
'qa-22-tony-n20-domande-generiche-su-sonno-e-strate',
'qa-23-tony-n21-sirtuine-restrizione-calorica-caso',
'qa-24-tony-n21-uso-dei-dispositivi-di-monitoraggio',
'qa-25-tony-n22-organizzare-la-routine-del-cliente',
'qa-26-business-13-06-24-massimo-filippi',
'qa-27-daniela-nuti-sedentarieta-donna-e-ritenzione',
'qa-28-tony-n23-lettura-dati-wearable-sonno-come-im',
'qa-29-tony-n24-organizzare-un-ritiro-massaggi-d-i',
'qa-30-daniela-15-07-2024',
'qa-31-tony-n25-dispositivi-per-stress-concetti-sul',
'qa-32-tony-n26-biohacking-uomo-sportivo-con-protes',
'qa-33-tony-n27-perdita-di-peso-inestetismi-deontol',
'qa-34-tony-n28-come-organizzare-la-tua-attivita-a',
'qa-35-tony-n29-lavoro-approfondito-su-caso-studio',
]],
];
// Un tono per capitolo, indicizzato per numero di capitolo - 1.
export const CH_COLORS = [
'#6b8e6b', '#7a8b5a', '#8a8a4e', '#a08a56', '#a67c52',
'#9c6b58', '#8a6f7d', '#6f7d8a', '#5f8a82', '#6d8a6a',
'#5a6b8c', '#7c6b9c', '#8a7d5a', '#7a8a86', '#a67c8a',
'#5f8a7d', '#b07a4e',
];
// Macro-aree: raggruppano i capitoli nelle pagine indice della sezione.
export const AREAS = [
['corso-base', 'Corso base',
'Le 25 lezioni fondamentali del corso: sonno e ritmi, movimento e peso, freddo e respiro, mente e HRV, luce e recupero, consulenza, basi molecolari, longevità, performance cognitiva, integrazione.',
'#6b8e6b', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]],
['slide', 'Slide ufficiali',
'I deck del Master in Monitoraggio Biometrico e i focus tematici, con i grafici, le tabelle e i referti originali.',
'#8a7d5a', [13]],
['live-e-ospiti', 'Live e ospiti',
'Gli incontri live mensili (Accelerator), le lezioni live di approfondimento clinico e gli interventi degli ospiti del corso.',
'#7c6b9c', [11, 12, 14]],
['consulenze-masterclass', 'Consulenze e Masterclass',
'Casi studio reali di consulenza e le masterclass formative per professionisti: comunicazione, coaching, psicosomatica.',
'#a67c8a', [15, 16]],
['qa', 'Q&A',
"L'appuntamento ricorrente del corso: risposte agli argomenti della University e casi studio reali portati dagli studenti.",
'#b07a4e', [17]],
];
// Sottocartella del progetto sorgente in cui cercare il file <slug>.studio.md.
export function sourceDirFor(slug) {
if (slug.startsWith('accelerator')) return 'accelerator';
if (slug.startsWith('lezione-live')) return 'lezioni-live';
if (slug.startsWith('slide-')) return 'slide';
if (slug.startsWith('speaker-')) return 'speaker';
if (slug.startsWith('consulenza-')) return 'consulenze';
if (slug.startsWith('masterclass-')) return 'masterclass';
if (slug.startsWith('qa-')) return 'qa';
return 'studio';
}
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env node
/**
* sync-campus.mjs — importa le guide di studio del corso Biohacking Campus
* nella sezione riservata /campus del sito.
*
* COSA FA
* Legge i file `<slug>.studio.md` dal progetto sorgente, li converte in
* frammenti HTML e produce in `campus-content/`:
* - `nav.json` → aree, capitoli, guide (ordine, titoli, prev/next)
* - `pages/<slug>.html` → corpo della guida
* - `assets/` → immagini delle slide, copiate dal sorgente
* Le pagine sono renderizzate a runtime da src/pages/campus/ con lo stile
* del sito InsanityLab; qui non si genera alcun layout.
*
* USO
* npm run sync-campus [-- <path-progetto-biohacking-campus>]
* Default: /data/Sorgenti/AI-OS/projects/personale/biohacking-campus
*
* IDEMPOTENTE
* Rigenera nav.json e le pagine, rimuovendo i frammenti orfani. Gli assets
* sono copiati solo se mancanti o più vecchi del sorgente.
*/
import { readFile, writeFile, mkdir, readdir, rm, copyFile, stat } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join, dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { CHAPTERS, CH_COLORS, AREAS, sourceDirFor } from './campus/structure.mjs';
import { parseGuide } from './campus/parse.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const OUT_DIR = resolve(HERE, '..', 'campus-content');
const DEFAULT_SRC = '/data/Sorgenti/AI-OS/projects/personale/biohacking-campus';
async function copyTree(src, dest) {
await mkdir(dest, { recursive: true });
let copied = 0;
for (const entry of await readdir(src, { withFileTypes: true })) {
const from = join(src, entry.name);
const to = join(dest, entry.name);
if (entry.isDirectory()) {
copied += await copyTree(from, to);
continue;
}
const [s, d] = [await stat(from), existsSync(to) ? await stat(to) : null];
if (!d || d.mtimeMs < s.mtimeMs || d.size !== s.size) {
await copyFile(from, to);
copied++;
}
}
return copied;
}
async function main() {
const srcRoot = resolve(process.argv[2] ?? DEFAULT_SRC);
if (!existsSync(srcRoot)) {
console.error(`Progetto sorgente non trovato: ${srcRoot}`);
process.exit(1);
}
const pagesDir = join(OUT_DIR, 'pages');
await mkdir(pagesDir, { recursive: true });
// 1. parsing di tutte le guide, capitolo per capitolo
const chapters = [];
const written = new Set();
const flat = []; // ordine globale di lettura, per prev/next
for (const [num, title, sub, slugs] of CHAPTERS) {
const lessons = [];
for (const [i, slug] of slugs.entries()) {
const file = join(srcRoot, sourceDirFor(slug), `${slug}.studio.md`);
if (!existsSync(file)) {
console.warn(` guida mancante, salto: ${slug}`);
continue;
}
const g = parseGuide(await readFile(file, 'utf8'), slug, i + 1);
await writeFile(join(pagesDir, `${slug}.html`), g.html);
written.add(`${slug}.html`);
const entry = {
slug: g.slug, kind: g.kind, label: g.label, titleFull: g.titleFull,
titleShort: g.titleShort, relatore: g.relatore, concetti: g.concetti.slice(0, 6),
chapter: num,
};
lessons.push(entry);
flat.push(entry);
}
chapters.push({ num, title, sub, color: CH_COLORS[num - 1], lessons });
}
// 2. prev/next lungo l'ordine globale, e area di appartenenza
const areaOf = new Map();
for (const [slug, , , , chapterNums] of AREAS)
for (const n of chapterNums) areaOf.set(n, slug);
flat.forEach((l, i) => {
l.area = areaOf.get(l.chapter);
l.prev = flat[i - 1] ? { slug: flat[i - 1].slug, area: areaOf.get(flat[i - 1].chapter), title: flat[i - 1].titleShort } : null;
l.next = flat[i + 1] ? { slug: flat[i + 1].slug, area: areaOf.get(flat[i + 1].chapter), title: flat[i + 1].titleShort } : null;
});
const byNum = new Map(chapters.map((c) => [c.num, c]));
const areas = AREAS.map(([slug, title, sub, color, chapterNums]) => ({
slug, title, sub, color,
chapters: chapterNums.map((n) => byNum.get(n)).filter(Boolean),
}));
await writeFile(join(OUT_DIR, 'nav.json'), JSON.stringify({ areas, count: flat.length }, null, 2));
// 3. rimozione dei frammenti orfani
let removed = 0;
for (const f of await readdir(pagesDir)) {
if (!written.has(f)) { await rm(join(pagesDir, f)); removed++; }
}
// 4. assets delle slide
const assetsSrc = join(srcRoot, 'sito', 'assets');
const copied = existsSync(assetsSrc) ? await copyTree(assetsSrc, join(OUT_DIR, 'assets')) : 0;
console.log(`Campus: ${flat.length} guide, ${areas.length} aree, ${copied} asset copiati, ${removed} pagine orfane rimosse.`);
}
await main();