Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion screencast/lib/fixture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,6 @@ WHERE { <${url}> ?slot ?block . ?block a ?type . FILTER(?type IN (ldh:XHTML, ldh
export async function patchDocument({ ldh, certFile, certPassword, certPasswordFile, url, update }) {
const password = certPassword ?? (certPasswordFile ? (await fs.readFile(certPasswordFile, 'utf8')).trim() : null);
if (!password) throw new Error('patchDocument needs --cert-password or --cert-password-file');
const r = await run(ldh, ['patch', url, '-f', certFile, '-p', password], update);
const r = await run(ldh, ['patch', url, '-c', certFile, '-p', password], update);
return { ok: r.code === 0, out: String(r.err || r.out).split(password).join('••••').slice(0, 300) };
}
11 changes: 6 additions & 5 deletions screencast/overview/voice.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
// reproducible. The with-timestamps endpoint is used, so each take also keeps its
// character alignment for the subtitles.
//
// node overview/voice.mjs [--api-key-file ~/.config/elevenlabs/api-key] [--dry-run]
// node overview/voice.mjs [web-algebra/SCRIPT.md] [--api-key-file ~/.config/elevenlabs/api-key] [--dry-run]
//
// Writes tracks/overview/voice.json — id, text, spoken, file, seconds per line — and
// Another film's script can be named instead; its takes then go under tracks/<its folder>/.
// Writes tracks/<folder>/voice.json — id, text, spoken, file, seconds per line — and
// prints the lengths.

import fs from 'node:fs/promises';
Expand All @@ -23,15 +24,15 @@ import { createHash } from 'node:crypto';
import { TRACKS } from '../lib/harness.mjs';

const HERE = path.dirname(new URL(import.meta.url).pathname);
const SCRIPT = path.join(HERE, 'SCRIPT.md');
const OUT = path.join(TRACKS, 'overview');
const args = process.argv.slice(2);
const SCRIPT = path.resolve(args.find((a, i) => !a.startsWith('--') && args[i - 1] !== '--api-key-file') ?? path.join(HERE, 'SCRIPT.md'));
const OUT = path.join(TRACKS, path.basename(path.dirname(SCRIPT)));
const CACHE = path.join(OUT, 'voice');

const VOICE = 'onwK4e9ZLuTAKqWW03F9'; // Daniel
const MODEL = 'eleven_multilingual_v2';
const FORMAT = 'mp3_44100_128';

const args = process.argv.slice(2);
const flag = (name, fallback) => {
const i = args.indexOf(name);
return i === -1 ? fallback : args[i + 1];
Expand Down
7 changes: 4 additions & 3 deletions screencast/render/overview.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ const cfg = JSON.parse(await fs.readFile(cfgPath, 'utf8'));
const onlyArg = argv.indexOf('--only');
const only = onlyArg === -1 ? null : new Set(argv[onlyArg + 1].split(','));
const noAudio = argv.includes('--no-audio');
// --reuse keeps the clips already cut (tracks/.overview/<id>.mp4) and re-cuts only the ids
// --reuse keeps the clips already cut (tracks/.<output>/<id>.mp4) and re-cuts only the ids
// named after it, so a change to one shot does not re-encode the film's thirty.
const reuseArg = argv.indexOf('--reuse');
const reuse = reuseArg === -1 ? null : new Set((argv[reuseArg + 1] ?? '').startsWith('--') || !argv[reuseArg + 1] ? [] : argv[reuseArg + 1].split(','));
const at = (p) => path.resolve(ROOT, p);
const TRACKS = at('tracks');
const TMP = path.join(TRACKS, '.overview');
// each cut keeps its clips in its own folder, named after its output, so two cuts never share them
const TMP = path.join(TRACKS, `.${path.basename(cfg.output, '.mp4')}`);
await fs.mkdir(TMP, { recursive: true });

function run(cmd, args) {
Expand Down Expand Up @@ -137,7 +138,7 @@ async function cardHtml({ eyebrow, title, lede, links, note }, W, H) {
.pill.is-primary { background: #98ccff linear-gradient(180deg, rgba(255,255,255,0.22), rgba(0,0,0,0.10)); color: #003259; border-color: transparent; }
.note { font-family: 'Geist Mono', monospace; font-size: ${px(14)}; letter-spacing: 0.04em; text-transform: uppercase; color: #87929e; margin: ${px(36)} 0 0; }
</style><div class="card">
<div class="wordmark"><span class="mark"></span>LinkedDataHub</div>
<div class="wordmark"><span class="mark"></span>${esc(cfg.wordmark ?? 'LinkedDataHub')}</div>
${eyebrow ? `<p class="eyebrow">${esc(eyebrow)}</p>` : ''}
<h1>${esc(title)}</h1>
${lede ? `<p class="lede">${esc(lede)}</p>` : ''}
Expand Down
33 changes: 33 additions & 0 deletions screencast/scenes/probe-shacl-adm.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Not a scene. The admin Node shape form's target class lookup: which endpoint, which query, what comes back.
import { runScene, resolve, geometryFrom, sleep } from '../lib/harness.mjs';
const opts = await resolve('/');
const { identity, base } = opts;
const ADMIN = base.replace('://', '://admin.');
const identities = identity ? [...identity, { ...identity[0], origin: ADMIN }] : null;
const NS_DOC = `${ADMIN}/ontologies/namespace/`;
const sugg = (page) => page.locator('.ac-cb-panel[role=listbox]:visible li.ac-cb-item').evaluateAll((ls) => ls.map((l) => [l.textContent.replace(/\s+/g, ' ').trim(), l.title]));
await runScene({
id: 'probe-shacl-adm', target: NS_DOC, identity: identities,
geometry: geometryFrom(opts, { width: 1440, height: 900, deviceScaleFactor: 1 }),
async body({ page }) {
page.on('request', (r) => { const q = decodeURIComponent(r.url()) + (r.postData() ?? ''); if (/SELECT|DESCRIBE/.test(q) && /label|Label/.test(q)) console.log(' → ', r.method(), r.url().slice(0, 90), '|', q.replace(/\s+/g, ' ').slice(q.indexOf('SELECT'), q.indexOf('SELECT') + 400)); });
page.on('response', async (r) => { if (/sparql|\/ns\?/.test(r.url()) && r.request().method() !== 'OPTIONS') { try { const t = await r.text(); if (/label/i.test(decodeURIComponent(r.url()))) console.log(' ← ', r.status(), t.replace(/\s+/g, ' ').slice(0, 300)); } catch {} } });
await page.context().addCookies([{ name: 'LinkedDataHub.first-time-message', value: 'true', domain: new URL(ADMIN).hostname, path: '/' }]);
await page.goto(NS_DOC, { waitUntil: 'load' });
await page.waitForSelector('.ldh-pane.is-active .ldh-block', { timeout: 30_000 });
await sleep(2500);
await page.locator('.ldh-create-dock button.drop-toggle').first().click(); await sleep(1200);
await page.locator('.add-constructor:visible').filter({ hasText: /Node shape$/ }).first().click(); await sleep(3500);
const g = page.locator('form:visible .ldh-prop-group').filter({ hasText: /target class/i }).first();
const input = g.locator('input.resource-combobox:visible').first();
console.log(' input attrs:', await input.evaluate((i) => JSON.stringify({ name: i.name, cls: i.className, ds: { ...i.dataset }, list: i.getAttribute('list') })));
console.log(' group html:', (await g.evaluate((e) => e.outerHTML)).slice(0, 1500));
await input.click(); await input.pressSequentially('Person', { delay: 60 }); await sleep(8000);
console.log(' targetClass suggestions (Person):', JSON.stringify(await sugg(page)));
await input.fill(''); await input.pressSequentially('Namespace', { delay: 60 }); await sleep(8000);
console.log(' targetClass suggestions (Namespace):', JSON.stringify(await sugg(page)));
// a typed URI?
await input.fill(''); await input.pressSequentially('http://xmlns.com/foaf/0.1/Person', { delay: 20 }); await sleep(5000);
console.log(' after typing URI: suggestions', JSON.stringify(await sugg(page)), 'hidden ou:', await g.locator('input[name=ou]').evaluateAll((is) => is.map((i) => i.value)));
},
});
106 changes: 106 additions & 0 deletions screencast/scenes/probe-shacl-flow.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Not a scene. Dry run of the SHACL flow: before (no Person type), admin creates a property
// shape and a node shape for foaf:Person, clears the ontology, after (Person offered, form
// from the shape, empty save → violation).
import { runScene, resolve, geometryFrom, sleep } from '../lib/harness.mjs';
import { switchDocumentMode } from '../lib/blocks.mjs';
import { patchDocument } from '../lib/fixture.mjs';
const opts = await resolve('/');
const { identity, base } = opts;
const ADMIN = base.replace('://', '://admin.');
const identities = identity ? [...identity, { ...identity[0], origin: ADMIN }] : null;
const NS_DOC = `${ADMIN}/ontologies/namespace/`;
const plain = { click: async (l) => l.click() };
const sugg = (page) => page.locator('.ac-cb-panel[role=listbox]:visible li.ac-cb-item').evaluateAll((ls) => ls.map((l) => [l.textContent.replace(/\s+/g, ' ').trim(), l.title]));
const formOf = (page) => page.locator('form:visible').filter({ has: page.locator('.ldh-prop-group') }).last();
const dumpForm = async (page, what) => {
const groups = await formOf(page).locator('.ldh-prop-group').evaluateAll((gs) => gs.map((g) => ({
label: (g.querySelector('.pred')?.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 30),
inputs: [...g.querySelectorAll('input:not([type=hidden]), textarea, select')].map((i) => `${i.tagName.toLowerCase()}.${i.className.split(' ')[0]}#${i.name}=${(i.value ?? '').slice(0, 30)}`),
violation: g.className.includes('violation'), text: g.className.includes('violation') ? g.textContent.replace(/\s+/g, ' ').trim().slice(0, 160) : undefined,
})));
console.log(` ${what}:`, JSON.stringify(groups));
};
const group = (page, re) => formOf(page).locator('.ldh-prop-group').filter({ has: page.locator('.pred', { hasText: re }) }).first();
const typeIn = async (page, re, text) => { const i = group(page, re).locator('input:not([type=hidden]):visible, textarea:visible').first(); await i.click(); await i.fill(''); await i.pressSequentially(text, { delay: 15 }); };
const removeRow = async (page, re) => { const b = group(page, re).locator('button.btn-remove-property').first(); if (await b.count()) await b.click(); };
const save = async (page) => { await formOf(page).locator('button.btn-save').first().click(); await sleep(4000); };
const dockCreate = async (page, label) => {
await page.locator('.ldh-create-dock button.drop-toggle').first().click(); await sleep(1200);
await page.locator('.add-constructor:visible').filter({ hasText: new RegExp(`${label}$`) }).first().click(); await sleep(3500);
};
const RESET = `PREFIX sh: <http://www.w3.org/ns/shacl#>
DELETE { ?s ?p ?o } WHERE { ?s a ?t . FILTER(?t IN (sh:NodeShape, sh:PropertyShape)) ?s ?p ?o }`;
const instanceTypeInput = async (page) => { const g = formOf(page).locator('.ldh-prop-group').filter({ has: page.locator('input[name=pu][value$="#type"]') }).first(); const edit = g.locator('button.add-type-combobox').first(); if (await edit.count()) { await edit.click(); await sleep(800); } return g.locator('input:not([type=hidden]):visible').first(); };

const reset = await patchDocument({ ldh: opts.ldh, certFile: opts.certFile, certPassword: opts.certPassword, certPasswordFile: opts.certPasswordFile, url: NS_DOC, update: RESET });
console.log(' reset:', JSON.stringify(reset));
await runScene({
id: 'probe-shacl-flow', target: base + '/', identity: identities,
geometry: geometryFrom(opts, { width: 1440, height: 900, deviceScaleFactor: 1 }),
async body({ page }) {
page.on('response', async (r) => { if ([422, 400, 500].includes(r.status())) { try { console.log(' ←', r.status(), r.request().method(), r.url().slice(0, 80), (await r.text()).replace(/\s+/g, ' ').slice(0, 600)); } catch {} } });
await page.context().addCookies([{ name: 'LinkedDataHub.first-time-message', value: 'true', domain: new URL(ADMIN).hostname, path: '/' }]);

// ── before ──
await page.goto(base + '/', { waitUntil: 'load' });
await page.waitForSelector('.ldh-pane.is-active .ldh-block', { timeout: 30_000 }); await sleep(2000);
await switchDocumentMode(page, plain, 'read-mode'); await sleep(2500);
await dockCreate(page, 'Instance');
console.log(' Instance form html:', (await formOf(page).evaluate((f) => f.outerHTML)).slice(0, 2500));
const tin = await instanceTypeInput(page);
console.log(' type input:', await tin.count(), await tin.evaluate((i) => `${i.tagName}.${i.className}#${i.name}`).catch(() => null));
if (await tin.count()) { await tin.click(); await tin.pressSequentially('Person', { delay: 40 }); await sleep(6000); console.log(' BEFORE type suggestions (Person):', JSON.stringify(await sugg(page))); }

// ── admin: property shape ──
await page.goto(NS_DOC, { waitUntil: 'load' });
await page.waitForSelector('.ldh-pane.is-active .ldh-block', { timeout: 30_000 }); await sleep(2500);
await dockCreate(page, 'Property shape');
await dumpForm(page, 'Property shape form (fresh)');
await typeIn(page, /^Name$/, 'Name');
await typeIn(page, /^Path$/, 'http://xmlns.com/foaf/0.1/name'); await sleep(1500); await page.keyboard.press('Escape');
await typeIn(page, /^Min count$/, '1');
await typeIn(page, /^Datatype$/, 'http://www.w3.org/2001/XMLSchema#string'); await sleep(1500); await page.keyboard.press('Escape');
await removeRow(page, /^Max count$/);
await dumpForm(page, 'Property shape form (filled)');
await save(page);
console.log(' after PS save: forms', await page.locator('form:visible').filter({ has: page.locator('.ldh-prop-group') }).count(), 'blocks', await page.locator('.ldh-pane.is-active .ldh-block').count());
if (await page.locator('form:visible').filter({ has: page.locator('.ldh-prop-group') }).count()) await dumpForm(page, 'PS form still open');

// ── admin: node shape ──
await dockCreate(page, 'Node shape');
await typeIn(page, /^Label$/, 'Person shape');
await typeIn(page, /^Target class$/, 'http://xmlns.com/foaf/0.1/Person'); await sleep(1500); await page.keyboard.press('Escape');
const pin = group(page, /^Property$/).locator('input:not([type=hidden]):visible').first();
await pin.click(); await pin.pressSequentially('Name', { delay: 40 }); await sleep(6000);
const s = await sugg(page); console.log(' Property suggestions (Name):', JSON.stringify(s));
const hit = page.locator('.ac-cb-panel[role=listbox]:visible li.ac-cb-item').filter({ hasText: /Name/ }).first();
if (await hit.count()) await hit.click(); else console.log(' !! no property shape suggestion');
await sleep(800);
await dumpForm(page, 'Node shape form (filled)');
await save(page);
console.log(' after NS save: forms', await page.locator('form:visible').filter({ has: page.locator('.ldh-prop-group') }).count(), 'blocks', await page.locator('.ldh-pane.is-active .ldh-block').count());
if (await page.locator('form:visible').filter({ has: page.locator('.ldh-prop-group') }).count()) await dumpForm(page, 'NS form still open');
console.log(' blocks:', JSON.stringify(await page.locator('.ldh-pane.is-active .ldh-block').evaluateAll((bs) => bs.map((b) => b.textContent.replace(/\s+/g, ' ').trim().slice(0, 120)))));

// ── clear ──
const clear = page.locator('.ldh-pane.is-active form[action$="/clear"] button').first();
console.log(' clear button:', await clear.count());
if (await clear.count()) { await clear.click(); await page.waitForLoadState('load'); await sleep(3000); console.log(' after clear url:', page.url()); }

// ── after ──
await page.goto(base + '/', { waitUntil: 'load' });
await page.waitForSelector('.ldh-pane.is-active .ldh-block', { timeout: 30_000 }); await sleep(2000);
await switchDocumentMode(page, plain, 'read-mode'); await sleep(2500);
await dockCreate(page, 'Instance');
const tin2 = await instanceTypeInput(page);
await tin2.click(); await tin2.pressSequentially('Person', { delay: 40 }); await sleep(6000);
const s2 = await sugg(page); console.log(' AFTER type suggestions (Person):', JSON.stringify(s2));
const person = page.locator('.ac-cb-panel[role=listbox]:visible li.ac-cb-item[title="http://xmlns.com/foaf/0.1/Person"]').first();
if (await person.count()) {
await person.click(); await sleep(5000);
await dumpForm(page, 'Person form');
await save(page);
await dumpForm(page, 'after empty save');
} else console.log(' !! Person not offered');
},
});
Loading
Loading