Communitygithub.com

Tanush-Gupta1812/py-shorts-studio

Erstellt, schreibt, animiert und rendert virale 60fps vertikale 2D-Vektor-Erklärvideos, YouTube Shorts und Reels mit PyCairo, Edge TTS Active-Word-Karaoke-Untertiteln, prozeduralen Soundeffekten und direkter FFmpeg-Pipe-Kodierung. Setzt nahtlose Audio- und visuelle Match-Cut-Loops für unendliche Bindung ein.

Was ist py-shorts-studio?

Py-Shorts-Studio ist ein leistungsstarkes Python-Framework zur Erstellung von vertikalen (1080x1920, 9:16) 60 FPS Bewegungsgrafik-Erklärvideos (Shorts). Es streamt PyCairo-Vektorframes direkt über stdin an FFmpeg, synchronisiert Voiceovers mit wortgenauen Karaoke-Untertiteln, synthetisiert benutzerdefinierte Soundeffekte und mischt Multi-Track-Audio – alles ohne Festplatten-E/A. Die Engine implementiert eine strikte „Sentence-Bridge“-Skriptregel und einen pixelidentischen visuellen Match-Cut, um einen nahtlosen Endlosschleifen-Effekt zu erzielen, der die Wiedergabebindung maximiert. Entwickler nutzen es, um auf jedem Thema (Geschichte, Technik, Wissenschaft, Wirtschaft, Technologie, Survival oder Rätsel) schnell virale Kurzvideos zu produzieren, indem sie ein 4-teiliges Skriptformat mit hoher Bindung, vordefinierte Animationsprimitive und ein autonomes Ausführungsworkflow befolgen.

Funktioniert mit~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Tanush-Gupta1812/py-shorts-studio/tree/HEAD/.agents/skills/py-shorts-studio

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Py-Shorts-Studio: Virale 2D-Vektor-Erklär-Engine

py-shorts-studio ist ein leistungsstarkes Python-Framework für die Erstellung vertikaler (1080x1920, 9:16), 60 FPS Bewegungsgrafik-Erklärvideos (Shorts). Es umgeht Festplatten-E/A, indem es PyCairo-Vektorframes direkt an FFmpeg stdin streamt, Voiceovers mit aktiven Wort-Karaoke-Untertiteln ausrichtet, benutzerdefinierte Soundeffekte synthetisiert und Multi-Track-Audio mischt.


1. Nahtloser Audio- + Visueller Match-Cut-Loop (Unendliche Bindung)

Um die durchschnittliche prozentuale Ansicht zu maximieren (>100% Loop-Bindung bei YouTube Shorts & Instagram Reels), muss jedes generierte Short sowohl eine Audio-Satzbrücke als auch einen pixelidentischen visuellen Match-Cut erzwingen.

A. Die Satzbrücken-Skriptregel

Wenn YouTube ein Short loopt, verbindet sich das Ende des Audios augenblicklich mit dem Anfang. Das Skript muss dies ausnutzen:

  1. Eröffnungs-Hook: MUSS sofort mit einer scharfen Warum / Wie / Was-Frage beginnen.
    • Beispiel: "Warum würde Iron Mans Anzug Tony Stark in drei Sekunden töten?"
    • Beispiel: "Warum zerfallen moderne Autobahnen in fünfzehn Jahren, während römische Straßen zweitausend halten?"
  2. Abschließende Einleitungsphrase: Der letzte Satz MUSS mit einer nachlaufenden, erwartungsvollen Einleitungsphrase enden, anstatt mit einem endgültigen Abschied oder einem Aufruf zum Abonnieren:
    • Beispiel: "...weshalb die Leute immer noch fragen,"
    • Beispiel: "...also, wenn dich das nächste Mal jemand fragt,"
    • Beispiel: "...und das erklärt,"
  3. Kontinuierliche grammatikalische Schleife: Wenn das Video von der nachlaufenden Phrase in die Eröffnungsfrage loopt, bildet es einen nahtlosen grammatikalischen Satz:

    "...weshalb die Leute immer noch fragen — Warum würde Iron Mans Anzug Tony Stark in drei Sekunden töten?"

  4. Keine nachlaufende Stille:
    • total_dauer = audio_dauer + 0.05 (NIEMALS 0.4s oder 0.75s nachlaufende Totenstille hinzufügen).
    • Jede Pause am Ende zerstört die Illusion einer Endlosschleife. Die Audiospule muss das erste Wort des Neustarts ohne wahrnehmbare Lücke treffen.

