@Lenciel

江湖儿女(21) - 少吃胡萝卜

这两天百度的一位副总裁被裁,沸沸扬扬。

网上不少信息都指向她在报班学习「打造个人 IP」的课程后,实践用攻击性言论带流量起号的玩法,结果给百度和自己都带来了很恶劣的后果。

我分享一个自己常给团队讲的故事。

据说,在二战中,英国发明了一种新型机载雷达,让飞行员可以在夜间瞄准并击落德国飞机。

英国军方不想让德国知道这项技术,但又必须对大众解释自己不可思议的新能力。

结果,英国想了一个办法:对外大肆宣传英国飞行员通过吃过量的胡萝卜,获得了非凡的视力。

于是,有很多德国飞行员开始大量吃胡萝卜,然后继续被击落,直到真相大白。

这听起来有点天方夜谭,但一旦有意识地思考和关注这类问题,就会发现它普遍存在。

本质上,一旦有人或者组织以不愿或不能公开承认的方式取得成功时,就必须为自己找到「我就只是多吃了一些胡萝卜」或者「经过研究我吃了胡萝卜」之类的借口。

比如:

  • 有很多公司通过某些业务动作(比如广告弹窗、暗扣或者诱导自动续费)赚钱,Ta 们对外宣传自己是因为在某个渠道里面有了一个高 ROI 的打法获得了增长。然后有不少早期公司会学习研究这些打法;

  • 有些公司挂出来的 JD 是有水分的。这个岗位的实际工作范围可能不是那样,甚至这个岗位已经有了内定人选,只是 HR 要把 JD 挂出来走完招聘流程,对外假装还在招聘这个岗位。然后不少人会浪费时间去投这些岗位;

  • 有些人因为人脉、运气、平台本身发展的红利甚至是不正当手段取得了一些成绩。Ta 们会把这些成绩归因到自己的某些战略战术的成功,甚至去售卖自己总结的方法论。然后很多青涩一些的同学会去掏钱学习;

如果去参加一些商业上的局或者行业的会议,可以看到满天的胡萝卜。

这个故事绝不是说,人人都有脏事儿,我们应该把下限放低:就算有一天发现自己的榜样干过奇怪的事情,仍然可以选择自己不干奇怪的事情。

而是商业社会不是微积分,不存在单一的、公开的、完整的信息和结论。因此,辨别和避开胡萝卜,是我们需要掌握的基本技能。

这意味着我们核心关注的那些领域,必须构建个人关系网络,从大量的碎片化的信息里面去尝试形成自己的判断和认知。

千万不能因为自己有焦虑,听到别人说如何投资如何选择如何搭架构如何铺渠道就可以成功,马上去下重注做实验,是有可能让自己下牌桌的。

我看过很多残酷的例子,因为创业圈子,VC 圈子大家特别愿意去聊这些。

但哪怕不是掌握着企业命运,只是个普通员工,能不浪费时间和精力去「吃胡萝卜」,很多时候就已经战胜了 50% 的同龄人了。

焦虑的时候,多砍柴磨刀,少吃胡萝卜。

不花钱的同声传译

最近参加一个技术会议,我请大家举手简单统计了一下参会者在 ChatGPT、Kimi 等产品上的活跃度,发现每天都用的人大大减少了。

这跟我两年前的判断差不多: LLM 对搜索会有很大的冲击(因为用户脑子里有关键词,好写 prompt),但大部分时候,聊天仍然是个非常糟糕的用户界面(问什么怎么问,压力都在用户这边)。

如果让我总结使用 LLM 频度最高的场景,是嵌在飞书里的那些跟开会相关的功能:自动字幕,自动转文本,自动给总结等等。

如果让蒙爷总结,他的最核心场景会是 Youtube 上实时给视频的添加的中文字幕,一下子把他可以看懂的视频扩大了几个数量级。

这些功能都涉及语音转文本再进行处理(记录、总结或者翻译)。由于目前的解决方案基本都是跑在服务器端的,所以也有一些问题。

