Block > text2youtubevideo.sh

 12 min (2403 Wörter, 15463 Zeichen)

Inhaltsverzeichnis

Hinweis

Dieser Eintrag ist älter als 3 Jahre und entspricht vermutlich nicht mehr dem neuesten Stand der Technik/Realität.

Vor einiger Zeit entdeckte ich durch Zufall wieder das kleine Programm espeak , was Text in Phoneme umwandelt, die dann von einem anderen Programm, z.B. mbrola in Sprache umgewandelt werden kann (die dann gefuehlt besser klingt als Microsoft Sam ). Das an sich ist ja schonmal grossartig, und so habe ich einfach mal ein paar Stimmen runtergeladen und damit herumexperimentiert. Mbrola und acht deutsche Stimmen koennen unter ArchLinux aus dem AUR runtergeladen werden, espeak aus dem community -Repository:

yaourt -S mbrola mbrola-voices-de1 mbrola-voices-de2 \
  mbrola-voices-de3 mbrola-voices-de4 mbrola-voices-de5 \
  mbrola-voices-de6 mbrola-voices-de7 mbrola-voices-de8
sudo pacman -S espeak

Nach der Installation liess ich einfach mal einen kleinen Text von den verschiedenen Stimmen vorlesen:

text="Hallo, ich bin eine Computerstimme!"
espeak "$text" -vde
for i in $(seq 2 7) ; do # leider funktionieren nur die Stimmen 2-7
  espeak "$text" -vmb-de$i
done

Und ab da hatte mich der Ehrgeiz gepackt und ich wollte mehr 😉
Ich finde es absolut faszinierend, aus reinem Text durch diverse Tools sowas wie Bilder, PDFs, Videos, Audio, usw. entstehen zu lassen. So kam mir die Idee, einen Text vorlesen zu lassen, mit Hintergrundmusik und einer Videospur zu versehen und mit einem Thumbnail und Untertiteln auf YouTube hochzuladen.

Nachdem ich einen Text gefunden hatte (naemlich die Martin Luther Uebersetzung der Bibel von 1912 ), zerstueckelte ich ihn in passende Zeilenlaengen (mit fmt, also 75 Zeichen) und liess die ersten 100 Zeilen abwechselnd mit verschiedenen Stimmen und zeilenabhaengigem Pitch mit espeak vorlesen. Jede Zeile liess ich in eine eigene Wav -Datei ausgeben.

Hintergrundmusik #

Die Hintergrundmusik fand ich schon etwas schwieriger. Angefangen habe ich mit einem awk-Script von kmkeen . Das gefiel mir auch schon ganz gut, allerdings war da nichts drin, was sich in Abhaengigkeit der Eingabe aendern wuerde, bzw. sah ich keine gute Moeglichkeit dazu.
Dann habe ich auf der Suche die ABC-Notation , scala und chasp entdeckt, was ich schonmal sehr geil fand, allerdings war es mir zu viel Aufwand, den Eingabetext entsprechend zu formatieren (nur Noten, dazu noch Notenlaengen usw.).
Und dann fand ich diesen Post , in dem durch ein kleines C-Programm 8-bit Musik generiert wird. Awesome!
Dazu z.B. einfach mal folgendes in eine Datei datei.c schreiben:

main(t){for(;;t++)putchar((t*5&t>>7)|(t*3&t>>10));}

Das mit gcc kompilieren:

gcc -w datei.c

Und ausfuehren, fuer unbegrenzte Musik:

./a.out

Geil! 😊
In dem Blogpost gibt es Links zu drei Videos, in denen einige Funktionen genannt werden. Und noch mehr Funktionen gibt es in diesem Thread .
Noch mehr zum Thema gibt es in diesem Post und es gibt sogar eine Website , auf der fuer jede Stereo-Seite eine eigene Funktion eingegeben und dann zusammen abgespielt werden kann. Awesome!

