CocoIndex Rust 示例深度解析:用 Whisper 把音频文件夹增量转写成 Postgres 索引表
2026/9/15 21:31:08 网站建设 项目流程

CocoIndex Rust 示例深度解析:用 Whisper 把音频文件夹增量转写成 Postgres 索引表

【免费下载链接】cocoindexIncremental engine for long horizon agents 🌟 Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/co/cocoindex

本篇技术指南围绕 CocoIndex 仓库中的 Rust 示例 audio_to_text 展开,讲解如何用一段约 150 行的 Rust 程序,把本地目录中的音频文件逐个交给 OpenAI Whisper(whisper-1)转写,并以文件名为主键、每个文件一行地写入 Postgres。读完本文,你将掌握 CocoIndex Rust SDK 的完整调用链:fs::walk_items目录遍历、#[cocoindex::function]组件声明、ApiTranscriber转写、mount_table_target托管目标,以及增量处理(未变更文件免转写、删除文件自动清理孤儿行)的底层原理。

示例定位:把"死音频"变成可查询的转写表

一个存放语音备忘录、会议录音、播客片段的文件夹,在没有转写文本之前几乎是"死数据"。本示例的核心思路极其简洁——不做分块(chunking),每个文件对应一行

  • Walk:递归遍历本地目录,按常见音频扩展名(.mp3.wav.m4a.flac.ogg.webm.aac.aiff)过滤文件;
  • Transcribe:用 OpenAI Whisper(默认模型whisper-1)转写每个文件;
  • Store:在 Postgres 中为每个文件写入一行AudioTranscription,以filename为主键。

最终得到的表coco_examples.audio_transcriptions只有两列filename(主键)和text。由于主键就是文件名,这张表同时充当"哪些文件已被转写"的索引——无需任何额外的记账表。你可以直接对它做 SQL 查询、JOIN 其他表,或者把它喂给后续的 embedding 管线。

示例的默认输入目录 examples/rust/audio_to_text/audio_files 内置了两个测试音频(greeting.wavpipeline_note.wav),clone 仓库后即可直接运行验证。

与 Python 示例的平行对照

本示例是仓库内 Python 示例 examples/audio_to_text 的 Rust 移植版。官方 README 给出了四行核心对照,四个关注点(来源、逐文件计算、转写、目标)在两个语言实现中一一对应:

关注点PythonRust(本示例)
数据源localfs.walk_dircocoindex::fs::walk
逐文件计算@coco.fn(memo=True) process_file#[cocoindex::function(memo)] transcribe
转写服务LiteLLMTranscriber("whisper-1")OpenAI/v1/audio/transcriptionswhisper-1
目标端postgres.mount_table_targetpostgres::mount_table_target

从实现层面看,Python 版在 examples/audio_to_text/main.py 中用@coco.fn(memo=True)修饰process_file,Rust 版在 examples/rust/audio_to_text/src/main.rs 中用#[cocoindex::function]修饰transcribe,两者语义相同:函数级 memo 使未变更的音频文件绝不会被重复转写

一个值得注意的差异:Python 版通过LiteLLMTranscriber走 LiteLLM 的转写接口(模型字符串可随意换成elevenlabs/scribe_v1或自托管端点),而 Rust 版直接调用 OpenAI 兼容的/v1/audio/transcriptionsHTTP 接口(由 SDK 的ApiTranscriber封装)。两种实现的增量语义完全一致。

源码剖析:端到端管线是怎么拼起来的

依赖与特性开关

先看 Cargo.toml。示例依赖本地路径的 SDK crate,并通过feature 开关按需启用能力:

cocoindex = { path = "../../../rust/sdk/cocoindex", features = [ "postgres", "embed_api", ] } tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } dotenvy = "0.15"
  • postgres:启用 Postgres 目标端(postgres::mount_table_targetTableTargetTableSchema);
  • embed_api:拉入 SDK 的ops::api::ApiTranscriber(Whisper 转写能力),示例不再手写 HTTP 请求;
  • dotenvy:启动时自动加载.env文件,便于管理OPENAI_API_KEYPOSTGRES_URL

这种特性开关设计是 CocoIndex SDK 的一贯做法:不用的连接器不会进入编译产物。

目录遍历:cocoindex::fs::walkwalk_items

示例用 src/main.rs 定义匹配模式:

const AUDIO_PATTERNS: &[&str] = &[ "**/*.aac", "**/*.aiff", "**/*.flac", "**/*.m4a", "**/*.mp3", "**/*.ogg", "**/*.wav", "**/*.webm", ];

