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:
@@ -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.
|
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
|
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
|
(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 |
|
| Video | Duur |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|||||||
@@ -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
|
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
|
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
|
neurale Microsoft-stemmen (nl-NL-FennaNeural standaard) via de Edge-leesfunctie.
|
||||||
stem via Azure Speech, zie README.
|
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):
|
Opbouw van het script (regels die met [ beginnen zijn koppen, de rest is de gesproken tekst):
|
||||||
# titel: ... titel op de eerste kaart
|
# 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
|
[slot] de slotkaart
|
||||||
Lege regels tussen de tekst zijn pauzes van een halve seconde.
|
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 pathlib import Path
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
@@ -162,6 +165,24 @@ async def spreek(tekst, stem, tempo, mp3):
|
|||||||
return woorden
|
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):
|
def _norm(w):
|
||||||
return re.sub(r"[^\w]", "", w.lower())
|
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%")
|
stem = meta.get("stem", "nl-NL-FennaNeural"); tempo = meta.get("tempo", "-5%")
|
||||||
naam = meta.get("uitvoer", script.stem.replace("video-script", "video"))
|
naam = meta.get("uitvoer", script.stem.replace("video-script", "video"))
|
||||||
pauze = 0.8 # seconden stilte na elk segment
|
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-"))
|
tmp = Path(tempfile.mkdtemp(prefix="video-"))
|
||||||
clips, cues, t0 = [], [], 0.0
|
clips, cues, t0 = [], [], 0.0
|
||||||
for n, s in enumerate(segs, 1):
|
for n, s in enumerate(segs, 1):
|
||||||
@@ -224,7 +246,10 @@ def main():
|
|||||||
else:
|
else:
|
||||||
delen = [d.strip() for d in s["arg"].split("|")]
|
delen = [d.strip() for d in s["arg"].split("|")]
|
||||||
kaart_beeld(map_ / "beelden" / delen[0], delen[1] if len(delen) > 1 else "", png)
|
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)
|
d = duur(mp3)
|
||||||
for a, b, z in cues_uit_woorden(s["spraak"], woorden, d):
|
for a, b, z in cues_uit_woorden(s["spraak"], woorden, d):
|
||||||
cues.append((t0 + a, t0 + min(b + 0.15, d), z))
|
cues.append((t0 + a, t0 + min(b + 0.15, d), z))
|
||||||
|
|||||||
Reference in New Issue
Block a user