Das Run-Programm und die HTTP-Anfrage von JMP verwenden KI-Tools und FFmpeg, um ein Video zu erstellen.
Abgeschlossenes Projekt auf YouTube.
Besorgen Sie sich ein Buch vonhttps://www.gutenberg.org/ebooks/67121 und speichern Sie den Text auf der Festplatte. Schreiben Sie JSL, um den Text in eine Datentabelle einzufügen, einen Absatz pro Zeile.
txt = Load Text File( "f:/PolarisAndTheGoddessGlorian.txt" );//
txt = Regex(
txt,
"\*\*\* START OF THE PROJECT GUTENBERG EBOOK .*? \*\*\*\s*(.*?)\*\*\* END OF THE PROJECT GUTENBERG EBOOK .*? \*\*\*",
"\1"
);
If( Length( txt ) != 438066,
Throw( "text?" )
);
// look for paragraphs separated by ~0D~0A~0D~0A
txt = Regex( txt, "\s*\r\n\r\n\s*", "❡", globalreplace );
txt = Regex( txt, "_", " ", globalreplace ); // italic indicated with _this_ style
txt = Regex( txt, "[\n\r]+", " ", globalreplace ); // all on one line
dtStory = New Table( "story", New Column( "paragraph", Character, "Nominal" ) );
rc = Pat Match(
txt,
Pat Repeat(
(Pat Pos( 0 ) | "❡") + //
(Pat Break( "❡" )) >> p + Pat Test(
dtStory << addrows( 1 );
dtStory:paragraph[N Rows( dtStory )] = p;
//Wait( 0.01 );
,
1
)
) + Pat R Pos( 0 )
);
dtStory << New Column( "OriginalSort", values( 1 :: N Rows( dtStory ) ) );
dtStory << New Column( "OriginalLength", formula( Length( paragraph ) ) );
dtStory << runformulas;
dtStory:OriginalLength << deleteformula;
Um mit mehr als einer Leserstimme zu experimentieren, erstellen Sie einige Heuristiken, um zu entscheiden, ob sich die Anführungszeichen um den von einer männlichen oder weiblichen Stimme gesprochenen Text befinden. Es könnte auch eine gute Wahl sein, diesen Schritt zu überspringen und einfach die beste Erzählerstimme zu verwenden; Das Wechseln der Stimmen ist etwas verwirrend.
Ihr Browser unterstützt das Audio-Element nicht.
Stimme des Erzählers
Ihr Browser unterstützt das Audio-Element nicht.
Männlicher Sprecher
Ihr Browser unterstützt das Audio-Element nicht.
Sprecherin
// speaker identification...just enough to choose a narrator vs female vs male voice
TalkingVerbs = "said" | "asked" | "repeated" | "exclaimed" | "muttered" | "called" | "continued" | "queried" |
"cried" | "comment" | "thought" | "say" | "shouted" | "awestruck" | "questioned" | "turned" | "replied" |
"answered" | "rejoined " | "screamed" | "gasped" | "bowed" | "answered" | "smiled" | "laughed" | "answer" |
"spoke" | "interposed" | "sobbed" | "enjoined" | "warned" | "shook" | "lowered" | "grew" | "grumbled" | "smile" |
"whispered" | "put" | "interposed" | "hiccoughed" | "informed" | "added" | "frowned" | "nodded" | "questioned" |
"gasped" | "pointed" | "broke" | "inquired" | "spoke" | "looked" | "barked" | "interrupted" | "shouted" |
"addressed" | "continued" | "told" | "suggested" | "directed" | "cautioned" | "comment" | "concluded" | "pleaded"
| "spoke" | "grinned" | "drew" | "snarled" | "volunteered" | "counsel" | "persisted" | "spat" | "counseled" |
"lifted" | "shout" | "roared" | "prayed" | "howled" | "shrieked" | "told" | "mused" | "growled" | "shouting" |
"groaned" | "grunted" | "rambled" | "directed" | "went" | "breathed" | "called" | "commanded" | "shuddered" |
"began" | "rejoined";
MaleSpeakerNames = "he" | "everson" | "commander" | "polaris" | "coxswain" | "zenas" | "wright" | "scotchman" |
"his" | "mackechnie" | "geologist" | "lieutenant" | "scot" | "stranger" | "oleric" | "captain" | "brunar" |
"ensign" | "brooks" | "minos" | "mordo" | "king" | "bel-ar" | "raissa" | "priest" | "man" | "melton" | "janess" |
"men" | "jastla" | "scientist" | "wright" | "voice" | "zoar" | "soldier" | "son" | "broddok" | "albar" |
"engineer" | "zind" | "chief" | "vedor" | "master" | "they" | "heralds";
FemaleSpeakerNames = "she" | "her" | "girl" | "rose" | "emer" | "lady" | "memene" | "queen" | "goddess" |
"glorian";
gapchars = ":; ,.?!\!n\!r"; // gaps between words ... for example he reluctantly said
gap = Pat Span( gapchars ) | Pat Pos( 0 ) | Pat R Pos( 0 );
distanceBetween = 1;
femalePattern = (gap + FemaleSpeakerNames >> spkr + gap + Pat Repeat(
Pat Break( gapchars ) + GAP,
0,
Expr( distanceBetween ),
RELUCTANT
) + TalkingVerbs >> verb + gap) //
| (gap + TalkingVerbs >> verb + gap + Pat Repeat(
Pat Break( gapchars ) + GAP,
0,
Expr( distanceBetween ),
RELUCTANT
) + FemaleSpeakerNames >> spkr + gap);
malePattern = (gap + MaleSpeakerNames >> spkr + gap + Pat Repeat(
Pat Break( gapchars ) + GAP,
0,
Expr( distanceBetween ),
RELUCTANT
) + TalkingVerbs >> verb + gap) //
| (gap + TalkingVerbs >> verb + gap + Pat Repeat(
Pat Break( gapchars ) + GAP,
0,
Expr( distanceBetween ),
RELUCTANT
) + MaleSpeakerNames >> spkr + gap);
// add a column for the speaker: narrator, male, female. The words above and algorithm below are
// specific to this story (and the time it was written, 1917)
// a paragraph is either all narrator or narrator + male speaker or narrator + female speaker
// usually there is a talkingverb and male or female speaker name in the paragraph. if not,
// the previous paragraph might end with
// he said:
// etc in the same format. if not, and the paragraph does not have a trailing matching quotation mark,
// the male/female speaker will continue at the next open quotation mark. if not, there are two places where
// the speaker should alternate male/female.
// for most of the book, male speaker will be the correct guess.
dtstory << New Column( "speaker", Character, "Nominal" );
prevspeaker = "narrator";
currspeaker = "narrator";
quotationmark = "\!"";
missingEndQuote = 0;
quotation = quotationmark + (//
(Pat Break( quotationmark ) + quotationmark + Pat Test(
missingEndQuote = 0;
1;
)) //
| //
(Pat Rem() + Pat Test(
missingEndQuote = 1;
1;
)));
Diese Muster, „malePattern“ und „femalePattern“, identifizieren Strukturen wie
„Jetzt denke ich, dass ich in all diesen Ereignissen endlich meinen Weg sehe“, murmelte er.
oder
„Das hast du wirklich gut gemacht, guter Oleric“, sagte sie schnell. „Mein König wird dir die verlorenen Fames verzeihen, deren Verlust sicherlich nicht deine Schuld war. Und diese – diese sind mir viele Fames wert.“ Sie wählte zwei der Perlen von angemessener Größe und schönem Glanz aus und gab sie Oleric.
wobei er und sie auch verschiedene Namen und Spitznamen von Charakteren sein könnten. Und sagte könnte aus dieser großen Liste von Verben entnommen werden ... Der Autor muss Freude daran gehabt haben, sie durchzugehen.
Als nächstes wenden Sie die Muster Zeile für Zeile an, mit etwas Logik für abwechselnde Sprecher und einem Sonderfall, bei dem die Heuristik fehlschlägt (865). (Die Kapitelnummern werden später verwendet, damit römische Ziffern wie IX „Neun“ sagen. Ich denke, das war nicht Teil der Trainingsdaten.)
iCHAPTER = 0;
chaps = {"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"};
For( irow = 1, irow <= N Rows( dtstory ), irow += 1,
p = dtstory:paragraph[irow];
continueSpeaker = missingEndQuote; // from previous paragraph
hasquote = Pat Match( p, quotation, "" );
While( Pat Match( p, quotation, "" ), 0 ); // remove all quoted bits before looking for "he said"
explicitSpeaker = 0;
For( distanceBetween = 0, distanceBetween < 5 & explicitSpeaker == 0, distanceBetween += 1,
If(
Pat Match( p, femalePattern ), // try giving female precedence around 1322
prevspeaker = currspeaker;
currspeaker = "female";
explicitSpeaker = 1;//
,
Pat Match( p, malePattern ),
prevspeaker = currspeaker;
currspeaker = "male";
explicitSpeaker = 1;//
)
);
If( irow == 865,
Show( irow, currspeaker, prevspeaker );
prevspeaker = "male";
);
If( hasquote,
If(
explicitSpeaker, dtstory:speaker[irow] = currspeaker || " explicit", /* else if*/continueSpeaker,
dtstory:speaker[irow] = currspeaker || " unquote", /*else if*/
Ends With( dtstory:paragraph[irow - 1], ":" ), dtstory:speaker[irow] = currspeaker || " leading", // else alternating speakers
{prevspeaker, currspeaker} = Eval List( {currspeaker, prevspeaker} );
dtstory:speaker[irow] = currspeaker || " alternate";
)
, //
dtstory:speaker[irow] = "narrator" || " " || currspeaker || Char( explicitSpeaker )
);
);
Der Autor hat einige strenge Regeln für Zitate verwendet, die jetzt wahrscheinlich anders sind; Die JSL nutzt ihre Steifigkeit aus. Ein abschließendes Anführungszeichen wird immer weggelassen, wenn derselbe Sprecher im nächsten Absatz fortfährt. Wenn sich zwei Sprecher abwechseln, können die von ihm/sie gesagten Teile weggelassen werden, sobald die Reihenfolge festgelegt ist. Und das Ende des vorherigen Absatzes kann den Sprecher im nächsten Absatz bestimmen.
Als die hungrigen Männer ihre Mahlzeit beendet hatten, sprach das Mädchen erneut:
„Sagen Sie, Mann vom Meer, ich habe gehört, dass in einem Gefängnis in der Stadt Adlaz eine schöne Dame auf Sie wartet. Stimmt das?“
„Ja, Dame, es ist wahr“, sagte Polaris; und er seufzte.
„Und du führst ein großes Heer dorthin, um sie zu befreien?“ Das Mädchen blieb hartnäckig.
„Ja, wenn ich darf.“
„Aber um nach Adlaz zu gelangen, musst du diese Festung Barme einnehmen; und du findest, dass es eine harte Nuss ist, sie zu knacken. Ist das nicht so?“
„Das stimmt auch, meine Dame.“
„Nun, hör mal, Mann.“ Das Mädchen stand auf und kam an den Tisch. „Du, der du einer Frau treu bist, wie nur wenige Männer es je tun; vielleicht kann dir die arme, verachtete, verstoßene Raula bei diesem Unterfangen etwas helfen.“
Das Audio wird in einer Ausdrucksspalte mit einem Ereignishandler gespeichert, der das Audio abspielt, wenn darauf geklickt wird. Die WavWriter-Funktion ist JSL in einer anderen Datei, irgendwo unten ...
dtStory << New Column( "audio",
expression,
Set Property(
"Event Handler",
Event Handler(
Click(JSL Quote(
Function( {thisTable, thisColumn, iRow},
RiffUtil:WavWriter( "$temp/yyy.wav", Blob To Matrix( Gzip Uncompress( eval(thisTable:thisColumn[iRow]) ), "int", 2, "big", 1 ), 22050 )
);
) )
)
)
);
dtStory << New Column( "nsamples" );
For( irow = 1, irow <= N Rows( dtstory ), irow += 1,
p = dtStory:paragraph[irow];
// do some fix-ups * * *, Chapter eye, --, "single"
pprev=p;
p = Regex( p, "\!"([a-zA-Z,]+)\!"", "\1", GLOBALREPLACE ); // he was "all" alone ==> he was all alone don't switch voice "Ivanhoe,"
If( p != pprev,
Show( "quoteword", irow, pprev, p )
);
pprev=p;
p = Regex( p, "\!"--", ",\!" ", GLOBALREPLACE ); // "go away"--he said--"or else" ==> "go away," he said, "or else" don't start segment with comma?
If( p != pprev,
Show( "quotehyphens", irow, pprev, p )
);
pprev=p;
p = Regex( p, "--", ", ", GLOBALREPLACE ); // -- does not slow down, but comma does
If( p != pprev,
Show( "hyphens", irow, pprev, p )
);
pprev=p;
p = Regex( p, "( [A-Z][a-z]?)\.", "\1", GLOBALREPLACE ); // _Mr. brown ==> _Mr brown
If( p != pprev,
Show( "Mr. B.", irow, pprev, p )
);
pprev=p;
If( Pat Match( p, Pat Pos( 0 ) + Pat Span( " *\n\r" ) + Pat R Pos( 0 ), "" ), // * * * ==> empty
Show( "asterix", irow, pprev, p )
);
pprev=p;
If( Pat Match( p, Pat Pos( 0 ) + "CHAPTER " ), // roman ==> one, two, ... ten
iCHAPTER += 1;
p = "Chapter " || chaps[iChapter] || ".";
Show( "chapno", irow, pprev, p );
);
if(trim(p)!="",samples = paragraphToSamples( p, dtStory:speaker[irow] ),samples=j(22050,1,0));//* * * paragraph
dtStory:audio[irow] = Gzip Compress( Matrix To Blob( samples, "int", 2, "big" ) );
dtStory:nsamples[irow] = N Rows( samples );
Wait( .1 );
);
Ein Großteil der oben genannten JSL nimmt einige zusätzliche Korrekturen am Text vor; Der KI-Erzähler wurde auf eine Art und Weise trainiert, die – nicht funktionierte, also ersetzt er – durch , um die gewünschte Pause zu bekommen. Dann mussten einige Zitate, die mit begannen , korrigiert werden, damit sie funktionierten. Vieles davon ist spezifisch für das Buch und den Erzähler. Am Ende erstellt „paragraphToSamples“ das 22,050-kHz-Audio, das dann komprimiert und in einem Blob in der Ausdrucksspalte gespeichert wird.
In addition to the paragraph text, there is an audio and pict column. The audio column has a click handler to play the binary data. This is the denouement of1729 paragraphs. Zusätzlich zum Absatztext gibt es eine Audio- und Bildspalte. Die Audiospalte verfügt über einen Click-Handler zum Abspielen der Binärdaten. Dies ist die Auflösung von 1729 Absätzen. Bevor wir uns den Code zum Erstellen von Bildern ansehen, werfen wir einen Blick auf „paragraphToSamples“, um die erste neue HTTP-Anfrage zu sehen. Die erste Funktion, getSamples, zeigt die Logik für die neue HTTP-Anfrage, die ein GET auf dem TTS-Server (Text to Speech) ausführt. Die zweite Funktion, „paragraphToSamples“, teilt einen Absatz in Segmente für die Stimmen auf, verwendet „getSamples“ für jedes Segment und fügt die Samples für den Absatz zusammen. Die neue HTTP-Anfrage verwendet die Felder, um den Sprechernamen und den Text in eine HTML-GET-Anfrage an einen Server zu packen, der auf einem lokalen Computer ausgeführt wird (Python, unten).
include("f://redwindows/TTS_wav_support.jsl");
/* on dell 780 main,
python tts2.py
to start the server for text-to-speech
*/
getsamples = Function( {text, speaker, rate},
{fields, req, b, c},
fields = Associative Array();
fields["text"] = text;
fields["speaker_id"] = speaker;
fields["language_id"] = "en";
req = New HTTP Request( URL( "http://192.168.0.57:8080//api/tts" ), Method( "GET" ), Query String( fields ) );
b = req << send;
(Save Text File( "$temp/xxx.wav", b ));
c = (RiffUtil:loadWav( "$temp/xxx.wav" ));
If( speaker == "jenny" & rate != 48000,
c = RiffUtil:resample( c, 48000, rate )
);
If( speaker != "jenny" & rate != 22050,
c = RiffUtil:resample( c, 22050, rate )
);
c;
);
/****
speakers=
{"jenny", "ED\!n", "p225", "p226", "p227", "p228", "p229", "p230", "p231", "p232", "p233", "p234", "p236", "p237", "p238", "p239", "p240", "p241",
"p243", "p244", "p245", "p246", "p247", "p248", "p249", "p250", "p251", "p252", "p253", "p254", "p255", "p256", "p257", "p258", "p259", "p260",
"p261", "p262", "p263", "p264", "p265", "p266", "p267", "p268", "p269", "p270", "p271", "p272", "p273", "p274", "p275", "p276", "p277", "p278",
"p279", "p280", "p281", "p282", "p283", "p284", "p285", "p286", "p287", "p288", "p292", "p293", "p294", "p295", "p297", "p298", "p299", "p300",
"p301", "p302", "p303", "p304", "p305", "p306", "p307", "p308", "p310", "p311", "p312", "p313", "p314", "p316", "p317", "p318", "p323", "p326",
"p329", "p330", "p333", "p334", "p335", "p336", "p339", "p340", "p341", "p343", "p345", "p347", "p351", "p360", "p361", "p362", "p363", "p364",
"p374", "p376"};
bestMaleSpeakers={"p226","p251"/*best*/,"p254","p256"/*alternate*/,"p299","p302"/*alternate*/,"p330"};
bestFemaleSpeakers={"p225","p243","p250","p273"/*best*/,"p274"/*alternate*/,"p294","p300"/*alternate*/};
****/
finalSampleRate = 22050;
malevoice = "p256"/*male*/;
narratorvoice = "p273"/*narrator*/;
femalevoice = "p300"/*female*/;
quotationmark = "\!"";
paragraphToSamples = Function( {paragraph, speaker},
{all, part, parta, rc, currentSpeaker, quoteSpeaker},
quoteSpeaker = Match( Substr( speaker, 1, 3 ), "mal", malevoice, "fem", femalevoice, narratorvoice );
all = J( 0, 1, 0 );// start with empty matrix to join the voice segments together
currentSpeaker = narratorvoice;
// quotation marks toggle between narrator and speaker, always start with narrator
// before first quotation mark. process whole paragraph. possible missing close
// quotation mark at end.
While( Length( paragraph ) > 0,
// break at quotation mark and remove from paragraph. take remainder if no more quotation marks.
rc = Pat Match( paragraph, (Pat Break( quotationmark ) >> part + quotationmark) | Pat Rem() >> part, "" );
If( rc != 1,
Throw( "patmatch?" )
);
If( Length( part ), // leading quotation mark (len==0) will switch from narrator to speaker
parta = getsamples( part, currentSpeaker, finalSampleRate ); // convert text to audio
all = all |/ parta;
);
// alternate speaker/narrator. Typically (ralph said) "Let's go" said ralph, "they are waiting."
If( currentSpeaker == narratorvoice,
currentSpeaker = quoteSpeaker//
, //
currentSpeaker = narratorvoice
);
);
all // return the joined samples
);
Der Python-Code für den lokalen Server sieht folgendermaßen aus (und entspricht der Funktionsweise von getSamples oben):
# modified from https://github.com/coqui-ai/TTS/discussions/2740#discussion-5367521 , thanks @ https://github.com/masafumimori
import io
import json
import os
from typing import Union
from flask import Flask, request, Response, send_file
from TTS.utils.synthesizer import Synthesizer
from TTS.utils.manage import ModelManager
from threading import Lock
app = Flask(__name__)
manager = ModelManager()
#model_path = None
#config_path = None
speakers_file_path = None
vocoder_path = None
vocoder_config_path = None
#model_name = "tts_models/en/vctk/vits"
#model_name = "tts_models/en/ek1/tacotron2"
# model_name = "tts_models/en/jenny/jenny"
model_path1, config_path1, model_item1 = manager.download_model("tts_models/en/vctk/vits")
# load models
synthesizer1 = Synthesizer(
tts_checkpoint=model_path1,
tts_config_path=config_path1,
tts_speakers_file=speakers_file_path,
tts_languages_file=None,
vocoder_checkpoint=vocoder_path,
vocoder_config=vocoder_config_path,
encoder_checkpoint="",
encoder_config="",
use_cuda=False,
)
model_path2, config_path2, model_item2 = manager.download_model("tts_models/en/jenny/jenny")
# load models
synthesizer2 = Synthesizer(
tts_checkpoint=model_path2,
tts_config_path=config_path2,
tts_speakers_file=speakers_file_path,
tts_languages_file=None,
vocoder_checkpoint=vocoder_path,
vocoder_config=vocoder_config_path,
encoder_checkpoint="",
encoder_config="",
use_cuda=False,
)
print(
" > Available speaker ids: (Set --speaker_idx flag to one of these values to use the multi-speaker model."
)
print(synthesizer1.tts_model.speaker_manager.name_to_id)
lock = Lock()
@app.route("/api/tts", methods=["GET"])
def tts():
with lock:
text = request.args.get("text")
speaker_idx = request.args.get("speaker_id", "")
language_idx = request.args.get("language_id", "")
style_wav = request.args.get("style_wav", "")
style_wav = style_wav_uri_to_dict(style_wav)
print(f" > Model input: {text}")
print(f" > Speaker Idx: {speaker_idx}")
print(f" > Language Idx: {language_idx}")
if speaker_idx=="jenny":
wavs = synthesizer2.tts(text, language_name=language_idx, style_wav=style_wav)
out = io.BytesIO()
synthesizer2.save_wav(wavs, out)
else:
wavs = synthesizer1.tts(text, speaker_name=speaker_idx, language_name=language_idx, style_wav=style_wav)
out = io.BytesIO()
synthesizer1.save_wav(wavs, out)
return send_file(out, mimetype="audio/wav")
def style_wav_uri_to_dict(style_wav: str) -> Union[str, dict]:
if style_wav:
if os.path.isfile(style_wav) and style_wav.endswith(".wav"):
return style_wav # style_wav is a .wav file located on the server
style_wav = json.loads(style_wav)
return style_wav # style_wav is a gst dictionary with {token1_id : token1_weigth, ...}
return None
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
Der Python-Code läuft auf einem Linux-Computer; Die TTS-Unterstützung ist unter Windows nicht verfügbar. HTTP-Anfrage ist egal! Danke @masafumimori für den Code, der mich davor bewahrt hat, Flask neu zu lernen.
$ python tts2.py
> tts_models/en/vctk/vits is already downloaded.
> Using model: vits
> Setting up Audio Processor...
| > sample_rate:22050
...
> Setting up Audio Processor...
| > sample_rate:48000
...
| > win_length:2048
> Available speaker ids: (Set --speaker_idx flag to one of these values to use the multi-speaker model.
{'ED\n': 0, 'p225': 1, 'p226': 2, 'p227': 3, 'p228': 4, 'p229': 5, 'p230': 6, 'p231': 7,
...
106, 'p374': 107, 'p376': 108}
* Serving Flask app 'tts2'
* Debug mode: off
INFO:werkzeug:WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:8080
* Running on http://192.168.0.57:8080
INFO:werkzeug:Press CTRL+C to quit
Der TTS-Server läuft ohne jegliche Cuda-Unterstützung auf alter Hardware mit den oben ausgewählten Stimmen ... schnell genug – Windows hat beim Warten auf die GET-Anfrage, um eine Antwort zu liefern, keine Zeitüberschreitung erfahren. Typischerweise werden die Antworten ungefähr in derselben Zeit zurückgegeben, in der die Audiowiedergabe dauert, bei anderen Stimmen kann es jedoch länger dauern. Wenn dies zu einem Problem wird, könnte der Python-TTS-Server mit einer Abfrageabfrage neu geschrieben werden, ähnlich wie der Easy-Diffusion-Server funktioniert (unten).
(Wenn ich Nvidia-Grafikkarten mit viel Speicher und Cuda-Kernen und neuere Computer mit größeren Netzteilen hätte, würde ich wahrscheinlich eine 10- bis 100-fache Leistungssteigerung erzielen.)
Der TTS-Server ist von diesem Paket abhängig:
pip install -U TTS
https://pypi.org/project/TTS/
https://docs.coqui.ai/en/latest/installation.html
Bilder
Ab Anfang 2024 gibt es eine Reihe von Websites, die Bilder aus Text generieren. Die kostenlosen Websites sind in der Regel langsam, ein Bild dauert ein bis fünf Minuten. Sie können auch die Software und trainierten Modelle für Stable Diffusion herunterladen, die von vielen Websites verwendet werden. https://github.com/easydiffusion/easydiffusion funktioniert unter Win und Linux. Der folgende Code geht von einem Satz von vier Linux-Rechnern ohne Cuda-Unterstützung aus und erstellt alle zwei Minuten ein Bild (schneller, langsamer, je nachdem ...). Es scheint mehrere APIs für die restbasierten Webversionen von Stable Diffusion zu geben. Der folgende Code funktioniert mit der Easy-Diffusion-Version.
Für jeden Absatz werden Bilder erstellt, indem der Text des vorhergehenden, aktuellen und folgenden Absatzes verwendet wird. Einige andere Schlüsselwörter sind enthalten (Farbe, dramatisch, HD, Landschaft, realistisch, Fantasie, filmisch) und einige negative Schlüsselwörter (NSFW, Comic, Pokémon, Text, Bildunterschrift, Wörter, Buchstaben, Album, Panel, Religion, Gedicht, Prosa). . Negative Wörter wie Text , Bildunterschrift , Wörter , Pokémon , Comic usw. waren ein Versuch, Bilder wie zu vermeiden
Not sure how many points this card is worth.Ich bin mir nicht sicher, wie viele Punkte diese Karte wert ist.
I can't look at it without trying to read it.Ich kann es nicht ansehen, ohne zu versuchen, es zu lesen.
Word balloons!Wortballons!
What are they saying?Was sagen Sie?
Einige davon landeten trotzdem in den Videos; Die negativen Worte sind lediglich Vorschläge, um das KI-Modell von Bildern abzulenken.
Weit oben verwendete die Audio-TTS-API HTTP GET, um Audiobeispiele (eine .wav- oder .mp3-ähnliche Datei) abzurufen. Es funktionierte einwandfrei, da GET die Antwort berechnen und in wenigen Sekunden zurückgeben konnte. Hätte es etwa eine Minute gedauert, gingen viele Tools (Windows, JMP, Browser) davon aus, dass die Verbindung unterbrochen war, und gaben statt eines Ergebnisses eine Fehlermeldung aus. Die unten stehende Easy Diffusion API löst einen längeren Prozess auf dem Server aus, der auf meinen langsamen Rechnern mehrere Minuten dauern kann. Ich fand keine ausreichend gute Beschreibung der API und griff auf die F12-Inspektion des Firefox- oder Chrome-Browsers zurück, um die Interaktion zwischen Browser und Server zu untersuchen. In der JSL sehen Sie, wo JMP wie ein Browser eine Anfrage mit Schlüsselwörtern und Parametern sendet und dann den Status vom Server abruft, bis das Bild fertig ist. Beim ersten POST antwortet der Server mit einer für die Anfrage eindeutigen Status-URL. Die JSL wird dadurch noch komplizierter, dass sie eine Liste von Servern und Status-URLs für vier parallel laufende Maschinen führt, die jeweils unabhängig voneinander an einer anderen Zeile der Datentabelle arbeiten.
Das ursprüngliche Design der addImage-Routine würde der Tabelle eine neue Bildspalte hinzufügen, wenn alle aktuellen Bildspalten der Zeile ausgefüllt wären; Die Idee war, es eine Weile laufen zu lassen. Am Ende habe ich Bilder aus zwei Spalten verschiedener Tabellen mit leicht unterschiedlichen Parametern verwendet.
addImage = Function( {img, irow, seed, request},
{c1, c2, icol},
For( icol = 2, icol < N Cols( dt ), icol += 2,
If( Is Empty( dt[irow, icol] ),
c1 = Column( dt, icol );
c2 = Column( dt, icol + 1 );
Break();
)
);
If( icol >= N Cols( dt ),
c1 = dt << New Column( "pict",
expression,
"None",
Set Property(
"Expression Role",
Expression Role(
"Picture",
MaxSize( 512, 512 ),
StretchToMaxSize( 0 ),
PreserveAspectRatio( 1 ),
Frame( 0 )
)
),
Set Display Width( 522 )
);
c2 = dt << New Column( "meta",
expression,
"None",
Set Property( "Expression Role", ),
Set Display Width( 20 )
);
);
c1[irow] = img;
c2[irow] = Eval Insert( "seed: ^seed^\!nrequest: ^request^" );
);
Die mehreren Server begannen als ein einziger LocalHost. Die Servertabelle enthält auch die Status-URL und die Zeilennummer. Wenn die Status-URL schließlich ein Bild liefert, kann dieses in der rechten Zeile gespeichert werden.
// these servers need to run the easydiffusion server...more would be better, cuda, etc
// with 4 I get a picture a minute; 1800/60 -> 30 hours to fill in the table.
// my old hardware does not support cuda cards good enough to use them. bummer.
LHtable = [// originally "localhost", now there are N hosts
1 => ["host" => "192.168.0.38:9000", // thinkd.local
"statusURL" => 0, "targetRow" => 0], 2 => ["host" => "192.168.0.37:9000",
// thinkc.local
"statusURL" => 0,
"targetRow" => 0], 3 => ["host" => "192.168.0.36:9000",
// thinkb.local
"statusURL" => 0,
"targetRow" => 0], 4 => ["host" => "192.168.0.35:9000",
// dell9010.local
"statusURL" => 0,
"targetRow" => 0]];
getLH = Function( {},
{i},
For( i = 1, i <= N Items( LHtable ), i += 1,
If( LHtable[i]["targetRow"] == 0,
Return( i )
)
);
Return( 0 );
);
Ein separater Schritt, um die Absatztripel zu kombinieren und einige unerwünschte Zeichen zu entfernen. Ich bin mir nicht sicher, warum die Eingabeaufforderung ein Ausdruck war. Es sollte ein Zeichen sein, funktioniert aber trotzdem. Diese Version verwendet if(1, um einen Test für Absätze mit geringer Länge zu vermeiden.
Im Stable-Diffusion-Modell gibt es keine Erinnerung von einem Bild zum nächsten. Der dreifache Absatz ist ein schlechter Versuch, ein Langzeitgedächtnis zu simulieren. Die Charaktere auf den Bildern ändern von Bild zu Bild ihre Haarfarbe, Kleidung usw. Möglicherweise sind hierfür bessere Modelle in Sicht.
// pre-pass the story into the table.
dt = New Table( "Untitled", New Column( "prompt", expression ), Set Cell Height( 522 ) );
For( iom = 1, iom <= N Rows( dtStory ), iom += 1,
txtprev = dtStory:paragraph[Max( 1, iom - 1 )];
txtprev = Regex( txtprev, "[^a-zA-Z,'.?]+", " ", GLOBALREPLACE );
txt = dtStory:paragraph[iom];
txt = Regex( txt, "[^a-zA-Z,'.?]+", " ", GLOBALREPLACE );
txtnext = dtStory:paragraph[Min( N Rows( dtStory ), iom + 1 )];
txtnext = Regex( txtnext, "[^a-zA-Z,'.?]+", " ", GLOBALREPLACE );
txt = " ( " || txt || " ) " || txtprev || " " || txtnext;
If( 1 /*300 < Length( txt ) < 700*/, // choose modest length paragraphs
dt << Add Rows( 1 );
//iom += 20;
dt:prompt[N Rows( dt )] = txt;
);
);
Dieser letzte, monolithische Block übernimmt die ganze schwere Arbeit. Es nutzt die obige LH-Struktur, um mehrere Server zu beschäftigen. Es wurde ursprünglich auch geschrieben, um mit Modellen zu experimentieren; Beachten Sie, dass die Modellliste jetzt nur noch SD-v1-5 enthält. Ebenso beträgt die Anzahl der Inferenzschritte nur 30. Und die äußere Schleife durchläuft lediglich die Zeilen.
Mehr sehen...Die Frames 1–30 sind sich entwickelnde Inferenzschritte.
Springen Sie also in Zeile 7 hinein: Solange kein Server im Leerlauf ist, senden Sie an jeden Server ein GET, um den Status abzufragen, speichern Sie das Bild, wenn es bereit ist, und markieren Sie den Server im Leerlauf. Dann viel weiter unten, wenn es einen inaktiven Server gibt, POST eine Anfrage, um mit der Generierung des nächsten Bildes zu beginnen. In der HTTP-POST-Anfrage können Sie sehen, wie ein Parameter ausgeblendet wurde, indem Sie ihn mit führenden Xs umbenannt haben. Wenn der Wert benötigt wird, sieht ihn der Server in der Anfrage nicht und verwendet höchstwahrscheinlich einen Standardwert. In meinem Fall kam ich zu dem Schluss, dass die Gesichtskorrektur nicht geholfen hat ... vielleicht war es eine schlechte Entscheidung.
While( 1,
For( irow = 1, irow <= N Rows( dt ), irow += 1,
For Each( {modelname},
{/*"wd-v1-3-full", "animagine-xl-3.0", "dreamlike-photoreal-2.0", */"sd-v1-5"/*, "sd21-unclip-h", "sd21-unclip-l"*/
},
For Each( {infsteps}, {30},
While( (iLH = getLH()) == 0,
For( iLH = 1, iLH <= N Items( LHtable ), iLH += 1,
request = New HTTP Request(
url( LHtable[iLH]["statusURL"] ),
Method( "GET" ),
Headers( {"Accept: application/json"}, {"Content-Type: application/json"} )
);
StatusHandle = request << Send;
If( !Is Empty( StatusHandle ),
statusReport = Parse JSON( StatusHandle );
If(
Is Associative Array( statusreport ) & Contains( statusreport, "status" ) &
statusreport["status"] == "succeeded",
// unpack final image data(s)
If( N Items( statusReport["output"] ) != 1,
Throw( "unsupported multiple items" )
);
For( i = 1, i <= N Items( statusReport["output"] ), i++,
addImage(
Open(
Decode64 Blob(
Regex(
statusReport["output"][i]["data"],
"data:image/jpeg;base64,(.*)",
"\1"
)
),
"jpg"
),
LHtable[iLH]["targetRow"],
Char( statusReport["output"][i]["seed"] ),
Char( statusReport["render_request"] )
)
);
Beep();
LHtable[iLH]["targetRow"] = 0;
LHtable[iLH]["statusURL"] = 0;//
, /*else if*/Is Associative Array( statusreport ) & Contains( statusreport, "step" ) &
Contains( statusreport, "output" ),
Throw( "needs work" );
); //
, //else no status update...
Wait( 1 )
);
)
);
If( iLH > 0, // found idle host...
txt = Regex( dt:prompt[irow], "jean", "john", globalreplace, ignorecase ); // try name tweak
txt = Regex( txt, "angelica", "susan", globalreplace, ignorecase ); // ditto
request = New HTTP Request(
url( LHtable[iLH]["host"] || "/render" ),
Method( "POST" ),
Headers( {"Accept: application/json"}, {"Content-Type: application/json"} ),
// Cinematic, Dynamic Lighting, Serene, by Salvador Dalí, Detailed and Intricate, Realistic, Photo, Dramatic, Polaroid, Pastel, Cold Color Palette, Warm Color Palette
JSON(
Eval Insert(
"\[
{"prompt":" color, Dramatic, HD, Landscape, Realistic, Fantasy, Cinematic, ^txt^",
"seed":^char(randominteger(1,4294967295))^,
"negative_prompt":"nsfw, comic, pokemon, text, caption, words, letters, album, panel, religion, poem, prose",
"num_outputs":1,
"num_inference_steps":^char(infsteps)^,
"guidance_scale":7.5,
"width":512,"height":512,
"XXXXuse_face_correction":"GFPGANv1.4",
"use_upscale":"RealESRGAN_x4plus",
"upscale_amount":"4",
"vram_usage_level":"balanced",
"sampler_name":"euler_a",
"use_stable_diffusion_model":"^modelname^",
"clip_skip":false,
"use_vae_model":"",
"stream_progress_updates":true,
"stream_image_progress_interval":2,
"stream_image_progress":false,
"show_only_filtered_image":true,
"enable_vae_tiling":true
}]\"
)
)
);
// occasionally the first send seems to happen when the server is ... asleep? The 2nd always works?
For( itry = 1, itry <= 10, itry += 1,
GenHandle = request << Send;
If( Is Empty( GenHandle ),
If( itry == 10,
Throw( "failed to connect, stable diffusion not running?" )//
, // else
Write( "\!nwaiting" );
Wait( 1 );
)
, //else
Break()
);
);
// above requested a picture. it is not ready yet, but it gave a URL to ask if ready. Below, ask status once/second.
LHtable[iLH]["statusURL"] = LHtable[iLH]["host"] || Parse JSON( GenHandle )["stream"];
LHtable[iLH]["targetRow"] = irow; //
);
)
)
)
);
Wenn das alles erledigt ist, müssen möglicherweise mehrere Datentabellen mit Bildern in einer einzigen Tabelle kombiniert werden. Ich habe ein wenig JSL verwendet, um durch 1729 Bildpaare zu klicken, um unerwünschte Bilder zu entfernen oder das Beste auszuwählen.
// interactive picture selection
dt = open("f://redwindows/story.jmp");
dta = open("f://redwindows/pic1024A.jmp");
dtb = open("f://redwindows/pic1024B.jmp");
if(nrows(dt) !=1729 |nrows(dtA) !=1729 |nrows(dtB) !=1729, throw("rows") );
irow=1;
newwindow("chooser",
hlistbox(
vlistbox(abox=mousebox(textbox())),
vlistbox(spacerbox(size(300,1)),
hcenterbox(buttons=vlistbox(
hcenterbox(buttonbox("up",irow=max(1,irow-1);updateRow())),
hcenterbox(hlistbox(
vcenterbox(buttonbox("==>",dt:pict[irow]=dtA:pict[irow];updateRow())),
cbox=mousebox(textbox()),
vcenterbox(buttonbox("<==",dt:pict[irow]=dtB:pict[irow];updateRow()))
)),
hcenterbox(buttonbox("down",irow=min(nrows(dt),irow+1);updateRow()))
)),
hcenterbox(rbox = textbox()),
tbox=textbox()
),
vlistbox(bbox=mousebox(textbox()))
)
);
abox<<SETCLICKENABLE></SETCLICKENABLE>
<P><SPAN class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="The middle image is the one I chose for this paragraph, the least bad." style="width: 999px;"><span class="lia-inline-image-display-wrapper" image-alt="The middle image is the one I chose for this paragraph, the least bad." style="width: 999px;"><img src="https://us.v-cdn.net/6038925/uploads/images/61750i8B95C86D958F0606/chooser.png" role="button" title="chooser.png" alt="The middle image is the one I chose for this paragraph, the least bad." /><span class="lia-inline-image-caption" onclick="event.preventDefault();">The middle image is the one I chose for this paragraph, the least bad.</span></span><SPAN class="lia-inline-image-caption" onclick="event.preventDefault();">Das mittlere Bild ist dasjenige, das ich für diesen Absatz ausgewählt habe, das am wenigsten schlechte.</SPAN></SPAN></P>
<P>Sobald sich die Bilder, Audiodateien und Absätze alle in derselben Tabelle befinden, können sie durch Ansteuern von FFmpeg mit RunProgram zu einem Video kombiniert werden. FFmpeg kann problemlos die Bilddaten von stdin akzeptieren, die vom Schreibmechanismus von RunProgram bereitgestellt werden. Erstellen Sie also die komplette Audiodatei und fügen Sie dann die Bilder einzeln ein. Ich habe mich dafür entschieden, ein 30-FPS-Video mit benutzerdefinierten Übergängen zwischen den Bildern zu erstellen. Die Bandbreite für die unkomprimierten 4K-Bilder ist ... sehr groß, daher habe ich ein bisschen Linux-Pipe als Python-Programm hinzugefügt, um zu vermeiden, dass genau dasselbe Bild 30 Mal pro Sekunde erneut gesendet wird. Und gegen Ende habe ich beschlossen, am Anfang jedes Kapitels/Videos eine zusätzliche Folie einzufügen.</P>
<CODE class=" language-jsl">chapter0based = 9; // 0..10, 0 is introduction
Include( "f://redwindows/TTS_wav_support.jsl" );
frame = J( 1080 * 2, 1920 * 2, 0 );
dtstory = Open( "/F:/redWindows/story.jmp", invisible );
dtpict = dtstory;//Open( "/F:/redWindows/pic1024B.jmp", invisible );
nn = 0;
imgrows = (N Rows( frame ) - 2048) / 2;
imgrows = imgrows :: (imgrows + 2047);
imgcols = imgrows;
textTop = 200;
textLeft = imgcols[N Cols( imgcols )] + 100;
textRight = N Cols( frame ) - imgcols[1];
textWide = textRight - textLeft;
dtstory << New Column( "position", formula( Col Cumulative Sum( :nsamples ) ) );
dtstory << runformulas;
//
chapters = 1 |/ (dtstory << getrowswhere( Starts With( paragraph, "CHAPTER " ) )) |/ (1 + N Rows( dtstory ));
audioSampleLength = Sum( dtstory:nsamples[chapters[chapter0based + 1] :: chapters[chapter0based + 1 + 1]] );
nParagraphRows = chapters[chapter0based + 1 + 1] - chapters[chapter0based + 1];
//dtStory = open("y://story.jmp");
sps = 22050;
//dtStory = Open( "f://redwindows/story.jmp" );
allsamples = J( sps + sps + audioSampleLength + sps, 1, 0 );// sps: title slide, sps:lead gray, ... , sps: trail gray
nSamps = sps + sps; // 1 sec silence before and after. extra second before for title
// 22050/30==735 audio samples/frame
// pre-calculate the durations for each frame. there is an extra black frame
// for the second of lead in audio, and an extra at the end.
frameCounts = J( 2 + nParagraphRows, 1, 0 );
pictureRow = J( 2 + nParagraphRows, 1, 0 );
frameCounts[1] = 30;
frameCounts[2 + nParagraphRows] = 30;
iFrameCount = 2;
iRunningSamples = 0;
For( irow = chapters[chapter0based + 1], irow < chapters[chapter0based + 1 + 1], irow += 1,
allsamples[nSamps + 1 :: nSamps + dtStory:nsamples[irow]] = Blob To Matrix( Gzip Uncompress( Eval( dtStory:audio[irow] ) ), "int", 2, "big", 1 );
nSamps += dtStory:nsamples[irow];
If( Mod( irow, 10 ) == 0,
Show( irow );
Wait( 0 );
);
iRunningSamples += dtStory:nsamples[irow];
nframes = Floor( iRunningSamples / 735 );
iRunningSamples -= nframes * 735;
pictureRow[iFrameCount] = irow;
frameCounts[iFrameCount++] = nframes;
);
Show( frameCounts, pictureRow );
f = RiffUtil:WavWriterHeader(
Eval Insert( "Y:\farm\PolarisAudio_Chapter_^chapter0based+1-1^.wav" ),
N Rows( allsamples ),
N Cols( allsamples ),
sps
);
File Size( f );
Save Text File( f, Matrix To Blob( allsamples, "int", 2, "little" ), Mode( "append" ) );
File Size( f );
curFrame = 1;
maxFrame = N Rows( frameCounts );
/* repeater.py - the frames are 33MB each, and repeated many times. This is a pipe program to repeat at the far end without retransmitting.
#!/usr/bin/python3
import sys
source = sys.stdin.buffer
dest = sys.stdout.buffer
while True:
binary = source.read(4)
if not binary:
break #eof
reps = int(binary.decode("utf-8"))
block = source.read(2160*3840*4)
for i in range(reps):
dest.write(block)
*/
tran = New Namespace(
"tran"
);
tran.setup = Function( {ir1, ir2},
{p1, p2, red1, green1, blue1, red2, green2, blue2, gray1, gray2, rank1, rank2},
If( ir1,
p1 = Eval( dtpict:pict[ir1] );
tran.dest = p1 << getpixels;//
, //
tran.dest = J( 2048, 2048, RGB Color( "gray" ) );
p1 = New Image( tran.dest );
);
If( ir2,
p2 = Eval( dtpict:pict[ir2] );//
, //
p2 = New Image( J( 2048, 2048, RGB Color( "gray" ) ) )
);
{tran.nc, tran.nr} = p1 << getsize;
tran.firsthalf = (1 :: 4011707)`;
tran.secondhalf = (4011708 :: (tran.nr * tran.nc))`;
{red1, green1, blue1} = p1 << getpixels( "rgb" );
gray1 = Shape( red1 + green1 + blue1, tran.nr * tran.nc, 1 );
{red2, green2, blue2} = p2 << getpixels( "rgb" );
gray2 = Shape( red2 + green2 + blue2, tran.nr * tran.nc, 1 );
rank1 = Rank( gray1 );
tran.y1 = Floor( (rank1 - 1) / tran.nc ) + 1;
tran.x1 = Mod( (rank1 - 1), tran.nc ) + 1;
tran.r1 = red1[rank1];
tran.g1 = green1[rank1];
tran.b1 = blue1[rank1];
rank2 = Rank( gray2 );
tran.y2 = Floor( (rank2 - 1) / tran.nc ) + 1;
tran.x2 = Mod( (rank2 - 1), tran.nc ) + 1;
tran.r2 = red2[rank2];
tran.g2 = green2[rank2];
tran.b2 = blue2[rank2];
);
tran.next = Function( {itrans},
{alpha, beta, x3, y3, xy, r3, g3, b3, firstz, secondz},
nframes = 30;
// For( itrans = 0, itrans < nframes, itrans += 1,
fract = (itrans + 1) / (nframes); // 0 is already transitioning, 29 is complete
alpha = (Cos( Pi() * fract ) + 1) / 2;// ease in, ease out
beta = 1 - alpha;
x3 = Round( alpha * tran.x1 + beta * tran.x2 );
y3 = Round( alpha * tran.y1 + beta * tran.y2 );
xy = (y3 - 1) * tran.nc + x3;
r3 = alpha * tran.r1 + beta * tran.r2;
g3 = alpha * tran.g1 + beta * tran.g2;
b3 = alpha * tran.b1 + beta * tran.b2;
// workaround problem at 4011707 (works) and 4011708 (fails)
firstz = RGB Color( r3[tran.firsthalf], g3[tran.firsthalf], b3[tran.firsthalf] );
secondz = RGB Color( r3[tran.secondhalf], g3[tran.secondhalf], b3[tran.secondhalf] );
tran.dest[xy[tran.firsthalf]] = firstz;
tran.dest[xy[tran.secondhalf]] = secondz;
//x=[1 2,3 4];x[[2,3,2,3]]=[55,66,77,88];x
//New Window( "", New Image( Shape( dest, nr, nc ) ) );
// should not need shape? Shape( tran.dest, tran.nr, tran.nc );
// rp << Write( Right( Char( 1e6 + 1 ), 4 ) ); //repeater.py expects 4-char repeat count, just 1 of each transition frame
// Wait( 0.1 );
// );
);
New Window( "", bb = Border Box( Text Box( "" ) ) );
rp = Run Program(
executable( "plink" ),
options(
{"dell9010",
// idea from https://stackoverflow.com/questions/34167691/pipe-opencv-images-to-ffmpeg-using-python
"\!"/mnt/nfsc/farm/repeater.py 2>repeater.stderr.txt|ffmpeg", "-y", // overwrite output
"-f", "rawvideo", // tell ffmpeg the stdin data coming in is
"-vcodec", "rawvideo", // "raw" pixels, uncompressed, no meta data
"-s", Eval Insert( "^ncols(frame)^X^nrows(frame)^" ), // raw video needs this meta data to know the size of the image
"-pix_fmt", "argb", // the JSL color values are easily converted to argb with MatrixToBlob
"-r", "30", // input FPS
"-thread_queue_size", "256", // 128 was slow. applies to the following input. is this in frames or some other unit? 8 produces a warning, 1024 is too big?
"-i", "-", // this is the pipe input. everything before this is describing the input data
//"-an", // disable audio, or supply a sound file as another input
"-i", Eval Insert( "/mnt/nfsc/farm/PolarisAudio_Chapter_^chapter0based+1-1^.wav" ), //
"-vcodec", "mpeg4", // encoding for the output
"-b:v", "5000k", // bits per second for the output
Eval Insert( "/mnt/nfsc/farm/polaris_Chapter_^chapter0based+1-1^.mp4" ), // the name of the output
Eval Insert(">ffmpeg.stdout^chapter0based+1-1^.txt 2>ffmpeg.stderr^chapter0based+1-1^.txt\!"")}
),
WriteFunction( // the WriteFunction is called when FFMPEG wants JMP to write some data to FFMPEG's stdin
Function( {this},
//Show( curframe, maxFrame, chapter0based + 1, chapters[chapter0based + 1] );
//Show( rp << canread, rp << isreadeof, rp << canwrite );
//Wait( 0.1 );
write(curframe,"/", maxFrame," ",hptime()/1e6," ");
Try(
If( curFrame < maxFrame,
If( curFrame == 1, // inject title slide
t = Text Box(
"\[
Built with JMP software, FFmpeg, Stable Diffusion
and TTS voices p273 (narrator), p256 (male), and p300 (female)
https://github.com/easydiffusion/easydiffusion
https://pypi.org/project/TTS/
https://github.com/coqui-ai/TTS
Polaris and the Goddess Glorian by Charles B. Stilson
available at https://www.gutenberg.org/ebooks/67121
Compilation Copyright © 2024 Craige Hales
]\",
<<SETWRAP></SETWRAP>
<P>Die angehängten Dateien werden größtenteils oben erläutert. Sie werden nicht einfach daran arbeiten, dieses Projekt zu reproduzieren, nicht ohne ein wenig Nachdenken. Wenn ich etwas ausgelassen habe, was wahrscheinlich erscheint, schreiben Sie es in die Kommentare.</P>
<P><SPAN class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="Is that...surprise? fear? (unused early image.)" style="width: 512px;"><span class="lia-inline-image-display-wrapper" image-alt="Is that...surprise? fear? (unused early image.)" style="width: 512px;"><img src="https://us.v-cdn.net/6038925/uploads/images/61751i3EC31DE70AD150AF/BadDog.jpg" role="button" title="BadDog.jpg" alt="Is that...surprise? fear? (unused early image.)" /><span class="lia-inline-image-caption" onclick="event.preventDefault();">Is that...surprise? fear? (unused early image.)</span></span><SPAN class="lia-inline-image-caption" onclick="event.preventDefault();">Ist das... eine Überraschung? Furcht? (unbenutztes frühes Bild.)</SPAN></SPAN></P>