在[上一篇文章](/writing-and-video.html)中，我们探讨了为什么知识型视频的信息密度这么低，但是还有很多up主去做。
受大家的讨论启发，我做了一个小工具，可以在不离开YouTube或B站App的情况下，对视频进行语音识别，从而对视频的内容进行速读。
在有了这个工具之后，我发现即使是那些比较长的视频也没有太多的恐惧心理了。
以前在收藏夹里吃灰的视频也敢拿出来看了。
基本上，30到60分钟的视频花一两分钟的时间就可以知道哪些部分是值得精读的，哪些部分是可以跳过的。
一方面提高了效率，另外一方面也降低了看知识型视频的门槛。
这个产品现在已经发布在了[我的网站](https://yage.ai/caption/)上给大家免费使用。

我目前没有把它做成一个AI summary的形式。
原因是我个人的感受是现在AI做总结和缩略还是非常屎，经常丢失insights，所以还是需要人类来看原始的script。
但是我们总可以手动复制粘贴识别出来的脚本的内容，然后使用[GPT进行更多的问答](/GPT-API-usage-creation.html)。
下面会提到我对于iOS和Mac还特别做了一个shortcut，这样和GPT等第三方app结合起来就更方便了。
这篇文章的主要内容其实是分享这些技术上的收获。

1. **YouTube视频如何下载**：这里我使用的是一个Python库叫做yt_dlp，它可以直接下载B站和YouTube上的视频、音频、字幕和其他原数据。但有一个很坑的地方是从最近几天开始，YouTube开始对类似的库甚至包括自己官方的客户端进行限制，要求用户必须登录才能下载视频。所以网上建议的一种解决方法是把登录之后的cookies放到这个工具里，然后就可以模拟登录。但是我在试验之后发现，这样的确可以下载一两个视频，但是过了几个小时或者下载视频个数多了以后，这个cookie会失效，Google还是会让你登录。最终的解决方法是使用OAuth2来进行登录。在一次登录过后，接下来下载视频就不会出现类似的警告了。

2. **灰产**：可能是因为上面提到的Google最新的限制，现在网上很多YouTube下载网站都用不了了。所以一种变通的方法是大家会使用浏览器上的插件来下载视频，因为这些插件可以access网站的cookie，所以它可以模拟我们进行登录，从而直接解析视频地址。但很显然这也带来了一个安全隐患。据小道消息，有灰产在出10美元一个用户的价格来购买这些插件的所有权。

3. **功能和性能要求**：这个工具首先调用OpenAI的Whisper API对音频进行语音识别，然后再调用GPT API给识别出来的原始文本加入标点符号分段来增加它的可读性。所以程序的主要任务就是提供一个fast API的接口，然后在后台调用OpenAI的各种API就可以。所以服务器本身并不需要多强的性能。

4. **通过异步模型增强用户体验**：我们往往是在看比较长的视频的时候才会需要用这样一个工具，因为短的视频我们直接加速看完就拉倒了。这就导致我们不论是语音识别还是GPT重整文本都会花费比较长的时间，因此传统的纯阻塞的Web API对于用户的体验就特别差。它需要在一个网页或者客户端前面傻等。对于一个20分钟的视频，我们可能需要5分钟左右才能完成整个流程。在整个过程中间我们也没有任何进度的反馈，也不知道程序有没有卡死，或者网络连接断开，整个体验特别差。

    所以一种可能的方法是我们把后端的API设计成一个基于任务的形式，提供几个不同的API，让客户端可以提交一个新任务，然后返回一个任务的ID，然后用这个ID可以去查询这个任务的进度。当进度到100%以后再使用另一个API去拿到语音识别和GPT重整的结果，这样就可以实现状态的频繁更新，也能让客户端进行异步处理，用户体验就因此好了很多。

5. **Python做并发Web Service**：因为我的后端用的是Python来实现的，所以有一些关于Python的独特的挑战。到目前为止，Python的解释器仍然是有进程锁的，换言之它不能进行多线程操作，而只能进行多进程操作。这对于web service来说是很不好的，因为每一个进程就没办法并发，响应多个请求。

    这个有两种解决方法，一种方法是使用异步编程，让我们在等待OpenAI的服务器的过程中把程序的控制权交还给CPU，让它能够响应另一个请求。另一种方法是我们用类似gunicorn之类的Process Manager来开启多个进程。两种方法都是能实现目的的，但是也有各自的缺陷。

    第一种方法只对那些IO密集，比如经常需要等待磁盘或者网络的程序比较实用。对于我们的情况，我们有一个步骤需要对下载下来的音频文件进行重新转码。在这个过程中，CPU是完全被占用的，因而无法实现并发处理。而第二个方法它的问题在于进程之间是没办法实现内存共享的。所以我们不能通过共享变量的方式来管理任务。而对于这种每个任务的生命周期非常短，可能只有几分钟的情况而言，使用内存或者说共享变量来管理可能是最适合的方式。但是为了让不同的Python进程之间都能够共享这样的任务数据，我们必须采用更复杂的方案。用一个数据库或者Key-value Pair的后端，比如MongoDB或者Redis来共享数据。

    因此最终我们采用的API的架构是这样的。我们使用了异步编程，同时使用了gunicorn，数据通过后台的Redis来保证统一性，同时加入cache。此外说句题外话，为什么我们要对音频进行重新转码，是因为OpenAI的API有一个25M的文件大小限制。对于一些比较长的音频，比如几十分钟的那种，我们必须要把它转成低码率的音频才能让文件不至于超出25M。

6. **GPTS的尝试**：我还试着在做了网页端以后，把这个服务拓展到GPTS上。原因是GPTS本身会带来流量，而且这样在调用GPT API的时候就不用花我的钱，而是花每个用户自己的包月费用。

    但是在尝试了一段时间之后，我还是放弃了GPTS这个方向。主要原因是，如果你只是把它当作分发prompt的一种方式，那GPTS是很好用的。去附用自己或者团队经常使用的prompt的话，GPTS是很好用的，但如果你一旦想要正儿八经地把一些比较特别的能力带进来，就会发现它有这样那样的坑。

    其中一个是它对于跟第三方的交互，其中有两个坑对我来说尤其严重。第一，因为它的交互方式是会话式的，所以它做不到像我们的网页端那样每隔几秒钟去轮询一下任务状态的API。如果没有做完就继续等，如果做完了就显示结果。它只能类似回合制游戏一样，你戳他一下说好了没，他去看一眼说没好。然后控制权就完全交到你手里了。你只能下一次再戳他问好了没，他再跟你说结果是什么。

    所以它本质上跟我们的基于任务的API的架构就是不兼容的。为了适配GPTS的这种交互方式，我只能把API变成一个单点的API，识别和转识别的时候就彻底阻塞，直到识别彻底完成再返回。对于短一点的视频来说，它和GPTS结合得很好，GPTS可以利用这个单点API给出相当好的结果。但一旦我们真的在长视频上测试时，就发现这样的API总是超时。这个超时不是因为我们的web server超时，而是因为GPTS对于跟第三方API交互的时间限制得特别死。一旦超过大约半分钟到一分钟左右，它就会觉得这个API挂了，然后报错。

    因此，这两个限制直接把我们想象的这个产品给卡死了。所以我在努力了一段时间之后也没有办法在GPTS上支持长视频，而长视频是我们这个产品更有价值的地方。所以后来只能被迫放弃了GPTS这个方向。

7. **GPT偷懒**：在做完语音识别之后，我们还要有一个GPT重整的过程。这个过程的开发也非常有意思，其中最严重的问题在于模型是会偷懒的。尤其是当识别出来的文本比较长的时候，你让GPT去重整，它经常第一段第二段还在老老实实干活，第三段就比输入短了很多。第四第五段就直接跳过了，然后直接到最后两段把它重整一下，敷衍了事。而且不仅是GPT，常见的开源模型也有类似的问题

    这方面表现最好的是Claude V3，不论是最便宜的haiku还是最贵的opus都没有偷懒的问题。但是它有另外一个很严重的问题，就是:

8. **Context window的限制**: 虽然现代的LLM的context window看起来都很长，比如Claude v3是200K，GPT-4是128K，但是他们都暗戳戳的留了一个输出长度的限制。这两个模型都是4096，这就意味着即使你的输入很长，比如一个视频它识别出来的文本有15000个token，但是你没办法一下把它全部给重整出来。原因是API最多就只能输出4096个字符，然后就截断了。

    所以为了让输出能够限制在4096的范围内，我们必须要把输入给截成，比如三四千个token的小块，然后再把小块扔给API处理，最后再通过某种方式把处理后的结果给拼起来。这就是一个非常讽刺的事情。在4K，8K的年代里，我们没少干这种恶心的事情，到了128K，200K的年代，我们以为终于可以摆脱这种又丑陋又低效的方式了。结果没想到因为output window的限制，我们还是得被迫干这种拆分再合并的hack，非常痛苦，而且往往还会出现consistency的问题，比如第一段用的是输出用的是简体中文，第二段变成了繁体中文。合并以后看起来就会很奇怪。

8. **Shortcut**：在网页端和GPTS之外，我们还尝试了一些其他的产品形式，其中之一是shortcut。Shortcut是iOS上面的一个工具，它可以让你在手机上通过iCloud直接分发你的程序并且调用各种app的功能。所以基本上我就把[网页端的功能](/wechat-bot.html)在shortcut上[重新实现了一遍](https://www.icloud.com/shortcuts/4db043a4acb74152be3d8833afaeb9c2)。感兴趣的同学可以试试。它是一个非常友好的编程环境，但有条件的话最好在iPad上试，体验比在iPhone上要好很多。它的能力还是很强的，基本上可以实现我们客户端的所有功能，包括任务的管理，包括轮询。

    但在此之外它有两个功能特别好，第一是它可以和iPhone的其他组件结合，比如它可以作为分享的一个对象。当我们在B站看到一个视频，想要对它做语音识别的时候，我们可以直接点分享，然后再点我们的shortcut，就可以开始语音识别了。不用去复制它的URL到我们的网页上，然后粘贴URL，然后点回车。整个过程就非常流畅，你都不用离开B站APP，而且在它识别的过程中我们也可以正常的操作用手机。它就会有一个进度条在灵动岛上面显示，整个过程完全是后台进行的，非常无痛。
    
    第二个好的地方是，它的结果也可以以多种形式加以利用，比如我的shortcut里面，会不仅把结果显示出来，而且会把结果复制到剪贴板里。这样当你想要去再粘到其他APP里，比如notes，或者notion里面的时候就很方便。同时它也可以直接把这个结果放到与其他APP交互，比如你可以自己编辑这个shortcut，给它加上一个自定义的prompt，然后扔给ChatGPT，让它做问答。或者直接把它插入Notion里面，新建一个page，都是可以的。

总的来说，以上就是我在做这个project的过程中得到的技术收获。罗里吧嗦讲的一大堆，也是想跟大家分享一下，我的开发流程大约是什么样的，在整个过程中间是怎么又玩耍写代码，又学习积累经验的。也很好奇大家对于这个工具的想法。我的网页端在[这个URL](https://yage.ai/caption/)，Shortcut可以在[这里](https://www.icloud.com/shortcuts/4db043a4acb74152be3d8833afaeb9c2)下载。目前都是免费的工具，也很期待大家的反馈。

<script async data-uid="49b08a3e73" src="https://yage-ai.kit.com/49b08a3e73/index.js"></script>