Jedenfalls war meine erste Idee, eine Funktion davon zu nehmen, und die Zahlen durch andere Zahlen auszutauschen, die mir von einer kleinen Funktion geliefert wird, die als Eingabe den Text bekommt, daraus den SHA-512 -Hash bildet und mit tr nur die Zahlen ausgibt. Die ersten vier Zahlen davon ersetzen dann die Zahlen in dem C-Programm.
Allerdings gab es wegen zu grossen Zahlen Warnings, weswegen ich mich dann einfach entschieden habe, abhaengig von der Textlaenge modulo 10 eine von zehn Funktionen fuer das C-Programm auszuwaehlen. Ist nicht so toll, aber reicht fuer den Anfang. Verbesserungsvorschlaege sind sehr willkommen! 😊

sha512sum input.txt | awk '{print $1}' | tr '[:alpha:]' ' ' | while read a b c d line ; do
  echo "main(t){for(;;t++)putchar((t*$a&t>>$b)|(t*$c&t>>$d));}"
done

Eine weitere Idee war es, nur wenig Output von diesem Programm zu verwenden, um es dann mit paulstretch (leider nur eine GUI ) oder rubberband zu stretchen, allerdings fand ich das nicht so passend, da muesste ein besserer Input her.

Konkatenierung der Textstimmen & Vermischung mit der Hintergrundmusik #

Am Anfang dachte ich noch, die Konkatenierung der Textstimmen ist ja recht trivial, einfach mit cat die Dateien konkatenieren. Aber ja, geht natuerlich nicht (Header und so). Ich habe dann eine Loesung mit ffmpeg gefunden (Quelle ):

ffmpeg -y -f concat -i <( for f in espeak-*.wav ; do echo "file '$f'" ; \
  done ) -c copy concat.wav -loglevel quiet

Das klappte auch wunderbar. Erst spaeter fiel mir dann auf, dass die Laenge der einzelnen .wav-Dateien von der Laenge von concat.wav abweicht. Nachdem ich mir die Dateien mit soxi espeak-*.wav mal genauer angesehen habe, stellte ich fest, dass die Sample Rate mal 22050 und mal 16000 ist.
ffmpeg ist dann so nett, und wandelt das anscheinend um, womit manche Stimmen dann langsamer sind. Das wollte ich natuerlich nicht, daher habe ich die Sample Rate der .wav-Dateien mit sox alle auf 16k gesetzt und bei der Gelegenheit auch gleich mit einem 2. Channel versehen:

for i in espeak-*.wav ; do sox $i -r 16k -c 2 $i.wav ; mv $i.wav $i ; done

Damit waren die Laengen der einzelnen Dateien (fast) identisch mit der von ffmpeg (Quelle ):

$ echo $(soxi -D espeak-*.wav | tr '\n' '+' | sed 's/+$//') | bc
45.917001
$ soxi -D concat.wav
45.917000

Soweit so gut, nun wollte ich also alles zusammenmischen. Allerdings musste ich die Hintergrundmusik erst noch von Raw Audio in etwas geeignetes umwandeln, und dann noch auf die passende Laenge zurechtschneiden. Auch hier ist sox wieder ein sehr gutes Tool.
Aber zuerst mal musste ich etwas Musik generieren und in eine Datei schreiben lassen:

./a.out | head -n 1M > audio.raw

Update (2015-12-17)

meillo hat angemerkt, dass head hier nicht portabel ist, besser waere folgendes mit dd:

./a.out | dd bs=1024 count=1024 > audio.raw

Ok, nun noch die Laenge von concat.wav bestimmen, um die Laenge von audio.raw dann direkt zu begrenzen (Quelle raw2wav , Quelle trim ):

sox -r 16k -e signed -b 16 -c 1 -v -0.1 audio.raw audio.wav \
  trim 0 $(soxi -D concat.wav)

Direkt alles in einem Schritt 😊 Zuerst die Sample Rate auf 16k setzen, das ganze als signed, 16-Bit, Stereo und etwas leiser umwandeln und eben noch mit trim begrenzen.
Und damit musste ich nur noch alles zusammenmischen, und zwar wieder mit sox:

sox -m audio.wav concat.wav output.wav

Update

Ich habe mich nun dazu entschlossen, den letzten Schritt nicht zu machen sondern stattdessen einfach concat.wav nach output.wav zu kopieren, sprich, die Hintergrundmusik wegzulassen, da sie mit den Einstellungen einfach zu nervig war.

Awesome, was kann dieses Tool eigentlich nicht? 😄
Hier mal 15 Beispiele , was so alles mit sox moeglich ist 😉

Video #

