多模态模型与 TTS:从概念到代码,一次讲清楚
前言
在做 RAG 项目的过程中,我们通常会接触到文本向量化(Embedding)和语义检索。但 AI 的能力远不止文本——图片、音频、视频都可以成为模型的输入和输出。这篇文章带你从代码层面搞懂两件事:多模态模型和 TTS(Text-to-Speech)。
一、多模态模型 vs TTS:两个不同维度的概念
| 维度 | 多模态模型 | TTS |
|---|---|---|
| 核心能力 | 跨模态理解和生成(图片→描述、文字→图片、音频→文字...) | 文本→语音(单一方向) |
| 输入 | 文本 / 图片 / 音频 / 视频 | 仅文本 |
| 输出 | 文本 / 图片 / 音频 | 仅音频(mp3/wav 等) |
| 代表 | GPT-4o、Gemini、Claude 3.5 | OpenAI TTS、ElevenLabs、Edge TTS |
| 复杂度 | 理解+生成的综合能力 | 单一模态转换 |
TTS 是多模态能力中的一个小分支——文本→语音这一个方向。多模态模型是更通用的框架。
二、多模态模型的代码实现
2.1 核心:content 从字符串变成数组
纯文本调用时,content 就是一个字符串:
import { HumanMessage } from "@langchain/core/messages";
// 纯文本
new HumanMessage({
content: "这张图片里有什么?"
})
// 对应的 HTTP 请求体:
// {
// "role": "user",
// "content": "这张图片里有什么?"
// }
多模态调用时,content 变成数组,用 type 区分不同媒体类型:
import fs from "fs";
const imageBase64 = fs.readFileSync("./cat.jpg").toString("base64");
new HumanMessage({
content: [
{ type: "text", text: "这张图片里有什么?请用中文描述。" },
{
type: "image_url",
image_url: {
url: `data:image/jpeg;base64,${imageBase64}`
}
}
]
})
// 对应的 HTTP 请求体:
// {
// "role": "user",
// "content": [
// { "type": "text", "text": "这张图片里有什么?请用中文描述。" },
// { "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,..." } }
// ]
// }
这就是纯文本调用和多模态调用的唯一区别。 模型还是同一个(如 gpt-4o),API endpoint 没变,只是 content 字段从 string 变成了 ContentPart[]。
2.2 统一的 content 数组模式
所有非文本媒体的输入都遵循同一套规则——用 type 区分类型,用 base64 传输二进制数据:
| 输入类型 | content 数组元素 | 数据格式 |
|---|---|---|
| 文本 | { type: "text", text: "..." } | 原生字符串 |
| 图片 | { type: "image_url", image_url: { url: "data:..." } } | base64 |
| 音频 | { type: "input_audio", input_audio: { data: "...", format: "wav" } } | base64 |
| 视频 | { type: "video_url", video_url: { url: "data:..." } } | base64 |
甚至可以同时传多张图:
new HumanMessage({
content: [
{ type: "text", text: "图1和图2有什么不同?" },
{ type: "image_url", image_url: { url: "data:image/png;base64,图1..." } },
{ type: "image_url", image_url: { url: "data:image/png;base64,图2..." } }
]
})
三、为什么非文本要转 base64?
因为 HTTP API 只能传文本。 OpenAI 的 API 是 RESTful 接口,请求体是 JSON,JSON 里不能直接塞二进制数据。
三种传递图片/音频的方式:
// 方式1:base64 编码(最常用,数据直接内嵌在请求里)
{ image_url: "data:image/jpeg;base64,/9j/4AAQSkZJRg..." }
// 方式2:公网 URL(模型自己去下载)
{ image_url: "https://example.com/cat.jpg" }
// 方式3:先上传获取 file_id,再引用
{ image_id: "file-abc123" }
base64 本质就是把二进制字节流用 ASCII 字符表示,这样就能塞进 JSON 字符串里传输了。图片内容没有任何变化,512×512 的猫图转 base64 后还是那只猫。
四、base64 传输 ≠ 模型理解图片
这是两个完全不同阶段的事情,很多初学者容易混淆:
阶段一:客户端 → API 服务器(base64 传输)
原始图片二进制: FF D8 FF E0 00 10 4A 46 ...
↓ base64 编码
JSON 字符串: "/9j/4AAQSkZJRgABAQ..."
↓ 服务端 decode
还原为二进制: FF D8 FF E0 00 10 4A 46 ...
阶段二:服务端模型内部(真正"理解"图片)
原始图片 (512×512×3 RGB)
↓ 切成 patch(16×16 方格,每个 32×32 像素)
共 256 个 patch
↓ 每个 patch 展平成一维向量
↓ 过 Vision Encoder (ViT)
每个 patch → visual embedding
↓ 和文本 token 的 embedding 拼接
↓ 送入 Transformer 做交叉注意力 → 输出文本
| base64 编码 | patch + Vision Encoder | |
|---|---|---|
| 谁做的 | 你的客户端代码 | OpenAI/云端服务器 |
| 目的 | 让图片能通过 JSON 传输 | 让模型"看懂"图片 |
| 图片变化 | 格式变了,内容不变 | 被拆解成数字向量 |
| 阶段 | 发请求前 | 模型推理时 |
简单类比:base64 是快递员把包裹装进纸箱,patch 切分是收货人拆箱后阅读内容。两件事完全不搭边。
五、TTS 的代码实现
5.1 基本用法
import OpenAI from "openai";
import fs from "fs";
import { exec } from "child_process";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_BASE_URL,
});
// 调用 TTS API
const mp3 = await openai.audio.speech.create({
model: "tts-1", // 或 "tts-1-hd" 高质量版
voice: "shimmer", // 音色: alloy / echo / fable / onyx / nova / shimmer
input: "你好,今天天气真不错,适合出去散步。",
speed: 1.0, // 语速 0.25 ~ 4.0
});
// 将音频二进制流写入文件
const buffer = Buffer.from(await mp3.arrayBuffer());
fs.writeFileSync("./output.mp3", buffer);
// 播放音频(Windows)
exec("start output.mp3");
console.log("语音生成完成,正在播放...");
5.2 逐行解析
// mp3.arrayBuffer() → 从 API 响应中读取原始二进制数据(音频的 0101 字节流)
// Buffer.from() → 把二进制数据包装成 Node.js 的 Buffer 对象
const buffer = Buffer.from(await mp3.arrayBuffer());
// 把 Buffer 写成本地 .mp3 文件(sync 表示写完才继续往下走)
fs.writeFileSync("./output.mp3", buffer);
// Windows 用默认程序打开 .mp3(调用系统音乐播放器播放)
exec("start output.mp3");
一句话总结:从 API 拿原始音频字节 → 存成 mp3 文件 → 让系统用默认播放器打开。
5.3 TTS API 返回的不是 JSON
这是和 Chat API 最大的不同:
// Chat API — 返回 JSON,可以直接拿属性
const chatResponse = await openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "你好" }]
});
console.log(chatResponse.choices[0].message.content); // "你好!有什么可以帮你的?"
// TTS API — 返回二进制流(本质是 Fetch 的 Response 对象)
const audioResponse = await openai.audio.speech.create({
model: "tts-1",
voice: "shimmer",
input: "你好"
});
// audioResponse.content 是 undefined!
// 只有 body 属性,里面是原始 mp3 字节流
为什么这样设计? 音频文件体积大,如果编码成 JSON(比如 base64),体积会膨胀 33%,而且 JSON 解析本身也有开销。直接返回原始二进制流最高效。
5.4 TTS 内部原理(简化版)
文本 "你好,今天天气不错"
↓ 文本编码器(类似 BERT)
声学特征(梅尔频谱图)
↓ Vocoder(如 HiFi-GAN)
波形音频(mp3/wav 字节流)
↓ 返回给客户端
六、多模态模型的"输出"是怎么工作的?
很多人以为多模态模型可以直接输出图片和音频,实际上目前的实现方式是这样的:
你发文字 "用温柔的声音读这段话"
↓
GPT-4o 先生成文本回答
↓
内部调用 TTS 模块(音频合成)
↓
返回:文字 + 音频
你发图片 "给这只猫画个漫画版"
↓
GPT-4o 先理解图片内容
↓
生成绘图 prompt → 内部调用 DALL-E
↓
返回:描述文字 + 生成的图片
关键点:GPT-4o 本身不做像素级图像生成和波形级音频合成,它做的是"理解 + 规划",然后调度专门的生成模块。OpenAI 把它们打包在同一个品牌下,对用户来说像是一个模型什么都能输出。
| 输出类型 | API 方式 |
|---|---|
| 文本 | 直接返回 response.content |
| 图片 | API 返回图片 URL 或 base64 |
| 音频 | 单独调 TTS API(openai.audio.speech.create) |
所以在代码层面,文本输出是统一的,图片和音频输出通常走独立 API。
七、完整架构总结
把整个流程串起来:
┌─────────────────────────────────────────────────────┐
│ 你的客户端代码 │
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────┐ base64编码 ┌──────────┐ │
│ │ 图片 │ ─────────────→ │ JSON请求 │ ──── HTTP ──→│
│ └──────┘ └──────────┘ │
│ ┌──────┐ base64编码 ┌──────────┐ │
│ │ 音频 │ ─────────────→ │ JSON请求 │ ──── HTTP ──→│
│ └──────┘ └──────────┘ │
│ │
│ ←── 文本 JSON ──── Chat API 响应 │
│ ←── 图片 URL ──── 生成结果 │
│ ←── 音频二进制 ─── TTS API 响应 → 存文件 → 播放 │
└─────────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────┐
│ API 服务端 │
├─────────────────────────────────────────────────────┤
│ base64 decode → Vision/Audio Encoder → Vector │
│ → Transformer 交叉注意力 → 文本输出 │
│ → 或调度 DALL-E / TTS 等专用模块 │
└─────────────────────────────────────────────────────┘
八、一句话记住
- 多模态:
content从字符串变数组,用{ type, ... }区分媒体类型,base64 传输,encoder 理解。调用方式几乎不变。 - TTS:输入文本,API 返回音频流,直接存文件播放。和 Chat API 不同,响应是二进制不是 JSON。
希望这篇足够长的文章能让你对多模态和 TTS 有清楚的认识。如果有帮助,欢迎点赞收藏。