shot-word demo
概述;利用 React 框架 实现上传图片,调用 moon-shoot 多模态模型识别图片,并通过设计 prompt 生成返回一个 json 数据,并调用火山引擎语音模型生成对应的语音。
数据设计
- 用户上传的图片 image
- 图片多模态模型识别结果 data,里面有页面渲染需要的所有数据。
{ "image_discription": "图片描述", "representative_word": "图片代表的英文单词", "example_sentence": "结合英文单词和图片描述,给出一个简单的例句", "explaination": "结合图片解释英文单词,段落以 Look at...开头,将段落分句,每一句单独一行,解释的最后给一个日常生活有关的问句", "explaination_replys": ["根据 explaination 给出的回复 1", "根据 explaination 给出的回复 2"] }
- 例句语音,请求到的 explaination 的音频
页面结构
一个图片上传卡片,一个点击查看详情页面的按钮,以及点击按钮出现的详情页面,页面垂直排布,使用 flex 布局
事件处理
- 处理图片上传事件
- 点击查看详情按钮展开详情页和收起详情页
组件结构
- APP
- PictureCard
状态管理
- 图片上传状态
- 详情页展开收起状态
- Audio 是否请求到音频
效果


项目地址 AIFullStackJourney/React/shot-word at main · XXUZZWZ/AIFullStackJourney
难点
在前端将 base64 编码的图片数据转换成二进制对象,并使用 URL.createObjectURL()方法创建一个临时 URL,并设置给 img 标签的 src 属性,实现图片的预览。避免 Content-Security-Policy
对 data:
协议的限制,规避跨域资源访问问题(CORS)。 Blob URL 可流式播放,无需等待完整数据解码 内存管理更优(Base64 Data URL 体积增大 33%)
实现函数
javascript
const getAudioUrl = (base64Data) => {
// 创建一个数组来存储字节数据
var byteArrays = [];
// 使用atob()将Base64编码的字符串解码为原始二进制字符串
// atob: ASCII to Binary String (原始二进制字符串(每个字符表示一个字节,但以字符串的形式存在))
var byteCharacters = atob(base64Data);
// 遍历解码后的二进制字符串的每个字符
for (var offset = 0; offset < byteCharacters.length; offset++) {
// 将每个字符转换为其ASCII码值(0-255之间的数字)
var byteArray = byteCharacters.charCodeAt(offset);
// 将ASCII码值添加到字节数组中
byteArrays.push(byteArray);
}
// 创建一个Blob对象
// new Uint8Array(byteArrays)将普通数组转换为8位无符号整数数组
// { type: 'audio/mp3' } 指定Blob的MIME类型为MP3音频
var blob = new Blob([new Uint8Array(byteArrays)], { type: "audio/mp3" });
// 使用URL.createObjectURL创建一个临时的URL
// 这个URL可以用于<audio>标签的src属性
// 这个URL在当前页面/会话有效,页面关闭后会自动释放
// 创建一个临时 URL 供音频播放
return URL.createObjectURL(blob);
};
下面我将详细拆解 getAudioUrl
函数的每个步骤,解释其工作原理和前端处理的意义:
函数流程详解
javascript
const getAudioUrl = (base64Data) => {
// 步骤1:准备存储容器
var byteArrays = [];
作用:创建空数组用于存储转换后的二进制数据。
javascript
// 步骤2:Base64解码
var byteCharacters = atob(base64Data);
关键点:
atob()
是浏览器内置函数,将 Base64 字符串解码为二进制字符串- Base64 是文本表示形式,实际音频需要二进制格式
- 示例:
"TWFu"
(Base64) →"Man"
(二进制字符串)
javascript
// 步骤3:字符到字节转换
for (var offset = 0; offset < byteCharacters.length; offset++) {
var byteArray = byteCharacters.charCodeAt(offset);
byteArrays.push(byteArray);
}
核心操作:
- 遍历每个二进制字符
charCodeAt()
获取字符的 ASCII 码值(0-255)- 将数值存入数组
内存图示:
arduino
字符: 'M' 'a' 'n'
ASCII: 77 97 110
数组: [77, 97, 110]
javascript
// 步骤4:创建二进制对象
var blob = new Blob([new Uint8Array(byteArrays)], { type: "audio/mp3" });
关键技术:
new Uint8Array(byteArrays)
:- 将普通数组转为二进制数组(每个元素占 1 字节)
- 内存效率高于普通数组
new Blob()
:- 创建不可变的二进制大对象
{ type: "audio/mp3" }
指定 MIME 类型,告知浏览器这是 MP3 音频
javascript
// 步骤5:生成可访问URL
return URL.createObjectURL(blob);
};
核心价值:
-
创建指向内存 Blob 的临时 URL(格式:
blob:https://example.com/550e8400-e29b-41d4...
) -
可直接用于 HTML 媒体元素:
html<audio src="{getAudioUrl(base64Data)}" controls />
为何需要这样处理?
-
数据转换的必要性
- Base64 是文本格式(ASCII),音频需要二进制格式
- 浏览器无法直接播放 Base64 字符串
-
与 Data URL 的对比
javascript// 替代方案(不推荐): const dataUrl = `data:audio/mp3;base64,${base64Data}`;
- Blob URL 优势 :
- 支持流式播放(无需完整加载)
- 内存效率高(Base64 体积大 33%)
- 可手动释放内存
- Blob URL 优势 :
-
浏览器兼容性处理
-
旧版 IE 需 polyfill:
javascript// 兼容旧浏览器的atob if (typeof atob === "undefined") { atob = function (encoded) { return Buffer.from(encoded, "base64").toString("binary"); }; }
-
实际应用场景
语音消息预览:
javascript
// 接收Base64语音数据
socket.on("voice-message", (data) => {
const audio = new Audio(getAudioUrl(data));
audio.play();
// 播放后清理内存
audio.onended = () => URL.revokeObjectURL(audio.src);
});
性能优化技巧:
javascript
// 更高效的Uint8Array直接填充
const optimizedVersion = (base64Data) => {
const str = atob(base64Data);
const array = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
array[i] = str.charCodeAt(i);
}
return URL.createObjectURL(
new Blob([array], { type: "audio/webm" }) // 根据实际类型调整
);
};
关键注意事项
-
内存管理
-
主动释放资源:
javascriptconst url = getAudioUrl(data); audio.src = url; // 不再需要时 URL.revokeObjectURL(url);
-
页面关闭时浏览器自动回收
-
-
MIME 类型陷阱
-
audio/mp3
可能不匹配实际编码 -
正确设置类型:
实际格式 正确 MIME 类型 MP3 audio/mpeg
WAV audio/wav
OGG audio/ogg
-
-
大文件处理
-
超过 50MB 的音频建议分块处理:
javascriptconst CHUNK_SIZE = 1_000_000; // 1MB分块 const chunks = []; for (let i = 0; i < base64Data.length; i += CHUNK_SIZE) { const chunk = base64Data.slice(i, i + CHUNK_SIZE); chunks.push(atob(chunk)); } const blob = new Blob( chunks.map( (c) => new Uint8Array([...c].map((char) => char.charCodeAt(0))) ), { type: "audio/mp3" } );
-
这种处理方式在前端实现了 二进制转换 → 内存存储 → 媒体资源生成 的完整链路,是处理客户端音频数据的标准实践方案。
在前端处理 Base64 音频数据并生成 Blob URL(如示例代码所示)具有以下显著优势:
1. 减少服务器负担
- 直接在浏览器完成数据转换,避免将 Base64 数据上传到服务器再生成文件
- 节省服务器计算资源和存储空间
2. 提升响应速度
- 省去网络往返时间(RTT),音频数据转换后立即可播放
- 特别适合实时场景(如语音消息即时播放)
3. 增强隐私性
- 敏感音频数据无需通过网络传输,降低中间环节泄露风险
- 临时 URL 仅在当前页面生命周期有效(页面关闭自动释放)
4. 优化资源加载
html
<!-- 直接使用生成的 URL -->
<audio src="{getAudioUrl(base64Data)}" controls />
- 比 Base64 Data URL(
data:audio/mp3;base64,...
)更高效:- Blob URL 可流式播放,无需等待完整数据解码
- 内存管理更优(Base64 Data URL 体积增大 33%)
5. 跨域安全优势
- 避免
Content-Security-Policy
对data:
协议的限制 - 规避跨域资源访问问题(CORS)
6. 内存效率
-
使用
Uint8Array
直接处理二进制数据,比字符串操作更高效 -
临时 URL 可手动释放资源:
javascript// 播放结束后释放资源 audioElement.onended = () => { URL.revokeObjectURL(audioElement.src); };
潜在注意事项
- 浏览器兼容性 :现代浏览器均支持,但需注意:
- IE10+ 支持
Blob
,IE11+ 支持URL.createObjectURL
- 旧版安卓需 polyfill(如
core-js
)
- IE10+ 支持
- 大文件处理:超过 500MB 的音频可能引发内存问题
- MIME 类型 :确保
type
与实际格式匹配(示例中audio/mp3
需与实际编码一致)
性能优化建议
javascript
// 更高效的转换方式(减少循环次数)
const getAudioUrlOptimized = (base64Data) => {
const byteChars = atob(base64Data);
const byteArray = new Uint8Array(byteChars.length);
for (let i = 0; i < byteChars.length; i++) {
byteArray[i] = byteChars.charCodeAt(i);
}
return URL.createObjectURL(new Blob([byteArray], { type: "audio/mp3" }));
};
典型应用场景
- 即时通讯中的语音消息预览
- 网页版音频编辑器(本地处理)
- 从
FileReader.readAsDataURL()
获取的音频处理 - 免服务器的 Web Audio API 应用
⚠️ 重要提示 :对于持久化存储,仍需将原始 Base64 或二进制数据发送至服务器,Blob URL 仅适用于临时访问场景。