Das Video hinzufuegen war schon etwas schwieriger. Vor allem wusste ich erst mal ueberhaupt nicht, was ich da nehmen sollte. Es sollte ja schon etwas mit dem Eingabetext zu tun haben …
Beim Stoebern fand ich dann diese Frage bei StackExchange , wobei mir die Antworten schonmal gut gefielen. Weiter gefunden habe ich was mit gstreamer , allerdings war ich zu faul, mich damit zu beschaeftigen, wie das mit wav anstatt mp3 funktioniert. Und dann noch eine Frage .
Die erste Antwort hat mir gut gefallen, allerdings war das etwas zuviel auf einmal. Ich habe mich dann einfach fuer avectorscope entschieden:

ffmpeg -i output.wav -filter_complex avectorscope=s=1920x1080 -y \
  -acodec aac -strict -2 video.mp4

Zuerst hatte ich noch die Idee, das ganze einfach nur mit Bildern zu machen, allerdings waere das nicht so dynamisch gewesen. Mit sox ist es ziemlich einfach, ein Spectrogram zu erzeugen, allerdings laesst sich damit zusammen mit gnuplot auch leicht eine Waveform erstellen .

Thumbnail #

Fuer YouTube sollte es dann auch ein eigenes Thumbnail geben. Hier war zuerst meine Idee, die einzelnen Ziffern des Eingabetexts ins Hexadezimalsystem umzuwandeln um dann Hexadezimal-3-Tupel als RGB -Wert zu verwenden, um dann Pixel fuer Pixel ein Bild zu erstellen.
Nach einiger Suche fand ich dann eine gute Vorangehensweise , und zwar mit dd und convert! Krass!
Zwar nicht genau das, was ich wollte, aber es erfuellte den Zweck einer Thumbnailerstellung 😉

dd if=input.txt bs=$((1280*720*3)) count=1 | convert -size \
  1280x720 -depth 8 rgb:- thumbnail.png

Das war mir dann aber doch etwas zu unschoen, und dann fiel mir wieder gmic und mein Script zum Erstellen von Bildschirmhintergruenden ein.
Meine Idee war dann, den MD5 -Hash des Eingabetexts in 16 2-Tupel zu zerlegen um somit 4 RGBA Werte zu erhalten (Quelle ). Damit konnte ich dann den gmic-Befehl fuettern, um ein Verlaufsbild zu erstellen:

hex=$(md5sum input.txt | awk '{print $1}' | tr '[:lower:]' '[:upper:]')
hex1=$(cut -c-8 <<<$hex)
hex2=$(cut -c9-16 <<<$hex)
hex3=$(cut -c17-24 <<<$hex)
hex4=$(cut -c25-32 <<<$hex)
rgba1=$(printf "%d,%d,%d,%d" 0x${hex1:0:2} 0x${hex1:2:2} 0x${hex1:4:2} 0x${hex1:6:2})
rgba2=$(printf "%d,%d,%d,%d" 0x${hex2:0:2} 0x${hex2:2:2} 0x${hex2:4:2} 0x${hex2:6:2})
rgba3=$(printf "%d,%d,%d,%d" 0x${hex3:0:2} 0x${hex3:2:2} 0x${hex3:4:2} 0x${hex3:6:2})
rgba4=$(printf "%d,%d,%d,%d" 0x${hex4:0:2} 0x${hex4:2:2} 0x${hex4:4:2} 0x${hex4:6:2})

gmic 1280,720,1,3 -gimp_corner_gradient $rgba1,$rgba2,$rgba3,$rgba4 -o $thumbnail

Die Aufloesung haelt sich hier an die Best Pratices zur Thumbnailerstellung von YouTube .

Eine Beschriftung wollte ich dann auch noch haben, wobei sich dann recht einfach mit convert machen laesst:

text="Ich bin eine Beschriftung."
convert -background '#0008' -fill white -gravity center -size \
  1280x180 caption:"$text" thumbnail.png +swap -gravity south \
  -composite thumbnail_neu.png
Thumbnail fuer YouTube
Thumbnail fuer YouTube

Untertitel #