然后调用walk_items(&sourcedir, AUDIO_PATTERNS)?得到Vec<(String, FileEntry)>——每个元素是"稳定 key + 文件条目"。其底层实现位于 rust/sdk/cocoindex/src/fs.rs:walkwalkdir递归遍历并依据 glob 模式过滤文件,walk_items再为每个文件计算稳定 key。从 fs.rs 可见 key 就是相对路径(正斜杠分隔),例如sub/recording.mp3;文件按相对路径排序返回,保证遍历结果确定性。

FileEntry提供两类关键 API(fs.rs):

  • content():读取文件原始字节(转写需要原始音频);
  • relative_path()/key():用于拼转写上传文件名与目标行主键;
  • stem():去除扩展名的文件名。

转写核心:ApiTranscriber

src/main.rs 中的transcribe函数把转写封装成一个 memo 化的组件函数:

#[cocoindex::function] async fn transcribe(_ctx: &Ctx, file: &FileEntry) -> Result<String> { let bytes = file.content()?; // Whisper 通过上传文件名的扩展名嗅探音频容器, // 因此必须保留扩展名再上传。 let name = file .relative_path() .file_name() .and_then(|n| n.to_str()) .unwrap_or("audio") .to_string(); let mut transcriber = ApiTranscriber::new(TRANSCRIBE_MODEL); // "whisper-1" if let Ok(base_url) = std::env::var("OPENAI_BASE_URL") { transcriber = transcriber.with_base_url(base_url); } transcriber.transcribe_bytes(bytes, name).await }

ApiTranscriber定义在 rust/sdk/cocoindex/src/ops/api.rs,其行为要点如下(均有源码可证):

  • ApiTranscriber::new(model)默认使用 OpenAI 基础 URL,并从OPENAI_API_KEY环境变量读取密钥(api.rs);
  • with_base_url()可覆盖端点,因此示例支持OPENAI_BASE_URL环境变量以对接OpenAI 兼容的自托管端点(api.rs);此外还有with_api_key()显式指定密钥、with_language()传入 ISO-639-1 语言提示(如"en")等可选方法;
  • transcribe_bytes(bytes, filename)以 multipart 表单 POST 到{base_url}/audio/transcriptions,字段为modelfile,带 Bearer 认证,返回 JSON 中的text(api.rs)。

关键细节:Whisper 服务端靠上传文件名的扩展名嗅探音频容器格式,所以示例特意从relative_path()取出带扩展名的文件名再上传——若统一传audio这样的名字,.wav之外的格式可能识别失败。

Postgres 目标:mount_table_targetTableSchema

src/main.rs 先声明表结构:

fn transcription_schema() -> Result<postgres::TableSchema> { postgres::TableSchema::new( [ ("filename", postgres::ColumnDef::new("text")), ("text", postgres::ColumnDef::new("text")), ], ["filename"], // 主键列 ) }

随后在app_main中挂载目标(src/main.rs):

let table = postgres::mount_table_target(&ctx, &DB, TABLE, transcription_schema()?, Some(PG_SCHEMA)) .await?; let files = walk_items(&sourcedir, AUDIO_PATTERNS)?; mount_each!(files, |file| process_audio(ctx, file, table)).await?;
  • TABLE = "audio_transcriptions"PG_SCHEMA = "coco_examples",最终落库到coco_examples.audio_transcriptions
  • mount_table_target托管目标:自动建表、幂等 upsert、以及删除源文件后自动清理孤儿行;
  • mount_each!宏把files列表的每个元素挂载为一个子组件,逐一执行process_audio

process_audio是真正的"每文件处理"组件(src/main.rs):先调用 memo 化的transcribe拿到文本,再向目标声明一行:

#[cocoindex::function] async fn process_audio(ctx: &Ctx, file: FileEntry, table: postgres::TableTarget) -> Result<()> { let text = transcribe(ctx, &file).await?; table.declare_row( ctx, &AudioTranscription { filename: file.key(), text }, )?; Ok(()) }

运行入口:Environment 与 ContextKey

main.rs 中的main展示了 Rust 版的应用装配方式:

let db = postgres::Database::connect(&database_url()).await?; let app = Environment::builder() .db_path(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".cocoindex_db")) .provide_key(&DB, db) .build() .await? .app("AudioToText") .await?; let stats = app.run(move |ctx| app_main(ctx, dir)).await?; println!("{stats}");
  • Environment::builder()构建运行环境,.db_path()指向本地 LMDB 状态库(示例放在.cocoindex_db,用于持久化 memo 与目标状态);
  • ContextKey(示例中的static DB)是跨组件共享的依赖注入句柄,provide_key把 Postgres 连接注册进去。值得注意的是 main.rs 中ContextKey::new_with_statedb.state_id()生成指纹——数据库状态变化会参与 memo 判定
  • 命令行第一个参数若为index则取第二个参数为目录,否则取第一个参数,缺省回退到audio_files

增量语义:为什么同一段音频不会付两次钱

官方 README 明确了本示例的增量行为,结合源码可以拆成三层机制:

1. 组件 memo 跳过未变更文件。#[cocoindex::function]声明的transcribe是 memo 化的。FileEntry携带文件大小与修改时间元数据(fs.rs),引擎据此计算指纹:文件内容与函数代码都未变 → 直接命中 memo,Whisper 调用不会发生。这意味着反复cargo run时,未改动过的音频零成本跳过。

2. 目标状态对账删除孤儿行。托管TableTarget在每次运行时把"本次声明的目标状态"与"上次运行记录的状态"做对账(reconcile)。当某音频文件从源目录中被删除,本次不再声明对应行,引擎会在 Postgres 中删除这行孤儿数据——表始终与目录内容保持一致。

3. 逻辑变更触发重转写。如果改动了transcribe的代码(例如换了模型),memo 因函数代码指纹变化而失效,CocoIndex 会对受影响文件重新转写,并与 Postgres 中已有内容比对、只应用差异。这就是 README 中所说的"逻辑变更也会对账"。

这套机制的工程价值很直接:语音转写是昂贵的付费 API 调用,增量语义确保你只为自己真正新增/改动的音频买单。

运行指南

前置条件

  • 一个可连接的Postgres实例。仓库提供了现成的 compose 文件 dev/postgres.yaml,可用docker compose -f dev/postgres.yaml up -d启动本地容器;
  • OpenAI API 密钥whisper-1需要),或指向 OpenAI 兼容端点的OPENAI_BASE_URL

