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>