Zu guter Letzt wollte ich, wenn ich schonmal dabei bin, auch noch Untertitel fuer das Video haben. Das ist ja in meinem Fall keine grosse Sache, ich habe ja die Textzeilen und die Laenge der Textstimmen kann ich ja bestimmen.
Bei der Erstellung der Textstimmen habe ich also einfach nur fuer jede Textzeile eine srt -kompatible Form daraus gemacht. Die Laenge der Textstimmen habe ich dabei von Sekunden mit einer kleinen Funktion in hh:mm:ss:ms umgewandelt.
Awesome! 😊

Alles zusammen #

Die einzelnen Dateien gibt es hier zum runterladen:

Das Video (dank <video>-Tag im Browser abspielbar):

Und hier das Script zur Erstellung auf GitHub :

  text2youtubevideo.sh

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#!/bin/bash

# requirements
# TODO: test for mbrola-voices-de{1..8}
for i in wget awk mbrola aplay sox gmic ffmpeg convert espeak ; do
	command -v "$i" >/dev/null 2>&1 || { echo >&2 "I require \"$i\" but it's not installed. Aborting."; exit 1; }
done

# some variables, adjust to your needs
url="http://bitimage.dyndns.org/german/MartinLuther-1912/Martin_Luther_Uebersetzung_1912.txt"
lines=100
out="$HOME/tmp/$(basename "$0" .sh)"
# create project folder
if [ ! -d "$out" ] ; then
	mkdir -p "$out"
fi
bn="$out/$(basename "$0")"
txt="$out/martin_luther_uebersetzung_1912.txt"
concat="$bn-concat.wav"
cfile="${bn}.c"
binfile="${bn}.bin"
raw="${bn}.raw"
bgmusic="${bn}-bgmusic.wav"
thumbnail="${bn}.tmp.png"
thumbnail2="${bn}.png"
srt="${bn}.srt"
wav="${bn}.wav"
mp4="${bn}.mp4"
res_w="1920"
res_h="1080"
yt_w="1280"
yt_h="720"
debug=99

# if $txt is not available, download it
if [ ! -e "$txt" ] ; then
	wget -q -O "$txt" "$url"
fi

# get length of $txt
txtlen=$(wc -l<"$txt")

# generate md5sum of text, and convert to rgba
hex=$(md5sum "$txt" | awk '{print $1}' | tr '[:lower:]' '[:upper:]')
hex1=$(cut -c-8 <<<"$hex")
hex2=$(cut -c9-16 <<<"$hex")
hex3=$(cut -c17-24 <<<"$hex")
hex4=$(cut -c25-32 <<<"$hex")
rgba1=$(printf "%d,%d,%d,%d" 0x"${hex1:0:2}" 0x"${hex1:2:2}" 0x"${hex1:4:2}" 0x"${hex1:6:2}")
rgba2=$(printf "%d,%d,%d,%d" 0x"${hex2:0:2}" 0x"${hex2:2:2}" 0x"${hex2:4:2}" 0x"${hex2:6:2}")
rgba3=$(printf "%d,%d,%d,%d" 0x"${hex3:0:2}" 0x"${hex3:2:2}" 0x"${hex3:4:2}" 0x"${hex3:6:2}")
rgba4=$(printf "%d,%d,%d,%d" 0x"${hex4:0:2}" 0x"${hex4:2:2}" 0x"${hex4:4:2}" 0x"${hex4:6:2}")

# convert seconds to hours:minutes:seconds:milliseconds
function s2t() {
	T=$(cut -d\. -f1 <<<"$1")
	H=$((T/60/60%24))
	M=$((T/60%60))
	S=$((T%60))
	MS=$(cut -d\. -f2 <<<"$1")
	if [[ "$MS" == "0" ]] ; then MS="000" ; else MS=$(cut -c-3 <<<"$MS") ; fi
	printf '%02d:%02d:%02d,%03s' "$H" "$M" "$S" "$MS"
}

# read (german) text and save them to individual files
last=0.0
> "$srt"
fmt -t "$txt" | cat -n | head -n "$lines" | while read -r number line ; do
	tmpwav="$bn-espeak-$(printf "%05d\n" "$number").wav"
	voice=$(( (number % 6)+2 )) # only voices 2-7 available
	pitch=$(( number % 100 )) # pitch from 0-99
	espeak "$line" -vmb-de"$voice" -s 125 -p "$pitch" -w "$tmpwav"
	# change bit/sample rate to something equal with sox
	sox "$tmpwav" -r 16000 -c 2 "${tmpwav}".wav
	mv "${tmpwav}".wav "$tmpwav"
	# add subtitle
	l=$(soxi -D "$tmpwav")
	n=$(echo "$last+$l" | bc)
	{ echo "$number"; echo "$(s2t "$last") --> $(s2t "$n")"; echo "$line"; echo ""; } >> "$srt"
	last="$n"
done

# concatenate the wav files
ffmpeg -y -f concat -i <( for f in ${bn}-espeak-*.wav ; do echo "file '$f'" ; \
	done ) -c copy "$concat" -loglevel quiet

# create background music
# choose from these functions based on $txtlen%10
echo "main(t){for(;;t++)putchar(" > "$cfile"
case $((txtlen%10)) in
	0) echo "t*((t>>12|t>>8)&63&t>>4)" >> "$cfile" ;;
	1) echo "(t*(t>>5|t>>8))>>(t>>16)" >> "$cfile" ;;
	2) echo "t*((t>>9|t>>13)&25&t>>6)" >> "$cfile" ;;
	3) echo "t*(t>>11&t>>8&123&t>>3)" >> "$cfile" ;;
	4) echo "(t*(t>>8*(t>>15|t>>8)&(20|(t>>19)*5>>t|t>>3))" >> "$cfile" ;;
	5) echo "(t*5&t>>7)|(t*3&t>>10)" >> "$cfile" ;;
	6) echo "t*(t>>((t>>9|t>>8))&63&t>>4)" >> "$cfile" ;;
	7) echo "(t>>6|t|t>>(t>>16))*10+((t>>11)&7)" >> "$cfile" ;;
	8) echo "(t>>7|t|t>>6)*10+4*(t&t>>13|t>>6)" >> "$cfile" ;;
	9) echo "((t*(t>>8|t>>9)&46&t>>8))^(t&t>>13|t>>6)" >> "$cfile" ;;
esac
echo ");}" >> "$cfile"

# compile the program
gcc -w "$cfile" -o "$binfile" >/dev/null 2>&1

# output 1M lines of raw audio
#"$binfile" | head -n 1M > "$raw"
"$binfile" | dd bs=1024 count=1024 > "$raw"

# get length of $concat
len=$(soxi -D "$concat")
# TODO: check, if $len is enough

# convert raw audio to wav and trim it to $len
sox -r 16000 -e signed -b 16 -c 2 -v -0.1 "$raw" "$bgmusic" trim 0 "$len"

# mix background music with $concat
# sox -m $bgmusic $concat $wav
# don't mix it, ignore the background music
cp "$concat" "$wav"

# add video to audio
ffmpeg -y -loglevel quiet -i "$wav" -filter_complex \
	avectorscope=s=${res_w}x${res_h} -acodec aac -strict -2 "$mp4"
# also nice:
#ffmpeg -y -loglevel quiet -i $wav -filter_complex \
#	"[0:a]showwaves=s=${res_w}x${res_h}:rate=25,format=yuv420p[vid]" \
#	-map "[vid]" -map 0:a -codec:v libx264 -crf 18 -preset fast \
#	-c:a aac -strict -2 -b:a 256k $mp4

# thumbnail for youtube (png, 1280x720)
# create gradient thumbnail with rgba values
gmic -v -"$debug" "$yt_w,$yt_h,1,3" -gimp_corner_gradient "$rgba1,$rgba2,$rgba3,$rgba4" -o "$thumbnail"

# another thumbnail generation idea (but not as beautiful)
#dd if=$txt bs=$(($yt_w*$yt_h*3)) count=1 | convert -size ${yt_w}x${yt_h} -depth 8 rgb:- $thumbnail

# caption text
text="The first
$lines
lines of
$(basename "$txt"),
fully generated by a program."

# add caption to thumbnail
convert -background '#0008' -fill white -gravity center -size "${yt_w}x$((yt_h/4))" \
	caption:"$text" "$thumbnail" +swap -gravity south -composite "$thumbnail2"

exit 0

 Open Borders

Beitraginfos

 2015-11-25, 22:37:51
 2020-05-24, 14:22:11
 Yannic
 bibel, linux, youtube
 Permalink

Ähnliche Beiträge