LeX-logo op titelpagina en in de voet van elk document

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido van Dijk
2026-09-20 21:46:31 +02:00
parent 25fca50bc4
commit b2e66335e0
26 changed files with 85 additions and 14 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Maakt van sectieoverleg-script.txt een testopname met drie verschillende computerstemmen,
zodat de sprekerherkenning (gpt-4o-transcribe-diarize) iets te onderscheiden heeft.
Gebruik: OPENAI_API_KEY=sk-... python3 maak_opname_drie_stemmen.py
Vereist: ffmpeg (aanwezig) en een OpenAI-sleutel in de omgevingsvariabele; de sleutel wordt
nergens opgeslagen. Kosten: circa 4.000 tekens bij tts-1, ongeveer 6 cent.
Uitvoer: sectieoverleg-drie-stemmen.mp3 (128 kbps, om te beluisteren)
sectieoverleg-drie-stemmen-klein.mp3 (32 kbps mono, onder de 5 MB-grens van de tool)
"""
import json
import os
import re
import subprocess
import sys
import tempfile
import urllib.request
from pathlib import Path
HERE = Path(__file__).resolve().parent
SCRIPT = HERE / "sectieoverleg-script.txt"
STEMMEN = {"Karin": "nova", "Bram": "echo", "Fatima": "alloy"} # drie duidelijk verschillende stemmen
VERTELLER = "onyx" # voor de openingsregel
key = os.environ.get("OPENAI_API_KEY")
if not key:
sys.exit("Zet de sleutel in de omgevingsvariabele OPENAI_API_KEY en start opnieuw.")
def spreek(tekst: str, stem: str, doel: Path) -> None:
req = urllib.request.Request(
"https://api.openai.com/v1/audio/speech",
data=json.dumps({"model": "tts-1", "voice": stem, "input": tekst, "response_format": "mp3"}).encode(),
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=120) as r:
doel.write_bytes(r.read())
regels = [r.strip() for r in SCRIPT.read_text(encoding="utf-8").splitlines() if r.strip()]
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
delen = []
for i, regel in enumerate(regels):
m = re.match(r"^(Karin|Bram|Fatima):\s*(.+)$", regel)
if m:
stem, tekst = STEMMEN[m.group(1)], m.group(2)
else:
stem, tekst = VERTELLER, regel
deel = tmp / f"{i:03d}.mp3"
spreek(tekst, stem, deel)
delen.append(deel)
print(f"{i + 1}/{len(regels)} {stem}: {tekst[:50]}...")
lijst = tmp / "lijst.txt"
lijst.write_text("".join(f"file '{d}'\n" for d in delen))
# Korte stilte tussen sprekers komt vanzelf door de pauze aan het eind van elke zin.
subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-f", "concat", "-safe", "0", "-i", str(lijst),
"-c:a", "libmp3lame", "-b:a", "128k", str(HERE / "sectieoverleg-drie-stemmen.mp3")], check=True)
subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", str(HERE / "sectieoverleg-drie-stemmen.mp3"),
"-ac", "1", "-ar", "16000", "-b:a", "32k", str(HERE / "sectieoverleg-drie-stemmen-klein.mp3")], check=True)
for naam in ("sectieoverleg-drie-stemmen.mp3", "sectieoverleg-drie-stemmen-klein.mp3"):
p = HERE / naam
print(f"{naam}: {p.stat().st_size // 1024} kB")
+3 -3
View File
@@ -38,10 +38,10 @@ de handleidingen vallen onder de MIT-licentie:
## Uitzonderingen
- Het logo van AI Netwerk Limburg (`_gereedschap/logo-ai-netwerk-limburg.png` en de kopie in
`00-presentatie/beelden/`) en het logo van LeX Consultancy (`00-presentatie/beelden/logo-lex*.png`)
zijn merken en vallen niet onder deze licentie. Wie de
`00-presentatie/beelden/`) en het logo van LeX Consultancy (`_gereedschap/logo-lex.png`,
`00-presentatie/beelden/logo-lex*.png`) zijn merken en vallen niet onder deze licentie. Wie de
handleidingen of dia's bewerkt, vervangt het logo door het eigen logo of laat het weg
(`build_handleiding.py` en `maak_dias.py` verwachten een PNG op dat pad).
(`build_handleiding.py` en `maak_dias.py` verwachten PNG's op die paden).
- `08-chatflow-studiecoach/stercollectie-voedselweb-hv3.pdf` en de kopie in `06-inclusief-lesmateriaal/`
zijn van VO-content (Stercollecties, https://maken.wikiwijs.nl/157495), CC BY-SA 4.0. De bronvermelding
van VO-content blijft staan.
+2 -2
View File
@@ -73,5 +73,5 @@ De PDF's staan naast de bronnen in git; HTML en Word maak je zelf met build.py.
Opmaak in de bronnen: genummerde lijst = handelingen, tabel "Wat je intypt" = veldwaarden,
codeblok = letterlijk overtypen (label "Intypen"), oranje kader = valkuil, groen kader = "Zelf kiezen"
(blockquote die met **Zelf kiezen** begint). Opmaak en logo staan in `_gereedschap/build_handleiding.py`;
vervang het logo door dat van je eigen school of netwerk.
(blockquote die met **Zelf kiezen** begint). Opmaak en logo's staan in `_gereedschap/build_handleiding.py`;
vervang de logo's door die van je eigen school of netwerk.
+16 -9
View File
@@ -18,6 +18,7 @@ import markdown
TOOLS = Path(__file__).resolve().parent
LOGO = TOOLS / "logo-ai-netwerk-limburg.png"
LOGO_LEX = TOOLS / "logo-lex.png" # kleiner, naast het LAI-logo
# Licentie en contact, komen als colofon onderaan elk document (HTML, PDF en Word).
EIGENAAR = "Guido van Dijk, LeX Consultancy B.V. en AI Netwerk Limburg"
@@ -33,7 +34,9 @@ body { font-family: "Noto Sans", "DejaVu Sans", Arial, sans-serif; color: #1a1a1
.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; }
.kop .logos { flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: .3em; }
.kop img { width: 3.4cm; height: auto; }
.kop img.lex { width: 2cm; }
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; }
@@ -73,6 +76,7 @@ ol li { margin: .25em 0; }
.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; }
.voet img.lex { width: 1.1cm; }
"""
@@ -85,16 +89,16 @@ def read_source(src: Path) -> str:
return re.sub(r"(\n> [^\n]*\n)\n(?=> )", r"\1\n<!-- -->\n\n", text)
def logo_src(embed: bool) -> str:
def logo_src(embed: bool, pad: Path = LOGO) -> str:
if embed:
return "data:image/png;base64," + base64.b64encode(LOGO.read_bytes()).decode()
return LOGO.as_uri()
return "data:image/png;base64," + base64.b64encode(pad.read_bytes()).decode()
return pad.as_uri()
COMPACT_CSS = """
html { font-size: 9.2pt; } h1 { font-size: 17pt; } h2 { margin-top: 1em; } @page { margin: 1.5cm 2cm 1.5cm 2cm; }
p { margin: .35em 0; } table { margin: .5em 0; } th, td { padding: .25em .5em; }
.colofon { margin-top: .5em; padding: .3em .5em; font-size: .75em; border: 0; border-top: 1px solid #ddd; border-radius: 0; background: none; } .voet { margin-top: .6em; } .voet img { width: 1.1cm; } .kop img { width: 2.8cm; } ul, ol { margin: .3em 0 .3em 1.3em; }
.colofon { margin-top: .5em; padding: .3em .5em; font-size: .75em; border: 0; border-top: 1px solid #ddd; border-radius: 0; background: none; } .voet { margin-top: .6em; } .voet img { width: 1.1cm; } .voet img.lex { width: .8cm; } .kop img { width: 2.8cm; } .kop img.lex { width: 1.6cm; } ul, ol { margin: .3em 0 .3em 1.3em; }
"""
@@ -102,6 +106,7 @@ def to_html(md_text: str, here: Path, embed_images: bool, compact: bool = False)
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)
logo_lex = logo_src(embed_images, LOGO_LEX)
def figure(m: re.Match) -> str:
alt, src = m.group(1), m.group(2)
@@ -159,13 +164,15 @@ def to_html(md_text: str, here: Path, embed_images: bool, compact: bool = False)
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>'),
f'<img src="{logo}" width="120" alt="AI Netwerk Limburg" /><br />'
f'<img src="{logo_lex}" width="70" alt="LeX Consultancy" /></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>'),
f'<div class="logos"><img src="{logo}" alt="AI Netwerk Limburg" />'
f'<img src="{logo_lex}" class="lex" alt="LeX Consultancy" /></div></div>'),
body, count=1, flags=re.S)
colofon = (
@@ -188,8 +195,8 @@ def to_html(md_text: str, here: Path, embed_images: bool, compact: bool = False)
return (f'<!DOCTYPE html><html lang="nl"><head><meta charset="utf-8">'
f"<title>{title}</title><style>{CSS}{COMPACT_CSS}</style></head><body>{body}</body></html>")
voet = (f'<div class="voet"><img src="{logo}" alt="" />'
f'<span>AI Netwerk Limburg · ai-netwerk-limburg.nl · {title}</span></div>')
voet = (f'<div class="voet"><img src="{logo}" alt="" /><img src="{logo_lex}" class="lex" alt="" />'
f'<span>AI Netwerk Limburg · LeX Consultancy B.V. · {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>"
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB