一图入,一声出——多模态输入与 TTS 输出的完整链路

0 阅读7分钟

多模态模型与 TTS:从概念到代码,一次讲清楚

前言

在做 RAG 项目的过程中,我们通常会接触到文本向量化(Embedding)和语义检索。但 AI 的能力远不止文本——图片、音频、视频都可以成为模型的输入和输出。这篇文章带你从代码层面搞懂两件事:多模态模型TTS(Text-to-Speech)


一、多模态模型 vs TTS:两个不同维度的概念

维度多模态模型TTS
核心能力跨模态理解和生成(图片→描述、文字→图片、音频→文字...)文本→语音(单一方向)
输入文本 / 图片 / 音频 / 视频仅文本
输出文本 / 图片 / 音频仅音频(mp3/wav 等)
代表GPT-4o、Gemini、Claude 3.5OpenAI 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 有清楚的认识。如果有帮助,欢迎点赞收藏。