B. Pixelidentischer Frame-0-Match-Cut

Der allererste Frame ($t = 0.0\text{s}$) und der allerletzte Frame ($t = \text{total_dauer}$) müssen visuell identisch sein, damit der Betrachter nicht erkennen kann, wann sich das Video wiederholt.

Implementierungsmuster: draw_anchor_state

Jedes Projektskript muss eine gemeinsame draw_anchor_state(ctx, zoom=1.0, pan=(0, 0))-Funktion definieren, die das primäre „Helden“-Visuelle und das obere Banner enthält.

def draw_anchor_state(ctx: cairo.Context, zoom: float = 1.0, pan: tuple[float, float] = (0.0, 0.0)) -> None:
    """Rendert das master Helden-Diagramm und das obere Banner mit einem beliebigen Zoom/Pan-Zentrum.

    Wird sowohl von Szene 1 (Eröffnungs-Ease-Out-Zoom) als auch von der letzten Szene (Abschluss-Ease-In-Zoom) verwendet,
    um einen pixelidentischen Match-Cut über die Loop-Grenze hinweg zu gewährleisten.
    """
    ctx.save()
    # Zentrumsursprungstransformation (1080x1920 Mitte ist 540, 960)
    ctx.translate(540.0 + pan[0], 960.0 + pan[1])
    ctx.scale(zoom, zoom)
    ctx.translate(-540.0, -960.0)

    # 1. Hintergrund
    draw_dark_bg(ctx, 1080, 1920)

    # 2. Oberes Banner
    draw_top_banner(ctx, "HELDEN-THEMA-TITEL", "TECHNIK-ERKLÄRVIDEO", progress=1.0, theme="dark")

    # 3. Helden-Visuelles / Diagramm im Aktionsbereich
    # (Hier zentrales Diagramm, Querschnitt oder Apparatur zeichnen)
    ...
    ctx.restore()

Match-Cut-Zoom-Dynamik (0.0s – 1.2s & Letzte 1.2s)

  • Eröffnung ($t = 0.0\text{s} \to 1.2\text{s}$):
    • Startet bei $t = 0.0\text{s}$ stark hineingezoomt auf das Fokusdiagramm mit zoom = 1.45.
    • Über $0.0\text{s} \to 1.2\text{s}$ sanft herauszoomen zur Gesamtansicht (zoom = 1.0) mittels ease_out_cubic:
      if time_sec <= 1.2:
          zoom_t = ease_out_cubic(time_sec / 1.2)
          cur_zoom = lerp(1.45, 1.0, zoom_t)
          draw_anchor_state(ctx, zoom=cur_zoom)
      
  • Abschluss ($t = \text{total_dauer} - 1.2\text{s} \to \text{total_dauer}$):
    • Nahtloser Übergang zurück zu genau demselben Helden-Diagramm.
    • Über die letzten 1.2s hineinzoomen von zoom = 1.0 zurück zu zoom = 1.45 mittels ease_in_out_cubic:
      t_closing_start = total_dauer - 1.2
      if time_sec >= t_closing_start:
          zoom_t = ease_in_out_cubic((time_sec - t_closing_start) / 1.2)
          cur_zoom = lerp(1.0, 1.45, zoom_t)
          draw_anchor_state(ctx, zoom=cur_zoom)
      
  • Ergebnis: Frame bei $t = 0.0\text{s}$ (zoom = 1.45) und Frame bei $t = \text{total_dauer}$ (zoom = 1.45) sind pixelidentisch.

2. Die 4-teilige Hochbindungs-Skriptformel

Hochbindungs-Shorts folgen diesem strengen narrativen Bogen:

TeilTiming (45–55s Short)Timing (25–30s Short)Zweck & Ausführung
1. Paradox-Hook0.0s – 4.0s0.0s – 5.0sNenne einen krassen, kontraintuitiven Kontrast oder ein unmögliches Rätsel, beginnend mit Warum / Wie / Was. Verbanne Füllwörter, Einleitungen oder Begrüßungen.
2. Einschränkung / Eskalation4.0s – 20.0s5.0s – 12.0sEskaliere das Problem mit konkreten Zahlen, historischen Einsätzen oder Umweltgefahren. Führe vergleichende Metriken oder Status-HUD-Balken ein.
3. Der geniale Wendepunkt20.0s – 45.0s12.0s – 22.0sDas architektonische oder wissenschaftliche Geheimnis wird gelüftet. Zerlege die technischen Mechanismen mit animierten Diagrammen, Schnittansichten, Routen oder Komponentenschichten.
4. Auszahlung & NachlaufbrückeLetzte 5.0sLetzte 4.0sLiefere die „Aha!“-Auszahlung und ende mit einer nachlaufenden Satzbrücke, die nahtlos in den Eröffnungs-Hook zurückloopt, ohne nachlaufende Stille.
  • Sprechgeschwindigkeit: 150–170 Wörter pro Minute (WPM).
  • Zielwortanzahlen:
    • 45–55s Short: 110–145 Wörter.
    • 25–30s Short: 65–75 Wörter.

3. Visuelle & Layout-Regeln

  1. Leinwandspezifikationen:
    • Auflösung: 1080x1920 (9:16 vertikales Seitenverhältnis).
    • Bildrate: 60 FPS konstant.
    • Vektorgrafik-Präzision: Alle Formen, Symbole und Linien werden über PyCairo für scharfes Anti-Aliasing gezeichnet.
  2. Sichere Zonen (Entscheidend für mobile Benutzeroberflächen):
    • Obere sichere Zone ($y \ge 130$): Platziere Titel, Kapitel-Pills und obere Banner zwischen $y = 130$ und $y = 260$, um den Kanalnamen und obere UI-Overlays zu vermeiden.
    • Zentraler Aktionsbereich ($y = 280$ bis $y = 1420$): Alle Fokusgrafiken, Schnittansichten, Kartenrouten und Statistikabzeichen befinden sich hier.
    • Untertitel-Sicherheitszone unten: Untertitel werden bei $y = 1480$–$1540$ verankert. Halte Hintergrund-Fokuselemente oberhalb von $y = 1400$, um eine Überlappung mit dem Untertiteltext zu vermeiden. Vermeide wesentliche visuelle Details in den unteren 380px (wo sich Titel, Ton und Abonnieren-Buttons auf YouTube und TikTok befinden).
  3. Pacing-Regel (3–5 Sekunden Zustandsänderungen):
    • Die Aufmerksamkeit des Betrachters lässt schnell nach. Ändere den visuellen Zustand, den Kamerafokus oder löse eine Animation alle 3 bis 5 Sekunden aus (z. B. Abzeichen-Feder-Pop, HUD-Metrik-Füllung, Pfadfortschritt oder Schichtaufdeckung).
  4. Exaktes wort-synchrones Sounddesign:
    • Rate niemals SFX-Timings.
    • Führe zuerst generate_voiceover aus, um words.json zu erzeugen.
    • Überprüfe words.json auf wichtige Auslösewörter (z. B. Zahlen, Aktionsverben, Szenenwechsel) und platziere whoosh-, pop- und thud-Hinweise bei der genauen start_sec dieser Wörter.

4. Vollständige Engine-API-Referenz

Alle Module werden sauber aus engine exportiert:

from engine import (
    # Renderer
    VideoRenderer,
    # Easing-Mathematik
    clamp, lerp, ease_out_cubic, ease_in_out_cubic, spring_pop,
    cubic_bezier_point, cubic_bezier_tangent, cubic_bezier_angle,
    # 2D-Vektor-Primitive
    draw_parchment_bg, draw_dark_bg, draw_top_banner, draw_map_route,
    draw_hud_bars, draw_stat_badge, draw_rounded_rect, draw_card_shadow,
    # Aktive-Wort-Karaoke-Untertitel
    draw_karaoke_captions, chunk_words,
    # Audio-Synthese, Mischen & Multiplexen
    generate_voiceover, generate_voiceover_async,
    ensure_default_sfx, get_downloads_dir, mix_final_audio, mux_video_audio,
)

VideoRenderer (engine.renderer)

