diff --git a/.gitignore b/.gitignore index fb3d872..299bdfe 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,10 @@ Thumbs.db # Zwischenspeicher des Paketbaus und sein Ergebnis .cache/ dist/ + +# LaTeX-Zwischendateien +docs/handbuch/*.aux +docs/handbuch/*.log +docs/handbuch/*.out +docs/handbuch/*.toc +docs/handbuch/*.png diff --git a/docs/handbuch/bauen.ps1 b/docs/handbuch/bauen.ps1 new file mode 100644 index 0000000..e3c0fca --- /dev/null +++ b/docs/handbuch/bauen.ps1 @@ -0,0 +1,15 @@ +# Baut das Handbuch nach handbuch.pdf. +# +# Braucht MiKTeX oder TeX Live mit xelatex. Zweimal, damit das +# Inhaltsverzeichnis stimmt. + +$ErrorActionPreference = 'Stop' +Set-Location $PSScriptRoot + +foreach ($lauf in 1..2) { + xelatex -interaction=nonstopmode handbuch.tex | Out-Null + if ($LASTEXITCODE -ne 0) { throw "xelatex ist in Durchlauf $lauf gescheitert. Siehe handbuch.log" } +} + +Remove-Item handbuch.aux, handbuch.log, handbuch.out, handbuch.toc -ErrorAction SilentlyContinue +Write-Host "handbuch.pdf gebaut ($([math]::Round((Get-Item handbuch.pdf).Length/1KB)) KB)" diff --git a/docs/handbuch/handbuch.pdf b/docs/handbuch/handbuch.pdf new file mode 100644 index 0000000..f155422 Binary files /dev/null and b/docs/handbuch/handbuch.pdf differ diff --git a/docs/handbuch/handbuch.tex b/docs/handbuch/handbuch.tex new file mode 100644 index 0000000..cd669c7 --- /dev/null +++ b/docs/handbuch/handbuch.tex @@ -0,0 +1,449 @@ +\documentclass[11pt,a4paper]{article} + +\usepackage[ngerman]{babel} +\usepackage{fontspec} +\usepackage[a4paper,margin=2.4cm,headsep=14pt]{geometry} +\usepackage{xcolor} +\usepackage{titlesec} +\usepackage{enumitem} +\usepackage{booktabs} +\usepackage{tabularx} +\usepackage{fancyhdr} +\usepackage{fancyvrb} +\usepackage{mdframed} +\usepackage[hidelinks]{hyperref} + +\setmainfont{Segoe UI} +\setmonofont{Consolas}[Scale=0.92] + +\definecolor{lila}{HTML}{6B3FA0} +\definecolor{grau}{HTML}{555555} +\definecolor{hellgrau}{HTML}{F2F2F4} +\definecolor{rahmen}{HTML}{D8D8DE} + +\titleformat{\section}{\Large\bfseries\color{lila}}{\thesection}{0.7em}{} +\titleformat{\subsection}{\large\bfseries}{\thesubsection}{0.6em}{} +\titlespacing*{\section}{0pt}{20pt}{8pt} +\titlespacing*{\subsection}{0pt}{14pt}{5pt} + +\pagestyle{fancy} +\fancyhf{} +\fancyhead[L]{\small\color{grau}abdulive-toolkit} +\fancyhead[R]{\small\color{grau}\leftmark} +\fancyfoot[C]{\small\color{grau}\thepage} +\renewcommand{\headrulewidth}{0.4pt} +\renewcommand{\headrule}{\color{rahmen}\hrule height 0.4pt} +\renewcommand{\sectionmark}[1]{\markboth{#1}{}} + +% Keine Einrueckung, dafuer Abstand zwischen den Absaetzen. Sonst wirkt jeder +% Absatz nach einer Liste oder Tabelle wie ein neuer Einzug. +\setlength{\parindent}{0pt} +\setlength{\parskip}{7pt} +\renewcommand{\arraystretch}{1.15} + +\setlist[itemize]{leftmargin=1.3em,itemsep=2pt,topsep=4pt,parsep=0pt} +\setlist[enumerate]{leftmargin=1.6em,itemsep=3pt,topsep=4pt,parsep=0pt} + +% Ohne commandchars: Backslashes in Windows-Pfaden bleiben woertlich. Mit +% commandchars wuerde aus \textbackslash ein Backslash plus Leerzeichen, +% und ein kopierter Pfad waere kaputt. +\DefineVerbatimEnvironment{code}{Verbatim} + {fontsize=\small,xleftmargin=8pt,frame=leftline,framerule=1.6pt, + rulecolor=\color{rahmen},framesep=8pt} + +% Nur fuer Bloecke, die eine Einfaerbung brauchen. +\DefineVerbatimEnvironment{codef}{Verbatim} + {fontsize=\small,xleftmargin=8pt,frame=leftline,framerule=1.6pt, + rulecolor=\color{rahmen},framesep=8pt,commandchars=\\\{\}} + +\newmdenv[backgroundcolor=hellgrau,linecolor=rahmen,linewidth=0.6pt, + innertopmargin=8pt,innerbottommargin=8pt,innerleftmargin=10pt, + innerrightmargin=10pt,skipabove=10pt,skipbelow=10pt,roundcorner=2pt]{kasten} + +\newcommand{\pfad}[1]{\texttt{\small #1}} + +\begin{document} + +\thispagestyle{empty} +\vspace*{2.5cm} +{\Huge\bfseries\color{lila} abdulive-toolkit} + +\vspace{6pt} +{\Large Handbuch} + +\vspace{28pt} +{\large Aus einem Zuruf im Chat wird ein fertiges Hochformat-Video.} + +\vspace{40pt} +\begin{tabularx}{\textwidth}{@{}lX@{}} +\toprule +Version & 0.5.4 \\ +Stand & 9. September 2026 \\ +Gilt für & Windows 10 und 11, 64 Bit \\ +Quelltext & \url{https://git.zeasy.dev/ZeasyAlex/abdulive} \\ +\bottomrule +\end{tabularx} + +\vfill +\noindent\small\color{grau} +Das Toolkit speichert Chatverläufe. Die Datenbank enthält damit +personenbezogene Daten und gehört weder ins Repository noch in eine +Cloud ohne Absprache. +\newpage + +\tableofcontents +\newpage + +\section{Was das Werkzeug tut} + +Jemand tippt \texttt{!clip} in den Chat. Etwa eine Minute später liegt ein +fertiges 9:16-Video mit Untertiteln auf der Platte, bereit für TikTok, Reels +oder Shorts. + +Dazwischen passiert Folgendes: + +\begin{enumerate} + \item Das Toolkit liest den Chat mit und erkennt den Befehl. + \item Es weist OBS an, den Wiedergabepuffer zu speichern. OBS hat die + letzten 90 Sekunden ohnehin im Speicher, der Moment ist also schon + vorbei und trotzdem noch da. + \item OBS meldet den Dateipfad zurück. Das Toolkit wartet, bis die Datei + fertig geschrieben ist. + \item Aus den letzten 45 Sekunden wird ein Hochformat gebaut: Facecam oben, + Gameplay unten, beides ohne Verzerrung zugeschnitten. + \item Die Mikrofonspur wird transkribiert und als Untertitel ins Bild + gebrannt. +\end{enumerate} + +\begin{kasten} +\textbf{Während des Streams wird nicht gerendert.} Das Encodieren nimmt +Rechenzeit, die der Stream braucht. Die Warteschlange arbeitet erst nach +Streamende. Wer das anders will, setzt \texttt{renderWhileLive: true}. +\end{kasten} + +\subsection{Was dabei herauskommt} + +Je Clip entsteht ein Ordner mit: + +\begin{tabularx}{\textwidth}{@{}lX@{}} +\toprule +\pfad{final.mp4} & das fertige Video mit eingebrannten Untertiteln \\ +\pfad{vertical.mp4} & dasselbe ohne Untertitel \\ +\pfad{thumb.jpg} & Vorschaubild \\ +\pfad{subs.srt} & Untertitel als Textdatei, praktisch für die Beschreibung \\ +\pfad{transcript.txt} & reiner Text des Gesagten \\ +\bottomrule +\end{tabularx} + +\subsection{Was es noch nicht tut} + +Es lädt nichts hoch. Es entscheidet nicht, welcher Clip gut ist. Die +Sichtungsseite, auf der man Clips durchgeht und aussortiert, ist noch nicht +gebaut. + +\section{Installation} + +Vorausgesetzt wird Windows 10 oder 11 in 64 Bit und OBS Studio 30 oder neuer. +Sonst nichts. Node, ffmpeg, whisper.cpp und die Schriften sind im Paket +enthalten. + +In PowerShell: + +\begin{code} +irm https://git.zeasy.dev/ZeasyAlex/abdulive/raw/branch/main/scripts/install.ps1 | iex +\end{code} + +Das Skript lädt das Paket, prüft die Prüfsumme, installiert, holt das +Sprachmodell (etwa 465 MB), startet den Dienst und öffnet die +Einrichtungsseite im Browser. + +Es braucht keine Administratorrechte. Beim ersten Start warnt Windows +SmartScreen vor einem unbekannten Herausgeber, weil das Paket nicht signiert +ist. Einmal auf \emph{Weitere Informationen}, dann \emph{Trotzdem ausführen}. + +\subsection{Wohin es installiert} + +\begin{tabularx}{\textwidth}{@{}lX@{}} +\toprule +Programm & \pfad{\%LOCALAPPDATA\%\textbackslash Programs\textbackslash abdulive} \\ +Daten & \pfad{\%LOCALAPPDATA\%\textbackslash abdulive} \\ +Autostart & Aufgabe \texttt{abdulive}, bei jeder Anmeldung \\ +Updates & Aufgabe \texttt{abdulive-update}, täglich 5:00 Uhr \\ +\bottomrule +\end{tabularx} + +Programm und Daten liegen getrennt. Ein Update tauscht den Programmordner +komplett aus, die Datenbank bleibt davon unberührt. + +\section{Einrichtung} + +Die Einrichtungsseite liegt unter \url{http://localhost:4792/setup} und ist +nur vom eigenen Rechner erreichbar. Sie zeigt Zugangsdaten im Klartext, also +nicht während des Streams öffnen. + +\subsection{Schritt 1: Zwei Twitch-Anwendungen anlegen} + +Unter \url{https://dev.twitch.tv/console/apps} zwei Anwendungen erstellen. Bei +beiden als OAuth-Redirect-URL exakt eintragen: + +\begin{code} +http://localhost:4792/callback +\end{code} + +Die Anwendungen hängen nicht am Kanal. Sie können auch unter einem anderen +Konto angelegt werden. + +\subsection{Schritt 2: Broadcaster-Konto verbinden} + +Client-ID und Secret der ersten Anwendung auf der Einrichtungsseite eintragen, +speichern, dann auf \emph{Mit Twitch verbinden} klicken. + +\begin{kasten} +\textbf{Das kann nur der Streamer selbst.} Twitch bindet die Berechtigungen an +das Konto, das sich anmeldet. Es gibt keine Rolle, mit der jemand anderes das +übernehmen kann. Moderator und Redakteur helfen hier nicht. +\end{kasten} + +Damit bekommt das Toolkit Zugriff auf Clips, Titel und Kategorie, Abonnenten, +Follower, Raids, Kanalpunkte und Bits. + +\subsection{Schritt 3: Bot-Konto verbinden} + +Dasselbe mit der zweiten Anwendung, angemeldet als Bot-Konto. Das ist ein +eigenes, kostenloses Twitch-Konto, damit Antworten im Chat als Bot erkennbar +sind. + +Ohne dieses Konto liest das Toolkit den Chat trotzdem mit. Es kann nur nicht +antworten. + +\begin{kasten} +\textbf{Das Bot-Konto sollte Moderator im Kanal sein.} Der Streamer tippt +dafür einmal \texttt{/mod botname} in seinen Chat. Ohne Moderatorrechte gilt +ein Limit von 20 Nachrichten je 30 Sekunden statt 100, und im +Follower-only-Modus kommt der Bot gar nicht durch. +\end{kasten} + +\subsection{Schritt 4: OBS-Passwort} + +In OBS unter \emph{Werkzeuge} $\rightarrow$ \emph{WebSocket-Servereinstellungen} +den Server aktivieren, Port 4455. Das dort angezeigte Passwort auf der +Einrichtungsseite eintragen. + +\subsection{Fertig?} + +Unten auf der Seite steht \texttt{bereit} oder \texttt{unvollständig}. +Solange das Broadcaster-Konto fehlt, läuft das Toolkit im Lesemodus: Chat +wird mitgeschrieben, Zuschauerzahlen und Clips bleiben aus. + +\section{OBS einrichten} + +Ohne diese vier Punkte entstehen keine Clips oder sie sitzen schief. + +\subsection{Wiedergabepuffer} + +\emph{Einstellungen} $\rightarrow$ \emph{Ausgabe} $\rightarrow$ +\emph{Wiedergabepuffer} aktivieren, 90 Sekunden. + +Unter \emph{Einstellungen} $\rightarrow$ \emph{Hotkeys} eine Taste auf +\emph{Wiedergabepuffer speichern} legen. Das ist der Clip-Knopf für den +Streamer selbst. Ein eigener Windows-Hotkey wird bewusst nicht gebaut: OBS +meldet den gespeicherten Puffer ohnehin über WebSocket, das ist derselbe +Auslöser. + +\subsection{Getrennte Tonspuren} + +\emph{Einstellungen} $\rightarrow$ \emph{Ausgabe} $\rightarrow$ +\emph{Aufnahme} $\rightarrow$ \emph{Audiospuren}, und im Audiomixer je Quelle +die Zuordnung prüfen: + +\begin{tabularx}{\textwidth}{@{}lX@{}} +\toprule +Spur 1 & Mischung, kommt ins fertige Video \\ +Spur 2 & Mikrofon, daraus entstehen die Untertitel \\ +Spur 3 & Spielton \\ +\bottomrule +\end{tabularx} + +Das ist bei lauten Spielen der Unterschied zwischen brauchbarem und +unbrauchbarem Transkript. + +\subsection{Bildaufteilung} + +Der Zuschnitt nimmt feste Bereiche aus dem fertigen Streambild. Verrutscht die +Kamera um 50 Pixel, sitzt in jedem Short der Kopf schief. Deshalb liegt die +Position fest: + +\begin{tabularx}{\textwidth}{@{}lll@{}} +\toprule +\textbf{Element} & \textbf{Position} & \textbf{Größe} \\ +\midrule +Facecam & x 0, y 0 & 480 $\times$ 270 \\ +Gameplay-Band & x 515 bis 1405 & 890 $\times$ 1080 \\ +Chat, Alerts, Overlays & ab x 1405 & 515 px Spalte \\ +\bottomrule +\end{tabularx} + +Zwischen Kamera und Gameplay-Band liegen 35 Pixel Luft. Ein leicht +verrutschter Kamerarahmen schiebt so nicht sofort einen Streifen Gesicht ins +Spielbild. + +Zum Ausrichten liegt eine Vorlage bei. Als Bildquelle in OBS laden, ganz nach +oben ziehen, Deckkraft über einen Farbkorrektur-Filter auf etwa 60 Prozent, +eigene Quellen einpassen, Vorlage wieder ausblenden. + +Steht die Kamera in OBS als 4:3, vorher in den Kameraeinstellungen auf 16:9 +umstellen. Ein 4:3-Bild in einen 16:9-Rahmen zu ziehen staucht das Gesicht. + +\subsection{Fremder Text im Bild} + +Was im Streambild steht, ist eingebrannt und lässt sich nachträglich nicht +entfernen. Untertitel des Spiels und Live-Caption-Overlays landen also im +Short, zusätzlich zu den eigenen Untertiteln. + +\begin{itemize} + \item \textbf{Spieluntertitel:} abschalten. Sie laufen über die volle + Bildbreite, werden vom Zuschnitt angeschnitten und bringen im Short + nichts. + \item \textbf{Live-Captions:} verschieben statt abschalten. Für Zuschauer + ohne Ton sind sie wertvoll. Sie müssen nur aus dem Gameplay-Band + heraus, etwa nach unten links oder in die rechte Spalte. +\end{itemize} + +Welche OBS-Quelle den Text überhaupt malt, sagt \texttt{abdulive sources}. + +\section{Der laufende Betrieb} + +\subsection{Clips auslösen} + +\begin{tabularx}{\textwidth}{@{}lX@{}} +\toprule +\texttt{!clip} im Chat & jeder Zuschauer \\ +OBS-Hotkey & der Streamer \\ +Chat-Spitze & erkennt auffällige Reaktionen, standardmäßig aus \\ +\bottomrule +\end{tabularx} + +Abklingzeiten sind nicht optional. Ohne sie ist \texttt{!clip} nach zwei +Streams ein Spielzeug: 60 Sekunden global, 300 Sekunden je Zuschauer. Wer zu +früh dran ist, bekommt eine Antwort im Chat. + +Die Spitzen-Erkennung läuft von Anfang an mit und schreibt jeden Kandidaten +samt Punktzahl mit, löst aber nichts aus. Sobald ein paar Streams +protokolliert sind, lässt sie sich an echten Daten kalibrieren und +scharfschalten. + +\subsection{Befehle} + +\begin{tabularx}{\textwidth}{@{}lX@{}} +\toprule +\texttt{abdulive check-env} & Selbsttest: Programme, Schriften, Modell, Verzeichnisse, OBS \\ +\texttt{abdulive sources} & listet die OBS-Quellen und sagt, welche im 9:16-Ausschnitt landen \\ +\texttt{abdulive calibrate} & prüft den Bildzuschnitt an einer echten Aufnahme \\ +\texttt{abdulive render } & rendert eine Rohdatei nach, etwa nach einer Layoutänderung \\ +\texttt{abdulive fetch-model} & lädt ein Sprachmodell \\ +\texttt{abdulive update} & prüft auf eine neue Fassung \\ +\bottomrule +\end{tabularx} + +\subsection{Läuft es?} + +\begin{codef} +Start-ScheduledTask -TaskName abdulive \textcolor{grau}{# starten} +Stop-ScheduledTask -TaskName abdulive \textcolor{grau}{# stoppen} +\end{codef} + +Status und Zahlen liefert \url{http://127.0.0.1:4792/health}. Die Protokolle +liegen unter \pfad{\%LOCALAPPDATA\%\textbackslash abdulive\textbackslash logs}. + +\begin{kasten} +Bei einer Adresse im Browser immer \texttt{127.0.0.1} statt +\texttt{localhost} verwenden, wenn etwas hängt. Der Dienst lauscht nur auf +IPv4, und Windows probiert bei \texttt{localhost} zuerst IPv6. Das kostet +zwei Sekunden. +\end{kasten} + +\section{Updates} + +Die Aufgabe \texttt{abdulive-update} sieht täglich um 5:00 Uhr nach. Findet +sie eine neue Fassung, lädt sie das Paket, prüft die Prüfsumme und legt es +bereit. + +\textbf{Eingespielt wird beim nächsten Start.} Mitten im Stream wird nichts +ausgetauscht. Schlägt der Tausch fehl, läuft die alte Fassung weiter. + +Sofort übernehmen: + +\begin{code} +Stop-ScheduledTask -TaskName abdulive; Start-ScheduledTask -TaskName abdulive +\end{code} + +\section{Sicherung} + +\begin{code} +& "$env:LOCALAPPDATA\Programs\abdulive\backup.ps1" +\end{code} + +Schreibt eine in sich stimmige Kopie der Datenbank und hält 30 Generationen. +Die Datenbank wird im laufenden Betrieb beschrieben, ein einfaches Kopieren +ergäbe eine unbrauchbare Sicherung. + +\section{Deinstallation} + +\begin{code} +& "$env:LOCALAPPDATA\Programs\abdulive\uninstall.ps1" +\end{code} + +Nimmt die beiden Aufgaben, beendet den Dienst und löscht den Programmordner. +Die Daten bleiben stehen, weil in der Datenbank Chatverläufe liegen. Wer neu +installiert, findet seine Einrichtung wieder. + +Wirklich alles weg, auch Datenbank, Clips und Sprachmodell: + +\begin{code} +& "$env:LOCALAPPDATA\Programs\abdulive\uninstall.ps1" -AuchDaten +\end{code} + +\section{Wenn etwas nicht geht} + +\begin{tabularx}{\textwidth}{@{}lX@{}} +\toprule +\textbf{Symptom} & \textbf{Ursache und Abhilfe} \\ +\midrule +Keine Clips & OBS-WebSocket aus oder Passwort fehlt. \texttt{check-env} + zeigt es unter \emph{OBS}. \\ +Clips ohne Untertitel & Sprachmodell fehlt. \texttt{abdulive fetch-model}. \\ +Untertitel voller Unsinn & Transkribiert wird die falsche Tonspur. Mikrofon + gehört auf Spur 2. \\ +Kopf sitzt schief & OBS-Layout weicht von der Vorgabe ab. + \texttt{abdulive calibrate}. \\ +Fremder Text im Short & Eine OBS-Quelle liegt im Gameplay-Band. + \texttt{abdulive sources}. \\ +Chat wird nur gelesen & Broadcaster-Konto nicht verbunden. \\ +Oberfläche antwortet nicht & Protokolle unter + \pfad{\%LOCALAPPDATA\%\textbackslash abdulive\textbackslash logs} ansehen. \\ +\bottomrule +\end{tabularx} + +\section{Wo was liegt} + +\begin{tabularx}{\textwidth}{@{}lX@{}} +\toprule +\pfad{config\textbackslash config.yaml} & Einstellungen. Keine Geheimnisse. \\ +\pfad{config\textbackslash layout.yaml} & Bildaufteilung für den Zuschnitt \\ +\pfad{db\textbackslash} & Datenbank und verschlüsselte Zugangsdaten \\ +\pfad{clips\textbackslash} & fertige Videos \\ +\pfad{work\textbackslash} & Rohdateien der Verarbeitung \\ +\pfad{models\textbackslash} & Sprachmodelle \\ +\pfad{logs\textbackslash} & Protokolle \\ +\bottomrule +\end{tabularx} + +Alles unterhalb von \pfad{\%LOCALAPPDATA\%\textbackslash abdulive}. + +\begin{kasten} +\textbf{Der Arbeitsordner wird noch nicht aufgeräumt.} Rohdateien sammeln +sich unbegrenzt an, mehrere hundert MB je Clip. Bis eine Aufbewahrungsregel +gebaut ist, gelegentlich selbst nachsehen. +\end{kasten} + +\end{document}