首先是不够安全。不管是私人的通话,还是工作的会议,把音频录制和处理交给第三方,特别是国内一些厂商,还是让人感觉有点害怕的。

其次是不够灵活。比如,YouTube 自动加字幕的功能,依赖 Google 的服务。本地下载了一部冷门的电影,就还得老老实实花时间去找字幕。

这些问题的解决,核心是下面两个方面:

  • 有没有办法很容易的提取音轨或者转发音频流
  • 有没有办法在本地对音轨或者音频流使用 LLM 进行处理

如果是有钱人,我会推荐 Rogue Amoeba 家的 Loopback 和 Audio Hijack。 Loopback 可以通过创建虚拟声卡对音频进行各种的控制和转发解决第一步。而 Audio Hijack 的新版本里内置了一个 transcribe 模块解决第二步实际上背后也是 OpenAI 的 Whisper ,精确版对应 large-v2 的模型。

advancedblocks-transcribe-frommic.png
图1. 从输入设备对音频流进行录制和转写

如果稍微愿意动动手,我会推荐 ffmpeg +VB-Cable 解决第一个问题,whisper-cpp 解决第二步:因为它们免费,并且开源,完全可控。

下面的步骤针对 Mac,但 Widnes 上相同的思路应该也是工作的。

安装 ffmpeg 和 VB-Cable

ffmpeg 主要用来处理音频文件,安装就直接用 Homebrew。

brew install ffmpeg

然后如果是现场的音频文件,需要处理成 16khz 的 WAV 文件:

ffmpeg -i audio.mp3 -acodec pcm_s16le -ac 1 -ar 16000 audio.wav

如果是视频,则需要抓取里面的音轨做转换:

ffmpeg -i vod.mp4 -hide_banner -vn -ar 16000 -ac 1 -c:a pcm_s16le -y vod-resampled.wav

VB-Cable 是一个安装文件,装好了之后可以看到在音频配置里面多了一块虚拟声卡:

vb-cable.jpg
图2. 输入输出里面都有,因此可以录也可以回放

这个主要是用来捕获音频流的。

安装 whisper-cpp

虽然 Homebrew 提供了 formula 但是我选择了 clone 项目来编译。因为有一些平台相关的优化我不知道 Homebrew 的版本是怎么处理的。

比如,把 Apple Neural Engine (ANE) 用起来 whisper-cpp 能够快最少三倍,但是它需要做一些设置更多关于 whisper-cpp 对 Core ML 的支持,可以看这里

pip install ane_transformers
pip install openai-whisper
pip install coremltools

然后生成模型的 Core ML 版本,比如我使用了 large-v3 版本的模型:

./models/generate-coreml-model.sh large-v3

最后需要打开编译开关进行编译:

# using Makefile
make clean
WHISPER_COREML=1 make -j

当跑起来的时候提示 Core ML 模型正确加载了,就说明成功了:

./main -m models/ggml-large-v3-q5_0.bin ./samples/jfk.wav

...
whisper_init_state: kv cross size =  245.76 MB
whisper_init_state: loading Core ML model from 'models/ggml-large-v3-encoder.mlmodelc'
whisper_init_state: first run on a device may take a while ...
whisper_init_state: Core ML model loaded
ggml_backend_metal_buffer_type_alloc_buffer: allocated buffer, size =     8.80 MiB, ( 1490.55 / 12288.02)

处理音频文件

whisper-cpp 处理本地的音频文件就只需要组合使用参数,比如我要用 8 线程对一部法语电影的音轨做翻译并且生成字幕文件:

./main -m ./models/ggml-large-v3.bin -f samples/movie.wav -l fr -t 8 -osrt --translate

whisper-cpp 有很丰富的参数:

usage: whisper-cpp [options] file0.wav file1.wav ...