配置环境变量

export POSTGRES_URL=postgres://cocoindex:cocoindex@localhost/cocoindex export OPENAI_API_KEY=...

POSTGRES_URL缺省值正是postgres://cocoindex:cocoindex@localhost/cocoindex(见 main.rs 的database_url()),因此若使用默认本地容器可省略第一行。OPENAI_API_KEYApiTranscriber::new自动读取,也支持在仓库根目录放置.envdotenvy会自动加载)。

运行

cargo run # 遍历 ./audio_files -> 转写 -> 写入 Postgres cargo run -- /path/to/audio # 指定自定义源目录

首次运行会转写目录下所有匹配的音频;再次运行则只处理新增、修改、删除的文件。结束后控制台会打印本次运行的统计信息(println!("{stats}"))。

验证结果

用普通 SQL 即可查看转写结果:

psql "$POSTGRES_URL" -c \ 'SELECT filename, left(text, 200) AS preview FROM coco_examples.audio_transcriptions ORDER BY filename;'

输出形如:每个音频一行,filename是主键、text是转写文本。

扩展指引:按需改造这个示例

基于上面的调用链,你可以低成本地把它改造成自己的管线:

  • 换模型/换端点:把TRANSCRIBE_MODEL常量从"whisper-1"改成其他 OpenAI 兼容模型,或用with_base_url指向自托管 Whisper 服务(示例已支持OPENAI_BASE_URL环境变量);
  • 换音频范围:调整AUDIO_PATTERNS中的 glob 列表,或给walk_items传入不同的目录与模式;
  • 加列/换主键:修改AudioTranscription结构体与transcription_schema(),例如增加durationlanguage列,或改用其他稳定键;
  • 接其他目标:SDK 的mount_table_target同样存在于 lancedb.rs、doris.rs 等模块中,可把转写结果导入向量库等下游存储;
  • 对照 Python 实现:两个语言版本的语义一一对应,阅读 examples/audio_to_text/main.py 与 examples/audio_to_text/README.md 可同时掌握两种写法。

小结

audio_to_text是理解 CocoIndex Rust SDK 增量语义的最佳入门示例:数据源(fs::walk)、逐文件计算(#[cocoindex::function]+ memo)、昂贵外部调用(ApiTranscriber)与托管目标(postgres::mount_table_target)各司其职,15 行核心管线代码背后是完整的指纹、memo 与目标对账机制。读懂这个示例,你就能举一反三,把"音频 → 文本 → Postgres"的管线模式迁移到任意"文件 → LLM/模型调用 → 目标存储"的场景。

【免费下载链接】cocoindexIncremental engine for long horizon agents 🌟 Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/co/cocoindex

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询