← 开发日志

文本进,片子出

上一篇讲了我为什么要做这件事,末了我说,它到底怎么运作,留到后面的文章里再讲。这就是那篇。

那么 MoVox 究竟是个什么东西?我能给出的、最短又不掺水的答案是:一头进去是文本,一头出来是片子。

你丢给它一个普通的 .txt,就是随便哪儿都能免费下到的那种古典小说文本;它还给你的,是成片:每一幕都是水墨画的,每个角色都有自己的声音,画面会动,字幕也压好了。不上云,不用订阅,没有人按帧跟我收钱。整套东西,就跑在我书桌上的那一台机器上。

“片子”这两个字,我得说准一点,它在那句话里其实多担了些分量。MoVox 是一回一回做的:一回进去,一支 MP4 出来。一百二十回,就是一百二十个文件;你要把它当成一部很长的片子,还是一百二十部短片,都随你。我自己当它是一部剧集。

而这不是我随手做成这个样子的。以回为单位,是因为这些书本来就是以回为单位的。它们当年就是这么一回一回讲出来的:说书人讲到最要紧的地方停下,叫你明天再来。这个节奏,我照搬。

既然在抠字眼,名字我也顺带说一说。写了两篇了,MoVox 这个词我一直在用,却从没解释过。

它是两半拼起来的。Mo,就是墨:这些书当年印上去的那一味墨,也是画里用的那一味墨。Vox 是拉丁文里的”声音”。墨与声,两个音节就把这件事说完了——把旧日的文字,交到一副能开口的嗓子上。域名也是顺着下来的,movox.ink;至于这一页顶上那个”墨”字,不是字体,是元代书家的手笔。

我倒是很想说这名字是反复斟酌出来的。老实讲,取名字恰恰是我最不擅长的一环。它很早就冒出来了,然后就再没换过。

话虽如此,这句话还是把所有的活儿都藏起来了。我们把它拆开看。

先说一件事:为什么是一群人,而不是一个程序。

我完全可以从头到尾写成一个大程序,但我没有。MoVox 是一条智能体(agent)流水线,每个智能体只干一件事,干完交给下一个。

这个词我得先定义一下,因为”智能体”这三个字如今被用得太滥了。我说智能体,指的是系统里这么一块:它有自己的活儿、自己的模型、自己的一套指令,以及自己对”这算干完了”的判断。前一个环节做过什么,它不知道也不关心,只要交到它手上的东西是它预期的那个样子就行。

为什么要这么搭?两个原因,而且都是很无趣的工程原因。

第一,这些活儿彼此真的毫无关系。判断一句文言是谁说的,和用水墨画一座山,根本不是同一回事,也不存在哪个模型两样都最拿手。拆开之后,我可以给每个位置配最合适的模型;将来有更好的出来了,换掉一个,别的原封不动。

第二,也是真正救过我的那一条:东西是会挂的。做一回要好几个小时,跑到第三个小时崩了,我可不想从头再来一遍。所以每个智能体都把自己的产出写进数据库,动手之前也先去数据库里看一眼,已经做完的就跳过。你半夜把进程杀掉,第二天早上再开起来,它会从上次停下的那一个自然段接着做。不起眼,也不好看,但项目和演示之间的区别,就在这种地方。

好,五个位置,我们顺着这条线走一遍。

阅读智能体接过原始的小说,把它变成结构。先按回目切开,再逐回往下拆成一段一段:这一段是旁白,那一段是有人在说话;如果是有人在说话,那是谁说的,说的时候是什么情绪。

这一位我想多停一会儿,因为整条流水线是压在阅读智能体身上的。

有一点得先说明白:它后面的每一个环节,没有一个真的去读小说。一个都没有。配音智能体读的不是小说,是这样一行记录——这句话,由这个角色说出,情绪是怒。作画智能体读的也不是小说,是”这几个人,在这个地方,在这个时刻”。动画和剪辑就更靠后了。它们全都照着阅读智能体记下的东西干活,而且它们分不出哪条记录是对的,哪条是一本正经的胡说八道。

也就是说,阅读智能体每错一处,后面四个环节都会一路老老实实把它送上屏幕,谁也不会察觉哪里不对。所以这个位置我盯得最紧,整个项目里,也数它的产出被我一条条人工核对得最多。

而它真正的难处,多半不是大家以为的那一处。难在名字。

拿《三国演义》来说。刘备是刘备,也是玄德,那是他的字。手下人叫他主公,别人叫他皇叔,叫他使君,还有另外十来种叫法,全看是谁在跟他说话、给他几分敬意。同一个人,书里可以在一页之内换四个称呼,而且从不停下来交代一句这是同一个人——因为在 1522 年,但凡读这本书的人都知道。

要是阅读智能体没能分辨出来,刘备从第一回走出来就带着五个不同的嗓子。所以后面还有一整道工序:把整本书里出现过的名字都收上来,判断哪些是同一个人,然后合并成一个。这真是一步错步步错,上面说的那种连锁反应就以最纯粹的形式发生:说话的人认错,声音就配错,画面里站着的人也跟着错,而且底下没有一个环节会举手告诉你。这一步,我会单独给它写一篇。

配音智能体给每个角色一副只属于他的嗓子,铸一次,往后一直用。

