JMP的Run Program和HTTP Request使用AI工具和FFmpeg來製作影片。
YouTube 上已完成的項目.
從https://www.gutenberg.org/ebooks/67121取得一本書並將文字儲存到磁碟。編寫 JSL 將文字放入資料表中,每行一段。
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;
要試驗多個讀者的聲音,請創建一些啟發式方法來確定引號是由男性還是女性說出的文本引起的。跳過這一步並僅使用最好的旁白聲音可能也是一個不錯的選擇;切換聲音有點刺耳。
// 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;
)));
這些模式,malePattern 和femalePattern,識別如下結構:
「現在,在所有這些事件中,我想我終於找到了自己的出路,」他嘀咕道。
或者
「你確實做得很好,好奧萊里克,」她很快地說。 “我的國王會原諒你失去法德梅,它的損失肯定不是你的錯。而這些——這些對我來說值得很多法德梅。”她挑選了兩顆大小適中、光澤良好的珍珠,送給了奧萊里克。
其中他和她也可以是人物的各種名字和暱稱。並說可以從一大堆動詞中提取出來......作者一定在循環瀏覽它們時感到有些樂趣。
接下來,逐行應用這些模式,並使用一些交替說話者的邏輯和啟發式失敗的特殊情況(865)。 (稍後將使用章節編號來使像 IX 這樣的羅馬數字表示「九」。我認為這不是訓練資料的一部分。)
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 )
);
);
作者使用了一些嚴格的引文規則,現在可能會有所不同; JSL 利用了它們的剛性。當同一發言者在下一段中繼續時,總是會省略尾部的右引號。如果兩位發言者交替發言,一旦建立了順序,他說/她說的部分就可以省略。而上一段的結尾可以確定下一段的說話者。
當飢餓的男人們吃完飯後,女孩再次開口:
“請問,來自海上的男人,我聽說有一位美麗的女士在阿德拉茲鎮的監獄裡等著你,是真的嗎?”
「是的,女士,這是真的,」北極星說。他嘆了口氣。
“你率領一位偉大的主人去那裡釋放了她?”女孩堅持說。
“是的,如果可以的話。”
“但是要前往阿德拉茲,你必須佔領巴姆這座堡壘;你會發現它是一個很難破解的難題。不是嗎?”
“這也是真的,女士。”
“好吧,聽你說,夥計。”女孩站起身來,來到桌邊。 「你們這些對女人忠誠的人,很少有男人會忠誠;也許可憐的、被鄙視的、被拋棄的勞拉可以在這件事上為你們提供一些幫助。”
音訊儲存在表達式列中,其中包含單擊時播放音訊的事件處理程序。 WavWriter 函數是另一個檔案中的 JSL,位於下方的某個位置...
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 );
);
上面的 JSL 中有相當一部分是對文本進行一些額外的修改;人工智慧敘述者接受的訓練方式不起作用,因此它用代替,以獲得所需的停頓。然後它必須修復一些以 開頭的引用,以使它們正常工作。其中很多內容都是針對這本書和敘述者的。最後,paragraphToSamples 產生 22.050kHz 音頻,然後將其壓縮並儲存在表達式列中的 blob 中。
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.除了段落文字之外,還有音訊和圖片欄。音訊列有一個點擊處理程序來播放二進位資料。這是1729段的結局。在查看製作圖片的程式碼之前,讓我們先深入 paragraphToSamples 中看看第一個新的 HTTP 請求。第一個函數 getSamples 顯示了在 TTS(文字轉語音)伺服器上執行 GET 的新 HTTP 請求的邏輯。第二個函數 paragraphToSamples 將段落分割為語音片段,對每個片段使用 getSamples,並將該段落的樣本連接在一起。新 HTTP 請求使用這些欄位將發言者姓名和文字打包到 HTML GET 請求中,發送到本機電腦(Python,如下)上執行的伺服器。
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
);
本機伺服器的 Python 程式碼如下所示(這與上面的 getSamples 的工作方式相符):
# 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)
Python程式碼在Linux計算機上運作; Windows 上不提供 TTS 支援。 HTTP 請求並不在乎!感謝@ masafumimori 提供的程式碼,讓我免於重新學習 Flask。
$ 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
TTS 伺服器沒有任何 cuda 支持,在舊硬體上運行,並具有上面選擇的聲音...足夠快 - Windows 沒有超時等待 GET 請求提供答案。通常,答案大約在播放音訊的同時返回,但對於其他語音,時間可能會更長。如果這成為問題,可以使用輪詢查詢重寫 Python TTS 伺服器,類似於簡單擴散伺服器的工作方式(如下)。
(如果我擁有具有大量內存和 Cuda 核心的 Nvidia 顯示卡以及具有更大電源的新型計算機,我可能會獲得 10 到 100 倍的性能提升。)
TTS 伺服器依賴這個套件:
pip install -U TTS
https://pypi.org/project/TTS/
https://docs.coqui.ai/en/latest/installation.html
圖片
截至 2024 年初,您可以找到許多可以從文字產生圖片的網站。免費網站的速度往往很慢,一張圖片需要一到五分鐘。您也可以下載許多網站正在使用的穩定擴散軟體和訓練模型。 https://github.com/easydiffusion/easydiffusion適用於 win 和 linux。下面的程式碼假設一組四台沒有任何cuda 支援的Linux 機器,並且每兩分鐘製作一張圖片(更快,更慢,取決於...)似乎有幾個API 用於穩定擴散的基於REST 的Web 版本。下面的程式碼適用於簡單擴散版本。
使用前面、當前和後面段落的文字為每個段落建立圖片。其他一些關鍵字(顏色、戲劇、高清、風景、現實、幻想、電影)和一些否定關鍵字(nsfw、漫畫、寶可夢、文字、標題、單字、字母、專輯、面板、宗教、詩歌、散文) 。諸如文字、標題、文字、寵物小精靈、漫畫等負面詞彙是為了避免出現諸如此類的圖像。
Not sure how many points this card is worth.不知道這張卡值多少分。
I can't look at it without trying to read it.我不能不嘗試閱讀它就看它。
Word balloons!文字氣球!
What are they saying?他們在說什麼?
無論如何,其中一些最終出現在視頻中;負面的詞語只是讓人工智慧模型遠離圖像的建議。
如上所述,音訊 TTS API 使用 HTTP GET 來檢索音訊樣本(.wav 或 .mp3 之類的檔案)。它工作正常,因為 GET 能夠計算答案並在幾秒鐘內返回它。如果花費了一分鐘左右,許多工具(Windows、JMP、瀏覽器)會假設連線已中斷並產生錯誤訊息而不是結果。下面的 Easy Diffusion API 會在伺服器上觸發一個較長的過程,在我的慢速機器上需要幾分鐘。我沒有找到足夠好的API描述,於是求助於使用FireFox或Chrome瀏覽器的F12檢查來研究瀏覽器和伺服器的互動。在 JSL 中,您將看到 JMP 的作用類似於瀏覽器,使用關鍵字和參數 POST 請求,然後從伺服器取得狀態,直到圖片準備就緒。在初始 POST 中,伺服器會回覆請求特有的狀態 URL。 JSL 變得更加複雜,因為它保留了四台並行運行的機器的伺服器和狀態 URL 列表,每台機器獨立地處理資料表的不同行。
如果該行的當前圖像列已全部填充,addImage 例程的原始設計將向表中新增一個新的圖像列;我的想法是讓它運行一段時間...最後,我使用了來自不同表的兩列的圖像,參數略有不同。
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^" );
);
多個伺服器開始時是單一 LocalHost。伺服器表還保存狀態 url 和行號,因此當狀態 url 最終給出圖片時,可以將其儲存在右側行中。
// 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 );
);
合併段落三元組並刪除一些不需要的字元的單獨步驟。不知道為什麼提示是一個表達式,它應該是字符,但無論如何都有效。此版本使用if(1來避免測試適度長度的段落。
穩定擴散模型中沒有從一張圖片到下一張圖片的記憶。三段式是模擬長期記憶的糟糕嘗試。圖片中的人物會隨著影像的不同而改變髮色、服裝等。可能會出現更好的模型。
// 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;
);
);
最後一個整體區塊正在完成所有繁重的工作。它使用上面的 LH 結構來保持多個伺服器繁忙。它最初也是為了模型實驗而寫的。請注意,型號清單現在只是 sd-v1-5。同樣,推理步驟的數量僅為 30。而外循環只是遍歷行。
看更多...第 1-30 幀是不斷發展的推理步驟。
因此,跳到第 7 行附近:當沒有空閒伺服器時,向每個伺服器發送 GET 以獲取狀態,如果準備好則儲存影像並將伺服器標記為空閒。然後再往下,如果有空閒伺服器,則 POST 請求以開始產生下一個映像。在 HTTP POST 請求中,您可以透過使用前導 X 重新命名參數來查看參數是如何隱藏的;如果需要該值,伺服器將不會在請求中看到它,並且很可能會使用預設值。就我而言,我認為臉部矯正沒有幫助……可能是個糟糕的決定。
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; //
);
)
)
)
);
當這一切完成後,可能會有多個影像資料表組合成一個表。我使用了一些 JSL 來幫助我點擊 1729 對圖像,以刪除令人反感的圖像或選擇最好的圖像。
// 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();">中間的圖像是我為本段選擇的圖像,最不糟糕的圖像。</SPAN></SPAN></P>
<P>一旦圖片、音訊和段落都在同一個表格中,就可以透過 RunProgram 驅動 FFmpeg 將它們組合成影片。 FFmpeg 可以輕鬆接受 RunProgram 寫入機制提供的來自 stdin 的影像資料。因此,建立完整的音訊文件,然後一次輸入一個圖像。我選擇製作一個 30FPS 的視頻,並在圖片之間進行自訂過渡。 4K 未壓縮圖片的頻寬…很多,因此我添加了一些 Linux 管道作為 Python 程序,以避免每秒重新發送完全相同的圖像 30 次。最後,我選擇在每章/影片的開頭插入一張額外的投影片。</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>所附文件大部分已在上面進行了解釋。他們不會只是不假思索地複製這個項目。如果我漏掉了一些東西(這似乎是可能的),請在評論中大肆討論。</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();">這是……驚喜嗎?害怕? (未使用的早期影像。)</SPAN></SPAN></P>