Vue.js + Qwen3 构建现代化字幕管理平台实战

发布时间:2026/5/21 1:10:57

Vue.js + Qwen3 构建现代化字幕管理平台实战 Vue.js Qwen3 构建现代化字幕管理平台实战最近在做一个视频内容相关的项目团队里的小伙伴们经常被字幕制作和修改搞得焦头烂额。传统的字幕工具要么操作繁琐要么功能单一很难满足我们既要效率又要质量的需求。于是我们决定自己动手用 Vue.js 和 Qwen3 大模型服务搭建一个现代化的字幕管理平台。这个平台的目标很明确让上传视频、生成字幕、编辑时间轴、导出文件这些事变得像搭积木一样简单直观。今天我就把整个从零到一的搭建过程以及我们踩过的一些坑分享给大家。如果你也在为视频字幕处理发愁或者想了解如何将大模型能力无缝集成到前端应用中这篇文章或许能给你一些启发。1. 为什么我们需要一个现代化的字幕平台在开始动手之前我们先聊聊痛点。传统的字幕工作流是什么样的通常是这样的先用专业软件或在线工具把视频里的语音转成文字得到一个文本稿然后你需要手动把每一句话和视频画面的时间点对齐这个过程极其枯燥且容易出错最后导出 SRT 或 VTT 格式的字幕文件再导入到视频剪辑软件里。这个过程有几个明显的问题工具割裂转文字、对齐时间轴、编辑内容、导出格式往往需要切换多个软件。效率低下手动对齐时间轴是最大的瓶颈尤其是对于长视频。协作困难版本管理混乱多人修改同一份字幕时容易冲突。体验不佳很多专业工具学习成本高对非技术人员不友好。我们想要的平台应该能在一个界面里完成所有事情上传视频后自动转写和对齐提供一个直观的时间轴进行微调支持多版本管理和一键导出。而 Qwen3 这类大模型在语音识别和自然语言处理上的强大能力正好可以解决“自动转写”和“智能对齐”这两个核心难题。Vue.js 则负责构建一个响应迅速、交互友好的前端界面把复杂的后端处理过程封装成简单的点击操作。2. 技术栈选型与整体架构设计确定了目标接下来就是搭台子。我们的技术选型主要围绕“高效开发”和“良好体验”展开。前端Vue.js 生态Vue 3 Composition API这是我们的核心框架。Vue 3 的响应式系统和 Composition API 让逻辑组织更清晰尤其是处理字幕时间轴这种状态复杂的功能。Vite作为构建工具它的热更新速度极快能大幅提升开发体验。Pinia状态管理库。用来集中管理视频文件、字幕列表、编辑状态等全局数据比 Vuex 更简洁。Element PlusUI 组件库。提供了丰富的现成组件如上传、按钮、表格、滑块能帮我们快速搭建出美观的界面。Vue Router处理单页面应用的路由比如在字幕列表页和编辑页之间切换。后端与服务Qwen3 API我们通过调用其提供的语音识别ASR接口将视频中的音频转换为带初步时间戳的文本。这是字幕自动生成的起点。Node.js Express/Koa一个轻量级的后端服务。它主要做几件事接收前端上传的视频文件调用 Qwen3 的 API处理视频转码比如提取音频管理字幕对齐等异步任务提供 RESTful API 给前端调用。数据库可选如果只是 demo数据可以存在内存或文件里。但为了持久化存储用户、视频项目、字幕版本等信息我们选择了PostgreSQL用Prisma作为 ORM 来操作非常顺手。整体工作流是这样的用户在 Vue.js 前端页面上传视频。前端将视频文件发送到我们的 Node.js 后端。后端启动一个异步任务先提取视频音频然后调用 Qwen3 的语音识别服务得到原始字幕文本和粗略的时间戳。后端将任务 ID 返回给前端前端可以轮询或通过 WebSocket 获取任务进度。任务完成后前端获取到初步的字幕数据并在可视化编辑器里展示。用户在编辑器里进行精细化调整拖动时间轴、修改文本。编辑完成后前端可以请求后端将字幕导出为 SRT 或 VTT 格式的文件。这个架构清晰地将前端交互、后端业务逻辑和 AI 能力解耦每一层各司其职。3. 核心功能模块实现详解平台有几个核心功能模块我们一个一个来看怎么实现。3.1 视频上传与转码处理首先得把视频弄到系统里。我们使用 Element Plus 的el-upload组件可以很方便地实现拖拽上传和显示进度。template div classupload-area el-upload classupload-demo drag action/api/upload !-- 后端上传接口 -- :on-successhandleUploadSuccess :on-progresshandleUploadProgress :before-uploadbeforeUpload acceptvideo/* el-icon classel-icon--uploadupload-filled //el-icon div classel-upload__text将视频文件拖到此处或em点击上传/em/div template #tip div classel-upload__tip支持 MP4, AVI, MOV 等格式大小不超过 2GB/div /template /el-upload div v-ifuploadProgress 0 classprogress-info 上传中: {{ uploadProgress }}% /div /div /template script setup import { ref } from vue; import { ElMessage } from element-plus; import { UploadFilled } from element-plus/icons-vue; const uploadProgress ref(0); const beforeUpload (file) { const isVideo file.type.startsWith(video/); const isLt2G file.size / 1024 / 1024 / 1024 2; // 2GB if (!isVideo) { ElMessage.error(只能上传视频文件); return false; } if (!isLt2G) { ElMessage.error(视频大小不能超过 2GB); return false; } return true; }; const handleUploadProgress (event) { uploadProgress.value Math.round((event.loaded / event.total) * 100); }; const handleUploadSuccess (response) { ElMessage.success(视频上传成功); // 这里可以跳转到任务处理页面并传递视频ID // router.push(/process/${response.videoId}); }; /script视频上传到后端后我们不能直接把视频文件扔给 Qwen3。需要先提取出音频轨道因为语音识别只需要音频。这里我们用fluent-ffmpeg这个库在 Node.js 后端进行处理。// backend/services/videoProcessor.js import ffmpeg from fluent-ffmpeg; import path from path; import { promisify } from util; import fs from fs; const unlinkAsync promisify(fs.unlink); export async function extractAudio(videoPath, outputDir) { const audioFileName audio_${Date.now()}.wav; const audioPath path.join(outputDir, audioFileName); return new Promise((resolve, reject) { ffmpeg(videoPath) .output(audioPath) .audioCodec(pcm_s16le) // 输出为WAV格式兼容性好 .audioFrequency(16000) // 采样率设为16kHz适合语音识别 .on(end, () { console.log(音频提取完成: ${audioPath}); resolve(audioPath); }) .on(error, (err) { console.error(音频提取失败:, err); reject(err); }) .run(); }); } // 使用后记得清理临时音频文件 // await unlinkAsync(audioPath);3.2 异步调用 Qwen3 生成字幕提取出音频后就可以调用 Qwen3 的语音识别服务了。这里的关键在于这是一个耗时的操作必须做成异步的否则前端请求会超时。我们在后端创建一个任务队列可以用bull或agenda等库当收到处理请求时不立即执行而是创建一个任务放入队列并立即返回一个任务 ID 给前端。// backend/controllers/subtitleController.js import { SubtitleTask } from ../models/SubtitleTask.js; // 假设的任务数据模型 import { processSubtitleQueue } from ../queues/subtitleQueue.js; // 任务队列 export const createSubtitleTask async (req, res) { const { videoId, audioPath } req.body; try { // 1. 在数据库中创建一条任务记录状态为 pending const task await SubtitleTask.create({ videoId, audioPath, status: pending, }); // 2. 将任务加入处理队列 await processSubtitleQueue.add({ taskId: task.id, audioPath: audioPath, }); // 3. 立即返回任务ID让前端可以去查询状态 res.json({ success: true, taskId: task.id, message: 字幕生成任务已提交请稍后查询结果。, }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }; // 前端轮询任务状态 export const getTaskStatus async (req, res) { const { taskId } req.params; const task await SubtitleTask.findByPk(taskId); if (!task) { return res.status(404).json({ success: false, error: 任务不存在 }); } res.json({ success: true, status: task.status, // pending, processing, completed, failed result: task.result, // 完成后这里会有字幕数据 error: task.error, }); };真正的处理逻辑在队列的工作进程中这里会调用 Qwen3 的 API。// backend/workers/subtitleWorker.js import { callQwen3SpeechRecognition } from ../services/qwen3Service.js; export async function processSubtitleJob(job) { const { taskId, audioPath } job.data; // 更新任务状态为处理中 await updateTaskStatus(taskId, processing); try { // 调用 Qwen3 语音识别服务 const subtitleRawData await callQwen3SpeechRecognition(audioPath); // 将 Qwen3 返回的原始数据转换成我们平台需要的格式 // 例如: [{ start: 1.2, end: 4.5, text: 你好世界。 }, ...] const formattedSubtitles formatSubtitles(subtitleRawData); // 更新任务为完成状态并保存结果 await updateTaskStatus(taskId, completed, formattedSubtitles); // 清理临时音频文件 await unlinkAsync(audioPath); } catch (error) { await updateTaskStatus(taskId, failed, null, error.message); throw error; // 让队列知道任务失败 } }3.3 可视化时间轴编辑器这是前端最核心、也最体现交互复杂度的部分。我们需要一个组件能够以时间线的形式展示每一条字幕并允许用户拖动来调整开始和结束时间双击编辑文本。我们并没有从头造轮子而是基于一个优秀的开源库wavesurfer.js来构建。wavesurfer.js可以生成音频波形图我们再在其上叠加一个自定义的轨道来渲染和操作字幕块。template div classeditor-container !-- 波形图区域 -- div refwaveformRef classwaveform/div !-- 字幕轨道 -- div classsubtitle-track div classtimeline-ruler/div !-- 时间刻度尺 -- div v-for(sub, index) in subtitles :keyindex classsubtitle-block :style{ left: ${timeToPixel(sub.start)}px, width: ${durationToPixel(sub.end - sub.start)}px } mousedownstartDrag(sub, index, $event) dblclickeditText(index) div classblock-content{{ sub.text }}/div !-- 左右拖拽手柄 -- div classresize-handle left mousedown.stopstartResize(index, left)/div div classresize-handle right mousedown.stopstartResize(index, right)/div /div /div !-- 字幕列表表格形式同步编辑 -- el-table :datasubtitles stylewidth: 100% el-table-column propstart label开始时间 width120 template #defaultscope el-input-number v-modelscope.row.start :step0.1 :precision2 controls-positionright changesyncTimeFromTable(scope.$index, start, $event) / /template /el-table-column el-table-column propend label结束时间 width120 template #defaultscope el-input-number v-modelscope.row.end :step0.1 :precision2 controls-positionright changesyncTimeFromTable(scope.$index, end, $event) / /template /el-table-column el-table-column proptext label字幕文本 template #defaultscope el-input v-modelscope.row.text typetextarea autosize changesyncTextFromTable(scope.$index, $event) / /template /el-table-column /el-table /div /template script setup import { ref, onMounted, onUnmounted } from vue; import WaveSurfer from wavesurfer.js; const waveformRef ref(null); let wavesurfer null; const subtitles ref([]); // 字幕数据格式如 [{start, end, text}] // 初始化波形图 onMounted(() { wavesurfer WaveSurfer.create({ container: waveformRef.value, waveColor: #4F4A85, progressColor: #383351, height: 100, barWidth: 2, }); // 加载视频的音频URL wavesurfer.load(/api/audio/your-audio-file.mp3); }); // 时间秒转换为像素位置 const timeToPixel (time) { if (!wavesurfer) return 0; const duration wavesurfer.getDuration(); const containerWidth waveformRef.value?.clientWidth || 800; return (time / duration) * containerWidth; }; // 拖拽和调整大小的逻辑此处省略具体实现涉及鼠标事件监听和状态更新 const startDrag (sub, index, event) { /* ... */ }; const startResize (index, direction) { /* ... */ }; const editText (index) { /* ... */ }; // 从表格同步数据到时间轴 const syncTimeFromTable (index, field, value) { // 更新 subtitles 数据时间轴区块会通过响应式自动更新位置 }; const syncTextFromTable (index, value) { // 更新字幕文本 }; /script这个编辑器实现了“所见即所得”的编辑体验。在时间轴上拖动表格里的数字会变在表格里修改时间时间轴上的区块位置也会实时更新。3.4 多版本管理与导出字幕经常需要反复修改。我们为每个视频项目引入了简单的版本管理。每次用户点击“保存”时我们并不覆盖原字幕而是创建一条新的版本记录。// backend/models/SubtitleVersion.js 示例 { videoId: 123, version: 2, // 版本号 content: [...], // 完整的字幕数组 createdAt: 2023-10-27..., createdBy: user123, comment: 修正了前三句的时间轴, }前端可以提供一个版本对比视图高亮显示不同版本之间的差异比如文本修改、时间点偏移。导出功能就相对简单了后端根据前端指定的格式SRT 或 VTT将subtitles数组转换成对应的文本格式即可。// backend/utils/exportUtils.js export function generateSRT(subtitles) { let srtContent ; subtitles.forEach((sub, index) { srtContent ${index 1}\n; srtContent ${formatTime(sub.start)} -- ${formatTime(sub.end)}\n; srtContent ${sub.text}\n\n; }); return srtContent; } function formatTime(seconds) { const date new Date(seconds * 1000); const hrs date.getUTCHours().toString().padStart(2, 0); const mins date.getUTCMinutes().toString().padStart(2, 0); const secs date.getUTCSeconds().toString().padStart(2, 0); const ms (date.getUTCMilliseconds() / 1000).toFixed(3).split(.)[1]; return ${hrs}:${mins}:${secs},${ms}; }4. 总结从零开始用 Vue.js 和 Qwen3 搭建这个字幕平台整个过程更像是在解决一个又一个具体的工程问题而不是在钻研高深的算法。Vue.js 的响应式特性和丰富的生态让我们能快速构建出复杂但交互流畅的前端界面而 Qwen3 则像一个强大的“黑盒”通过 API 调用为我们提供了准确的语音转写能力省去了自研 ASR 模型的巨大成本。这个平台上线后团队处理视频字幕的效率提升非常明显。最耗时的“听写”和“初对齐”工作被自动化了同事们只需要把精力集中在最后的“精修”上体验好了很多。当然目前这个平台还有很多可以优化的地方比如引入更智能的断句和标点纠正、支持多人协同编辑、集成机器翻译生成多语言字幕等等。技术总是服务于业务的。这个项目的价值不在于用了多新的框架或多牛的模型而在于它切实地解决了一个高频、刚需的痛点。如果你也有类似的需求不妨参考这个思路用你熟悉的技术栈尝试搭建一个。你会发现把强大的 AI 能力封装成一个简单易用的工具本身就是一件很有成就感的事。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。

相关新闻