feat(video): Azure Speech als stemroute voor publicatie (zelfde stem, sleutel uit de omgeving)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido van Dijk
2026-09-21 18:15:22 +02:00
parent 8d15be7649
commit f1167f07c2
2 changed files with 32 additions and 5 deletions
+3 -1
View File
@@ -94,7 +94,9 @@ ondertitels ingesloten en apart als `.srt`): de schermafbeeldingen uit de handle
door een Nederlandse computerstem, als lossere vertelling van dezelfde stappen en valkuilen.
De tekst staat in `video-script.md`; `_gereedschap/maak_video.py` bouwt de video opnieuw
(ffmpeg, Pillow, edge-tts). Wie de stem of het tempo wil veranderen, past de kopregels van het
script aan.
script aan. De stem is nl-NL-Fenna van Microsoft, standaard via de Edge-leesfunctie; met een
Azure Speech-sleutel in de omgeving (`AZURE_SPEECH_KEY`) gebruikt het script dezelfde stem via
Azure, de route voor publicatie buiten deze repository.
| Video | Duur |
| --- | --- |
+29 -4
View File
@@ -6,8 +6,10 @@ Gebruik: python3 ../_gereedschap/maak_video.py video-script.md
Uitvoer: <naam>.mp4 (1920x1080, H.264, AAC, met ingesloten ondertitels) en <naam>.srt
Vereist: ffmpeg, Pillow en het pakket edge-tts (pip install edge-tts). De stem komt van de
neurale Microsoft-stemmen (nl-NL-FennaNeural standaard); voor publicatie dezelfde
stem via Azure Speech, zie README.
neurale Microsoft-stemmen (nl-NL-FennaNeural standaard) via de Edge-leesfunctie.
Voor publicatie dezelfde stem via Azure Speech: zet AZURE_SPEECH_KEY (en eventueel
AZURE_SPEECH_REGION, standaard westeurope) in de omgeving; de sleutel wordt nergens
opgeslagen. Zonder sleutel gebruikt het script edge-tts.
Opbouw van het script (regels die met [ beginnen zijn koppen, de rest is de gesproken tekst):
# titel: ... titel op de eerste kaart
@@ -24,7 +26,8 @@ Opbouw van het script (regels die met [ beginnen zijn koppen, de rest is de gesp
[slot] de slotkaart
Lege regels tussen de tekst zijn pauzes van een halve seconde.
"""
import asyncio, json, re, subprocess, sys, tempfile
import asyncio, json, os, re, subprocess, sys, tempfile, urllib.request
from xml.sax.saxutils import escape
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
@@ -162,6 +165,24 @@ async def spreek(tekst, stem, tempo, mp3):
return woorden
def spreek_azure(tekst, stem, tempo, mp3):
"""Dezelfde stem via Azure Speech (REST). Geeft geen woordgrenzen; de ondertitels worden
dan naar tekstlengte verdeeld."""
key = os.environ["AZURE_SPEECH_KEY"]; regio = os.environ.get("AZURE_SPEECH_REGION", "westeurope")
taal = "-".join(stem.split("-")[:2])
alineas = "".join(f"<p>{escape(a.strip())}</p>" for a in tekst.split("\n\n") if a.strip())
ssml = (f'<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="{taal}">'
f'<voice name="{stem}"><prosody rate="{tempo}">{alineas}</prosody></voice></speak>')
req = urllib.request.Request(f"https://{regio}.tts.speech.microsoft.com/cognitiveservices/v1",
data=ssml.encode("utf-8"), method="POST",
headers={"Ocp-Apim-Subscription-Key": key, "Content-Type": "application/ssml+xml",
"X-Microsoft-OutputFormat": "audio-24khz-96kbitrate-mono-mp3",
"User-Agent": "maak_video"})
with urllib.request.urlopen(req, timeout=120) as r:
Path(mp3).write_bytes(r.read())
return []
def _norm(w):
return re.sub(r"[^\w]", "", w.lower())
@@ -210,6 +231,7 @@ def main():
stem = meta.get("stem", "nl-NL-FennaNeural"); tempo = meta.get("tempo", "-5%")
naam = meta.get("uitvoer", script.stem.replace("video-script", "video"))
pauze = 0.8 # seconden stilte na elk segment
print("stem via", "Azure Speech" if os.environ.get("AZURE_SPEECH_KEY") else "edge-tts", stem, tempo)
tmp = Path(tempfile.mkdtemp(prefix="video-"))
clips, cues, t0 = [], [], 0.0
for n, s in enumerate(segs, 1):
@@ -224,7 +246,10 @@ def main():
else:
delen = [d.strip() for d in s["arg"].split("|")]
kaart_beeld(map_ / "beelden" / delen[0], delen[1] if len(delen) > 1 else "", png)
woorden = asyncio.run(spreek(s["spraak"], stem, tempo, mp3))
if os.environ.get("AZURE_SPEECH_KEY"):
woorden = spreek_azure(s["spraak"], stem, tempo, mp3)
else:
woorden = asyncio.run(spreek(s["spraak"], stem, tempo, mp3))
d = duur(mp3)
for a, b, z in cues_uit_woorden(s["spraak"], woorden, d):
cues.append((t0 + a, t0 + min(b + 0.15, d), z))