Dify-handleidingen: eerste uitgave, stand 20 september 2026
Handleidingen 01, 02, 03, 05, 07, 08 met Zelf kiezen-kaders en Maak hem van jou; bijlagen ontwerpkaart, ontwerppatronen, AI-tools in het onderwijs; draaiboek workshop Kennisfestival 23-09-2026; gedeeld bouwscript, extract.py, CC BY-SA 4.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bouwt alle handleidingen en bijlagen in één keer: python3 _gereedschap/bouw_alles.py"""
|
||||
import runpy, sys
|
||||
from pathlib import Path
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
for build in sorted(root.glob("*/build.py")):
|
||||
print(f"== {build.parent.name}")
|
||||
sys.argv = [str(build)]
|
||||
runpy.run_path(str(build), run_name="__main__")
|
||||
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bouwt een Dify-handleiding (.html, .pdf, .docx) uit een markdown-bron.
|
||||
|
||||
Gebruik vanuit een handleidingmap: python3 build.py
|
||||
(build.py in die map roept dit script aan met de naam van de .md)
|
||||
|
||||
Vereist: python3-markdown, google-chrome, libreoffice (lokaal aanwezig).
|
||||
Het logo van AI Netwerk Limburg staat naast dit script en komt op de eerste
|
||||
pagina rechtsboven, in PDF, HTML en Word.
|
||||
"""
|
||||
import base64
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import markdown
|
||||
|
||||
TOOLS = Path(__file__).resolve().parent
|
||||
LOGO = TOOLS / "logo-ai-netwerk-limburg.png"
|
||||
|
||||
CSS = """
|
||||
@page { size: A4; margin: 2cm 2cm 2.2cm 2cm; }
|
||||
html { font-size: 11pt; }
|
||||
body { font-family: "Noto Sans", "DejaVu Sans", Arial, sans-serif; color: #1a1a1a;
|
||||
line-height: 1.45; max-width: 17cm; margin: 0 auto; }
|
||||
.kop { display: flex; align-items: flex-start; justify-content: space-between; gap: 1.5em;
|
||||
margin-bottom: 1em; }
|
||||
.kop-tekst { flex: 1 1 auto; }
|
||||
.kop img { flex: 0 0 auto; width: 3.4cm; height: auto; }
|
||||
h1 { font-size: 22pt; margin: 0 0 .3em; line-height: 1.2; }
|
||||
h2 { font-size: 15pt; margin: 1.6em 0 .5em; padding-top: .3em; border-top: 2px solid #1d4ed8;
|
||||
page-break-after: avoid; break-after: avoid; }
|
||||
h3 { font-size: 12pt; margin: 1.2em 0 .4em; page-break-after: avoid; break-after: avoid; }
|
||||
p { margin: .5em 0; }
|
||||
hr { border: 0; border-top: 1px solid #ddd; margin: 1.5em 0; }
|
||||
strong { font-weight: 600; }
|
||||
code { font-family: "DejaVu Sans Mono", Consolas, monospace; font-size: .92em;
|
||||
background: #f1f5f9; padding: 0 .25em; border-radius: 3px; }
|
||||
blockquote { margin: .8em 0; padding: .5em .9em; background: #fff7ed;
|
||||
border-left: 4px solid #f59e0b; page-break-inside: avoid; break-inside: avoid; }
|
||||
blockquote p { margin: .2em 0; }
|
||||
blockquote.kies { background: #ecfdf5; border-left-color: #10b981; }
|
||||
table { border-collapse: collapse; width: 100%; margin: .8em 0; font-size: .95em;
|
||||
page-break-inside: avoid; break-inside: avoid; }
|
||||
th, td { border: 1px solid #cbd5e1; padding: .35em .6em; text-align: left; vertical-align: top; }
|
||||
th { background: #eff6ff; }
|
||||
figure { margin: .9em 0 1.1em; page-break-inside: avoid; break-inside: avoid; }
|
||||
figure img { width: 100%; height: auto; border: 1px solid #cbd5e1; display: block; }
|
||||
figcaption { font-size: .88em; color: #555; margin-top: .3em; }
|
||||
ul, ol { margin: .4em 0 .4em 1.4em; padding: 0; }
|
||||
li { margin: .15em 0; }
|
||||
ol li { margin: .25em 0; }
|
||||
.step { page-break-inside: avoid; break-inside: avoid; }
|
||||
.typ { margin: .6em 0 .9em; border: 1px solid #94a3b8; border-radius: 4px; background: #f8fafc;
|
||||
page-break-inside: avoid; break-inside: avoid; }
|
||||
.typ-label { font-size: .75em; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
|
||||
color: #334155; background: #e2e8f0; padding: .15em .7em; }
|
||||
.schema { margin: .6em 0 .9em; padding: .5em .7em; border: 1px dashed #94a3b8; border-radius: 4px;
|
||||
background: #fff; overflow-x: auto; page-break-inside: avoid; break-inside: avoid; }
|
||||
.schema pre { margin: 0; white-space: pre; font-family: "DejaVu Sans Mono", Consolas, monospace; font-size: .8em; line-height: 1.3; }
|
||||
.typ pre { margin: 0; padding: .5em .7em; white-space: pre-wrap; word-wrap: break-word;
|
||||
font-family: "DejaVu Sans Mono", Consolas, monospace; font-size: .9em; }
|
||||
.voet { margin-top: 3em; padding-top: .6em; border-top: 1px solid #ddd; font-size: .85em; color: #555;
|
||||
display: flex; align-items: center; gap: .8em; }
|
||||
.voet img { width: 1.6cm; height: auto; }
|
||||
"""
|
||||
|
||||
|
||||
def read_source(src: Path) -> str:
|
||||
text = src.read_text(encoding="utf-8")
|
||||
# Het instructieblok voor Claude Code bovenin hoort niet in de uitgave.
|
||||
return re.sub(r"^\s*<!--.*?-->\s*", "", text, count=1, flags=re.S)
|
||||
|
||||
|
||||
def logo_src(embed: bool) -> str:
|
||||
if embed:
|
||||
return "data:image/png;base64," + base64.b64encode(LOGO.read_bytes()).decode()
|
||||
return LOGO.as_uri()
|
||||
|
||||
|
||||
COMPACT_CSS = """
|
||||
html { font-size: 9.5pt; } h1 { font-size: 18pt; } h2 { margin-top: 1em; }
|
||||
p { margin: .35em 0; } table { margin: .5em 0; } th, td { padding: .25em .5em; }
|
||||
.voet { margin-top: .6em; } .voet img { width: 1.1cm; } .kop img { width: 2.8cm; } ul, ol { margin: .3em 0 .3em 1.3em; }
|
||||
"""
|
||||
|
||||
|
||||
def to_html(md_text: str, here: Path, embed_images: bool, compact: bool = False) -> str:
|
||||
body = markdown.markdown(md_text, extensions=["tables", "sane_lists", "smarty", "fenced_code"])
|
||||
title = re.search(r"<h1>(.*?)</h1>", body).group(1)
|
||||
logo = logo_src(embed_images)
|
||||
|
||||
def figure(m: re.Match) -> str:
|
||||
alt, src = m.group(1), m.group(2)
|
||||
if embed_images:
|
||||
data = base64.b64encode((here / src).read_bytes()).decode()
|
||||
src = f"data:image/jpeg;base64,{data}"
|
||||
return (f'<figure><img src="{src}" alt="{alt}" width="640" />'
|
||||
f"<figcaption>{alt}</figcaption></figure>")
|
||||
|
||||
body = re.sub(r'<p><img alt="([^"]*)" src="([^"]+)"\s*/?></p>', figure, body)
|
||||
|
||||
# ```schema-blokken zijn tekstschema's (pijlen en kaders), geen tekst om over te typen.
|
||||
if embed_images:
|
||||
body = re.sub(r'<pre><code class="language-schema">(.*?)</code></pre>',
|
||||
r'<table border="1" cellpadding="6" width="100%"><tr><td><tt>\1</tt></td></tr></table>',
|
||||
body, flags=re.S)
|
||||
else:
|
||||
body = re.sub(r'<pre><code class="language-schema">(.*?)</code></pre>',
|
||||
r'<div class="schema"><pre>\1</pre></div>', body, flags=re.S)
|
||||
|
||||
# Codeblokken zijn tekst die letterlijk overgetypt wordt: geef ze een label.
|
||||
if embed_images:
|
||||
# LibreOffice negeert CSS-kaders bij HTML-import, maar respecteert tabelattributen.
|
||||
body = re.sub(r"<pre><code>(.*?)</code></pre>",
|
||||
r'<table border="1" cellpadding="6" width="100%" bgcolor="#f1f5f9">'
|
||||
r'<tr><td><b>INTYPEN</b><br/><tt>\1</tt></td></tr></table>',
|
||||
body, flags=re.S)
|
||||
body = body.replace("<table>", '<table border="1" cellpadding="4" width="100%">')
|
||||
else:
|
||||
body = re.sub(r"<pre><code>(.*?)</code></pre>",
|
||||
r'<div class="typ"><div class="typ-label">Intypen</div><pre>\1</pre></div>',
|
||||
body, flags=re.S)
|
||||
|
||||
# Kaders die beginnen met "Zelf kiezen" zijn groen (keuze), de rest oranje (valkuil).
|
||||
body = re.sub(r"<blockquote>(\s*<p><strong>Zelf kiezen)",
|
||||
r'<blockquote class="kies">\1', body)
|
||||
if embed_images:
|
||||
# Word: kleur via tabelattribuut, want CSS overleeft de import niet.
|
||||
body = re.sub(r'<blockquote class="kies">(.*?)</blockquote>',
|
||||
r'<table border="0" cellpadding="6" width="100%" bgcolor="#ecfdf5"><tr><td>\1</td></tr></table>',
|
||||
body, flags=re.S)
|
||||
body = re.sub(r"<blockquote>(.*?)</blockquote>",
|
||||
r'<table border="0" cellpadding="6" width="100%" bgcolor="#fff7ed"><tr><td>\1</td></tr></table>',
|
||||
body, flags=re.S)
|
||||
|
||||
# Instructiezin en het beeld erbij horen op dezelfde pagina.
|
||||
body = re.sub(r"(<p>(?:(?!</p>).)*</p>)\n(<figure>.*?</figure>)",
|
||||
r'<div class="step">\1\2</div>', body, flags=re.S)
|
||||
|
||||
# Kop: titel plus eerste alinea (ondertitel) links, logo rechts.
|
||||
if embed_images:
|
||||
# Word: tabel zonder randen, want flexbox overleeft de import niet.
|
||||
body = re.sub(
|
||||
r"^(<h1>.*?</h1>\s*<p>.*?</p>)",
|
||||
lambda m: (f'<table border="0" width="100%"><tr>'
|
||||
f'<td width="78%" valign="top">{m.group(1)}</td>'
|
||||
f'<td width="22%" valign="top" align="right">'
|
||||
f'<img src="{logo}" width="120" alt="AI Netwerk Limburg" /></td></tr></table>'),
|
||||
body, count=1, flags=re.S)
|
||||
else:
|
||||
body = re.sub(
|
||||
r"^(<h1>.*?</h1>\s*<p>.*?</p>)",
|
||||
lambda m: (f'<div class="kop"><div class="kop-tekst">{m.group(1)}</div>'
|
||||
f'<img src="{logo}" alt="AI Netwerk Limburg" /></div>'),
|
||||
body, count=1, flags=re.S)
|
||||
|
||||
voet = (f'<div class="voet"><img src="{logo}" alt="" />'
|
||||
f'<span>AI Netwerk Limburg · ai-netwerk-limburg.nl · {title}</span></div>')
|
||||
|
||||
return (f'<!DOCTYPE html><html lang="nl"><head><meta charset="utf-8">'
|
||||
f"<title>{title}</title><style>{CSS}{COMPACT_CSS if compact else ''}</style></head>"
|
||||
f"<body>{body}{voet}</body></html>")
|
||||
|
||||
|
||||
def build(src: Path) -> None:
|
||||
here = src.resolve().parent
|
||||
stem = src.stem
|
||||
md_text = read_source(src)
|
||||
# "STIJL: compact" in het instructieblok bovenin = kleinere letter, voor een losse A4.
|
||||
compact = "STIJL: compact" in src.read_text(encoding="utf-8")
|
||||
|
||||
html_path = here / f"{stem}.html"
|
||||
html_path.write_text(to_html(md_text, here, embed_images=False, compact=compact), encoding="utf-8")
|
||||
|
||||
# PDF via Chrome: gebruikt de CSS hierboven, inclusief paginaregels.
|
||||
subprocess.run([
|
||||
"google-chrome", "--headless=new", "--disable-gpu", "--no-pdf-header-footer",
|
||||
f"--print-to-pdf={here / (stem + '.pdf')}", html_path.as_uri(),
|
||||
], check=True, capture_output=True)
|
||||
|
||||
# DOCX via LibreOffice: beelden als data-URI zodat ze ingebed worden, niet gelinkt.
|
||||
tmp_html = here / f".{stem}-embed.html"
|
||||
tmp_html.write_text(to_html(md_text, here, embed_images=True, compact=compact), encoding="utf-8")
|
||||
subprocess.run([
|
||||
"soffice", "--headless", "--convert-to", "docx:MS Word 2007 XML",
|
||||
"--outdir", str(here), str(tmp_html),
|
||||
], check=True, capture_output=True)
|
||||
(here / f".{stem}-embed.docx").rename(here / f"{stem}.docx")
|
||||
tmp_html.unlink()
|
||||
|
||||
for ext in ("html", "pdf", "docx"):
|
||||
p = here / f"{stem}.{ext}"
|
||||
print(f"{p.name}: {p.stat().st_size // 1024} kB")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit("gebruik: build_handleiding.py <handleiding.md>")
|
||||
build(Path(sys.argv[1]))
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Haalt een screenshot uit het Claude Code-sessietranscript.
|
||||
Gebruik: extract.py <screenshot-id zoals ss_1923g3a1u> <doelpad.jpg>
|
||||
Let op: JSONL hieronder wijst naar de Claude Code-sessie van 20 september 2026;
|
||||
bij een nieuwe sessie het pad aanpassen."""
|
||||
import base64, json, sys
|
||||
from pathlib import Path
|
||||
|
||||
JSONL = Path("/home/guido/.claude/projects/-home-guido-projecten-lexconsultancybv-nl-server/f0263769-3318-4702-826c-1957526cfde0.jsonl")
|
||||
|
||||
def find(ss_id):
|
||||
for line in JSONL.read_text().splitlines():
|
||||
if ss_id not in line:
|
||||
continue
|
||||
rec = json.loads(line)
|
||||
content = rec.get("message", {}).get("content", [])
|
||||
for block in content if isinstance(content, list) else []:
|
||||
if block.get("type") != "tool_result":
|
||||
continue
|
||||
inner = block.get("content", [])
|
||||
if not isinstance(inner, list):
|
||||
continue
|
||||
# In een browser_batch staan meerdere screenshots in één resultaat:
|
||||
# neem het beeld dat direct volgt op de tekst met dit ID.
|
||||
for i, b in enumerate(inner):
|
||||
if b.get("type") == "text" and ss_id in b.get("text", ""):
|
||||
for nb in inner[i + 1:]:
|
||||
if nb.get("type") == "image":
|
||||
return base64.b64decode(nb["source"]["data"])
|
||||
if nb.get("type") == "text" and "ss_" in nb.get("text", ""):
|
||||
break
|
||||
return None
|
||||
|
||||
ss_id, dest = sys.argv[1], Path(sys.argv[2])
|
||||
data = find(ss_id)
|
||||
if not data:
|
||||
sys.exit(f"geen beeld gevonden voor {ss_id}")
|
||||
dest.write_bytes(data)
|
||||
print(f"{dest.name}: {len(data)//1024} kB")
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 392 KiB |
Reference in New Issue
Block a user