class VideoRenderer:
    def __init__(
        self,
        output_path: Union[str, Path],
        width: int = 1080,
        height: int = 1920,
        fps: int = 60,
        crf: int = 18,
        preset: str = "fast",
        audio_path: Optional[Union[str, Path]] = None,
        ffmpeg_bin: Optional[str] = None,
    ) -> None: ...

    def create_surface(self) -> Tuple[cairo.ImageSurface, cairo.Context]: ...
    def write_surface(self, surface: cairo.ImageSurface) -> None: ...
    def render(
        self,
        draw_fn: Callable[[cairo.Context, int, float], None],
        total_frames: Optional[int] = None,
        duration_sec: Optional[float] = None,
        on_progress: Optional[Callable[[int, int], None]] = None,
    ) -> Path: ...

Easing-Hilfsfunktionen (engine.easing)

def clamp(val: float, min_val: float = 0.0, max_val: float = 1.0) -> float: ...
def lerp(a: float, b: float, t: float) -> float: ...
def ease_out_cubic(t: float) -> float: ...
def ease_in_out_cubic(t: float) -> float: ...
def spring_pop(t: float, overshoot: float = 1.2) -> float: ...
def cubic_bezier_point(p0: Point2D | float, p1: Point2D | float, p2: Point2D | float, p3: Point2D | float, t: float) -> Tuple[float, float] | float: ...
def cubic_bezier_tangent(p0: Point2D, p1: Point2D, p2: Point2D, p3: Point2D, t: float) -> Tuple[float, float]: ...
def cubic_bezier_angle(p0: Point2D, p1: Point2D, p2: Point2D, p3: Point2D, t: float) -> float: ...

Wiederverwendbare Vektor-Primitive (engine.primitives)

def draw_parchment_bg(ctx: cairo.Context, width: int = 1080, height: int = 1920, seed: int = 42) -> None: ...
def draw_dark_bg(ctx: cairo.Context, width: int = 1080, height: int = 1920) -> None: ...
def draw_top_banner(ctx: cairo.Context, title: str, subtitle: Optional[str] = None, progress: float = 1.0, theme: str = "light") -> None: ...
def draw_map_route(ctx: cairo.Context, points: Sequence[Tuple[float, float]], progress: float, color: Tuple[float, float, float] = (0.86, 0.15, 0.15), line_width: float = 8.0, dashed: bool = True, show_arrow: bool = True, pulse_time: float = 0.0) -> None: ...
def draw_hud_bars(ctx: cairo.Context, metrics: List[Dict[str, Any]], x: float, y: float, width: float = 880.0, theme: str = "light") -> None: ...
def draw_stat_badge(ctx: cairo.Context, value_text: str, label_text: str, x: float, y: float, scale: float = 1.0, accent_color: Tuple[float, float, float] = (0.98, 0.80, 0.08), theme: str = "light") -> None: ...
def draw_rounded_rect(ctx: cairo.Context, x: float, y: float, w: float, h: float, r: float) -> None: ...
def draw_card_shadow(ctx: cairo.Context, x: float, y: float, w: float, h: float, r: float, blur_layers: int = 4, offset_y: float = 6.0, base_alpha: float = 0.12) -> None: ...

Aktive-Wort-Karaoke-Untertitel (engine.captions)

def chunk_words(words: Sequence[Dict[str, Any]], max_words: int = 3, max_chars: int = 18) -> List[Dict[str, Any]]: ...
def draw_karaoke_captions(ctx: cairo.Context, words: Sequence[Dict[str, Any]], current_time: float, center_x: float = 540.0, y: float = 1480.0, font_path: Optional[Union[str, Path]] = None, font_size: int = 76, stroke_width: int = 8, active_color: Tuple[int, int, int] = (250, 204, 21), inactive_color: Tuple[int, int, int] = (255, 255, 255), active_scale: float = 1.12) -> None: ...

Voiceover, Soundeffekte & Mischen (engine.tts_align, engine.audio_mixer)

def generate_voiceover(script_text: str, output_audio_path: Union[str, Path], output_words_json: Optional[Union[str, Path]] = None, voice: str = "en-US-AndrewNeural", rate: str = "+12%") -> float: ...
def ensure_default_sfx(sfx_dir: Union[str, Path] = "assets/sfx") -> Dict[str, Path]: ...
def get_downloads_dir() -> Path: ...
def mix_final_audio(voiceover_path: Union[str, Path], sfx_events: Sequence[Tuple[float, str]], output_audio_path: Union[str, Path], total_duration: float, include_ambient: bool = True, sfx_dir: Union[str, Path] = "assets/sfx", ffmpeg_bin: Optional[str] = None) -> Path: ...
def mux_video_audio(video_path: Union[str, Path], audio_path: Union[str, Path], final_output_path: Optional[Union[str, Path]] = None, filename: Optional[str] = None, ffmpeg_bin: Optional[str] = None) -> str: ...