options:
  -h,        --help              [default] show this help message and exit
  -t N,      --threads N         [4      ] number of threads to use during computation
  -p N,      --processors N      [1      ] number of processors to use during computation
  -ot N,     --offset-t N        [0      ] time offset in milliseconds
  -on N,     --offset-n N        [0      ] segment index offset
  -d  N,     --duration N        [0      ] duration of audio to process in milliseconds
  -mc N,     --max-context N     [-1     ] maximum number of text context tokens to store
  -ml N,     --max-len N         [0      ] maximum segment length in characters
  -sow,      --split-on-word     [false  ] split on word rather than on token
  -bo N,     --best-of N         [5      ] number of best candidates to keep
  -bs N,     --beam-size N       [5      ] beam size for beam search
  -wt N,     --word-thold N      [0.01   ] word timestamp probability threshold
  -et N,     --entropy-thold N   [2.40   ] entropy threshold for decoder fail
  -lpt N,    --logprob-thold N   [-1.00  ] log probability threshold for decoder fail
  -debug,    --debug-mode        [false  ] enable debug mode (eg. dump log_mel)
  -tr,       --translate         [false  ] translate from source language to english
  -di,       --diarize           [false  ] stereo audio diarization
  -tdrz,     --tinydiarize       [false  ] enable tinydiarize (requires a tdrz model)
  -nf,       --no-fallback       [false  ] do not use temperature fallback while decoding
  -otxt,     --output-txt        [false  ] output result in a text file
  -ovtt,     --output-vtt        [false  ] output result in a vtt file
  -osrt,     --output-srt        [false  ] output result in a srt file
  -olrc,     --output-lrc        [false  ] output result in a lrc file
  -owts,     --output-words      [false  ] output script for generating karaoke video
  -fp,       --font-path         [/System/Library/Fonts/Supplemental/Courier New Bold.ttf] path to a monospace font for karaoke video
  -ocsv,     --output-csv        [false  ] output result in a CSV file
  -oj,       --output-json       [false  ] output result in a JSON file
  -ojf,      --output-json-full  [false  ] include more information in the JSON file
  -of FNAME, --output-file FNAME [       ] output file path (without file extension)
  -ps,       --print-special     [false  ] print special tokens
  -pc,       --print-colors      [false  ] print colors
  -pp,       --print-progress    [false  ] print progress
  -nt,       --no-timestamps     [false  ] do not print timestamps
  -l LANG,   --language LANG     [en     ] spoken language ('auto' for auto-detect)
  -dl,       --detect-language   [false  ] exit after automatically detecting language
             --prompt PROMPT     [       ] initial prompt
  -m FNAME,  --model FNAME       [models/ggml-base.en.bin] model path
  -f FNAME,  --file FNAME        [       ] input WAV file path
  -oved D,   --ov-e-device DNAME [CPU    ] the OpenVINO device used for encode inference
  -ls,       --log-score         [false  ] log best decoder scores of tokens
  -ng,       --no-gpu            [false  ] disable GPU

处理音频流

音频文件不能覆盖所有场景。如果想要在看各种网页或者视频的时候有一个「同声翻译」,或者是在开会的时候有「实时转录」,就涉及到处理音频流了。

把 VB-Cable 设置成输出之后,我们的所有音频就会转发到这个设备上(如果你同时还想听到,可以再甚至个输入)。然后编译 whisper-cpp 提供的 stream 进行监听:

brew install sdl2
make stream
./stream -m models/ggml-large-v3-q5_0.bin -t 8 --step 5000 --length 5000 -l zh

这里因为指定了输出语言是中文,所以如果你播放的是其他语言的内容,whisper-cpp 会尝试进行翻译:虽然效果一般,但是就像 YouTube 生成的自动字幕一样,能够帮助你大概看明白了。

TODO

从音频转文字之后,还可以做一些额外的处理,比如把一个会议的内容交给模型做提纯做归纳总结等等。

相信在各个活菩萨公司开源的大模型的帮助下,端侧的更多场景会从可玩过渡到可用。