我用”铸”这个字是认真的,不是”挑”。我不是从一堆现成音色里选一个。我用自然语言写一段人设,比如一位上了年纪的老将,嗓子里带沙,从不着急,声音是从胸腔里出来的,模型照着这段描述生成一副嗓子。然后把这副嗓子铺到十五种情绪上存起来,这样同一个角色可以发怒、可以哀求、可以倨傲,听上去还是他本人。

这里头有个小设计我挺得意。随机数的种子是从角色名加上那段人设文字里算出来的,所以同一段描述,永远生成同一副嗓子,跑多少遍都一样;描述改动一个字,你就是有意换了一个人。这不是我绕开的限制,这是我要的那个开关。

至于旁白,那会是我自己的声音。当初的目的就是用我的声音去读书。

作画智能体为每一幕画一张水墨关键帧。这一回念到这一段的时候,你看着的就是这张画。

说老实话,画出一张好看的水墨,如今反倒是容易的那一头,模型这方面很行。难的是:孙悟空在第二回和第八十回,得是同一只猴子。你让模型把同一个角色画两遍,回来的多半是两个表兄弟。所以每个主要角色先做一套角色设定图,也就是同一张脸的一组参考图,我再拿这组图去训练一个小小的附件出来。这就是 LoRA——如果你在别处见过这个词的话:它不是一个新模型,只是给现成模型挂上去的一个轻量附件,专门教会它一件很具体的事,这里就是一张很具体的脸。训练很便宜,还能几个叠着用;三个角色同框的时候,这一点就很重要了。

动画智能体把静止的关键帧拿过去,给它几秒钟的动感。这里我要把话说清楚:这不是动画片,没有人在对口型。水在动,衣袂在动,烟在飘,镜头在呼吸。八到十秒,一张画活过来。这是我想要的那个分寸,我也认为对这些故事来说,这个分寸是对的。

剪辑智能体负责合拢。它把每一段画面抻到跟那一幕旁白一样长,统一放大到 1080p,压上字幕,再把整回接成一支完整的文件。一回进去,一支 MP4 出来。

字幕这一件,比”把字打到画面上”要麻烦得多。要让一行字跟念这行字的声音严丝合缝地一起出现、一起消失,这个要求会一路往上游返工:它改变了音频最初是怎么生成的,也让每一个音频文件旁边都多出一个文本文件,专门记下那一段究竟念了什么、是怎么断开的,就为了让剪辑智能体后面能把两边对上。这个也够单独写一篇。

那么,我人在哪儿?

在他们中间。这是我最在意的一环,也是最容易在流程图上被漏掉的一环。

整条线上都设了关口,我就坐在那些关口上。我给数据库写了一个网页版的查看工具,好让我把阅读智能体的产出一条条读过去,看它哪句认错了。角色设定图不经我过目点头,一步也走不下去。音频要抽查。没有经过人看过、点头的东西,进不了成片。

这件事我已经专门写过一篇,这里就不重复。一句话:模型没有品味,人才有。不是流水线废料,而是手艺。

接下来是老实交代的部分,与其让一张漂亮的流程图暗示些什么,不如我自己先说清楚。

五个位置,还没有坐满。

阅读智能体是能跑的,跑的是一整部长篇(三国演义),一百二十回一回不落。名字合并能跑。人设撰写能跑。配音也能跑:角色能拿到铸好的嗓子,整回的文字能被念出来。这半条线是真的,是活的,产出我一段一段听过。

作画和动画这两个,设计做完了,也已经接进流水线里,但目前还是空壳。所谓空壳,就是房子盖好了,门装上了,电线也埋进墙里了,屋里还一件家具都没有。今天这条流水线跑到那两步,是直接穿过去的,什么也不产出。

剪辑智能体是个例外,而且是个让人高兴的例外。它已经能干活了,而且是从旁边绕进来的:我最早写它,是为了调音频——我得真的把一回从头播一遍,才听得出哪句配坏了,总不能一个一个去点那几百个音频文件。可为了做出这么个东西,字幕对时就得真刀真枪地解决掉,而现在这一块已经很稳了。所以它今天交出来的,是一支货真价实、能坐下来看的整回文件:声音是对的,字幕是对的,时间也是对的——只不过底下垫着的,是一块灰色的底板,那儿将来要放画。

那块灰底,正好是这个项目现在的写照。就此刻而言,MoVox 会读,会说,也会剪。它还不会看。接下来做的就是这个。而所谓公开构建,就是把线画在它实际所在的位置上,而不是画在我希望它在的位置上。

上面每一个决定,背后都压着三条约束,每一条我都会单独写一篇,这里只先插一下标记。

全部在本地跑,用我自己的硬件,不向任何人发一次 API 调用。流水线里每一个模型都必须是许可宽松的开源模型,Apache 2.0 或者 MIT;而我这么做的理由不是理想主义,是一个很实际的考虑,等写到那篇再说。还有,这一切得塞进一块消费级显卡里——这一条对设计的挤压,比你想的要大得多。

这三条,几乎就是这个项目里全部有意思的工程。工欲善其事,必先利其器。这个项目很大一部分工夫,就花在磨工具上。

这就是整个流水线。一头进去是文本,另一头一回一回地出来片子:五个不知疲倦的专才做出来的,外加一个人,在后面一件件替他们验货。

而在这条线的最尽头,依然站着那个读不了原著的孩子——他照样能拿到那一整段冒险。