5. Autonomer Ausführungsworkflow

Wenn du mit der Erstellung eines Shorts zu einem beliebigen Thema beauftragt wirst, führe diese Schritte nacheinander aus:

Schritt 1: Skriptkomposition (Satzbrücken-Regel)

Schreibe ein knappes 65–140 Wörter umfassendes Skript nach der 4-teiligen Formel.

  • MUSS mit einer scharfen Warum / Wie / Was-Frage beginnen.
  • MUSS mit einer nachlaufenden Satzbrücke enden ("...weshalb die Leute immer noch fragen,").
  • Wähle einen klaren Themenslug (z. B. colosseum_flood oder vibranium_physics).

Schritt 2: Voiceover & Zeitstempelgenerierung in scratch/

Halte output/ sauber, indem du alle Zwischendateien in scratch/ speicherst:

.venv\Scripts\python -c "from engine import generate_voiceover; generate_voiceover('''<skript>''', 'scratch/<slug>_vo.mp3', 'scratch/<slug>_words.json')"

Überprüfe scratch/<slug>_words.json, um Folgendes zu bestimmen:

  • Die genaue audio_dauer
  • Die Zeitstempel wichtiger Meilensteinwörter (z. B. wo der Hook endet, wo Zahlen gesprochen werden, wo der Wendepunkt und die letzte Brücke auftreten).

Schritt 3: Skriptgesteuerte Szenen-Pipeline-Erstellung

Erstelle projects/<slug>.py mit Importen aus engine:

  • Setze total_dauer = audio_dur + 0.05 (Keine nachlaufende Stille, um die sofortige Schleife beizubehalten).
  • Schreibe Zwischenvideo-/Audiodateien nach scratch/<slug>_silent.mp4 und scratch/<slug>_audio.aac.
  • Definiere draw_anchor_state(ctx, zoom, pan) für das Helden-Diagramm.
  • Verwende draw_anchor_state mit zoom = 1.45 -> 1.0 über ease_out_cubic für die Eröffnung $0.0\text{s} \to 1.2\text{s}$.
  • Verwende draw_anchor_state mit zoom = 1.0 -> 1.45 über ease_in_out_cubic für die letzten $1.2\text{s}$ ($t_{\text{close}} \to \text{total_dauer}$) und stelle so einen pixelidentischen Match-Cut mit Frame 0 sicher.
  • Definiere mittlere Szenenübergänge, die dem tatsächlichen Erzähltempo aus scratch/<slug>_words.json entsprechen.
  • Ordne SFX-Ereignisse [(t1, 'whoosh'), (t2, 'pop'), (t3, 'thud')] den genauen Wortzeitstempeln zu.
  • Implementiere draw_frame(ctx, frame_idx, time_sec) und rendere die Animation in scratch/<slug>_silent.mp4.

Schritt 4: Audio mischen & multiplexen

Führe diese Befehle aus:

# Soundeffekte mit dem Voiceover mischen
.venv\Scripts\python -c "from engine import mix_final_audio; mix_final_audio('scratch/<slug>_vo.mp3', sfx_events, 'scratch/<slug>_audio.aac', total_dauer)"

# Video und Audio multiplexen
.venv\Scripts\python -c "from engine import mux_video_audio; mux_video_audio('scratch/<slug>_silent.mp4', 'scratch/<slug>_audio.aac', filename='<slug>.mp4')"

Schritt 5: Im Verzeichnis output/ ablegen

Verschiebe das endgültige gerenderte Video nach output/<slug>.mp4.


Kurzreferenz: Typischer Projektskript-Boilerplate (projects/<slug>.py)

import math
from pathlib import Path
from engine import (
    VideoRenderer,
    clamp, lerp, ease_out_cubic, ease_in_out_cubic, spring_pop,
    draw_dark_bg, draw_top_banner, draw_hud_bars, draw_stat_badge,
    draw_karaoke_captions, chunk_words,
    generate_voiceover, mix_final_audio, mux_video_audio,
)

# ============================================================
# SCHRITT 1: Skript (4-teilige Formel + Satzbrücke)
# ============================================================
SCRIPT = """Warum würde ...? ... Weshalb die Leute immer noch fragen,"""
SLUG = "<slug>"

# ============================================================
# SCHRITT 2: Voiceover generieren & timestamp-ausgerichtete Metriken extrahieren
# ============================================================
audio_dur = generate_voiceover(SCRIPT, f"scratch/{SLUG}_vo.mp3", f"scratch/{SLUG}_words.json")
TOTAL_DUR = audio_dur + 0.05  # KEINE nachlaufende Stille!

# ============================================================
# SCHRITT 3: Helden-Ankerzustand (pixelidentischer Match-Cut)
# ============================================================
def draw_anchor_state(ctx, zoom=1.0, pan=(0.0, 0.0)):
    ctx.save()
    ctx.translate(540 + pan[0], 960 + pan[1])
    ctx.scale(zoom, zoom)
    ctx.translate(-540, -960)
    draw_dark_bg(ctx, 1080, 1920)
    draw_top_banner(ctx, "HELDEN-TITEL", "KATEGORIE", progress=1.0, theme="dark")
    # ... zeichne das Hauptdiagramm / die Hauptgrafik
    ctx.restore()

# ============================================================
# SCHRITT 4: Szenen-Rendering
# ============================================================
def draw_frame(ctx, frame_idx, time_sec):
    # MATCH-CUT-Eröffnungszoom
    if time_sec <= 1.2:
        zoom_t = ease_out_cubic(time_sec / 1.2)
        draw_anchor_state(ctx, zoom=lerp(1.45, 1.0, zoom_t))
        return

    # MATCH-CUT-Abschlusszoom
    t_start = TOTAL_DUR - 1.2
    if time_sec >= t_start:
        zoom_t = ease_in_out_cubic((time_sec - t_start) / 1.2)
        draw_anchor_state(ctx, zoom=lerp(1.0, 1.45, zoom_t))
        return

    # Mittlere Szenen ...
    draw_anchor_state(ctx, zoom=1.0)
    ...

    # Karaoke-Untertitel (immer aktiv)
    words = load_json(f"scratch/{SLUG}_words.json")
    draw_karaoke_captions(ctx, words, time_sec)

# ============================================================
# SCHRITT 5: Rendern & Multiplexen
# ============================================================
if __name__ == "__main__":
    renderer = VideoRenderer(
        output_path=f"scratch/{SLUG}_silent.mp4",
        width=1080, height=1920, fps=60, crf=18,
    )
    renderer.render(draw_frame, duration_sec=TOTAL_DUR)

    sfx_events = [(0.5, "whoosh"), (2.0, "pop"), (4.5, "thud")]
    mix_final_audio(f"scratch/{SLUG}_vo.mp3", sfx_events, f"scratch/{SLUG}_audio.aac", TOTAL_DUR)
    mux_video_audio(f"scratch/{SLUG}_silent.mp4", f"scratch/{SLUG}_audio.aac", filename=f"{SLUG}.mp4")
    print(f"✅ Short gerendert: output/{SLUG}.mp4")

Typischer .gitignore

output/
scratch/
assets/sfx/*.mp3
__pycache__/
*.pyc
.venv/

Warum das funktioniert

  • Loop-Mechanik: Zuschauer können nicht erkennen, wo das Video endet und wieder beginnt, was die durchschnittliche prozentuale Ansicht über 100% treibt (YouTube belohnt dies mit mehr Impressions).
  • PyCairo + FFmpeg stdin: Keine temporären PNGs auf der Festplatte. Jeder Frame ist ein Vektor, skaliert auf jede Auflösung, und wird mit GPU-freundlicher Hardwarekodierung direkt per Pipe an FFmpeg gesendet.
  • Wortgenaue Karaoke-Untertitel: Zuschauer lesen unbewusst mit, was die Bindung um ~30 % erhöht, selbst bei ausgeschaltetem Ton.
  • Edge TTS: Natürlich klingende neuronale Stimmen („Andrew Neural“) mit anpassbarer Geschwindigkeit (z. B. "+12%" für 150–170 Wörter pro Minute).

Verwandte Skills