前端集成实战用JavaScript与Vue调用Qwen3字幕API对于视频内容创作者来说为视频添加字幕是一项耗时且繁琐的工作。手动听写、打轴、校对一个十分钟的视频可能就要花掉半小时。如果能有一个智能工具自动生成准确的字幕并允许我们快速调整那效率的提升将是巨大的。这正是我们今天要探讨的主题如何将强大的Qwen3字幕生成能力无缝集成到你的Vue.js前端应用中。我们将构建一个完整的视频字幕工作流从上传视频、调用API生成字幕到在前端界面中直观地预览、编辑时间轴最后打包下载带字幕的视频文件。整个过程你只需要一个现代浏览器和基础的JavaScript与Vue知识。1. 项目概述与核心价值想象一下这样一个场景你的用户上传了一段产品演示视频点击一个按钮几分钟后一个带有可编辑时间轴的字幕界面就出现了。用户可以拖动字幕块来微调时间修改识别有误的文字然后一键导出为标准的SRT字幕文件或者直接合成一个带硬字幕的新视频。这不仅能极大提升内容生产的效率也能为你的应用带来独特的竞争力。这个方案的核心价值在于提升效率将数十分钟的手工字幕制作过程压缩到几分钟的自动化流程。改善体验提供直观的可视化编辑界面让非专业用户也能轻松调整字幕。技术整合展示了如何将后端AI能力Qwen3 API与前端富交互框架Vue.js深度结合构建完整的应用功能。我们将使用Vue 3的Composition API和现代JavaScript来构建这个功能。即使你对Vue 3不太熟悉跟着步骤走也能理解核心思路。2. 环境准备与项目搭建首先确保你有一个可以运行的环境。我们将创建一个新的Vue项目并安装必要的依赖。如果你还没有Node.js和npm请先安装它们。然后打开终端运行以下命令来创建一个新的Vue项目# 使用Vite创建项目这是目前最快速轻量的方式 npm create vuelatest my-subtitle-app在创建过程中你可以根据提示选择需要的特性。对于本项目我们至少需要✅ TypeScript (可选但推荐用于更好的开发体验)✅ Vue Router (可选如果你的应用需要多页面)❌ Pinia (状态管理本项目简单可先不用)✅ ESLint (代码规范推荐)项目创建完成后进入目录并安装我们需要的额外库cd my-subtitle-app npm install # 安装我们需要的库用于HTTP请求的axios以及一个简单的视频播放器组件这里以video.js为例你也可以用其他 npm install axios video.js videojs-player/vue npm install --save-dev types/video.js # 如果使用TypeScript现在基本的项目骨架就准备好了。你可以运行npm run dev来启动开发服务器。3. 核心组件设计与实现我们的应用主要包含三个核心部分视频上传/播放器、字幕生成控制区、字幕轨道编辑器。我们将分别构建它们。3.1 视频上传与播放器组件这个组件负责让用户选择视频文件并提供一个播放器来预览视频和监听时间变化。我们创建一个VideoPlayer.vue组件template div classvideo-section div v-if!videoUrl classupload-area clicktriggerFileInput dragover.prevent drophandleDrop input typefile reffileInput changehandleFileChange acceptvideo/* hidden / p点击或拖拽视频文件到此区域上传/p p classhint支持MP4, WebM, MOV等常见格式/p /div div v-else classplayer-container !-- 这里使用video.js播放器你也可以用原生video标签 -- video-player refvideoPlayer :srcvideoUrl controls timeupdatehandleTimeUpdate classvideo-js vjs-big-play-centered / div classvideo-info p当前文件: {{ videoFile?.name }}/p button clickresetVideo classbtn-secondary更换视频/button /div /div /div /template script setup langts import { ref } from vue; import { VideoPlayer } from videojs-player/vue; import video.js/dist/video-js.css; const emit defineEmits([file-selected, time-update]); const fileInput refHTMLInputElement(); const videoFile refFile(); const videoUrl refstring(); const triggerFileInput () { fileInput.value?.click(); }; const handleFileChange (e: Event) { const target e.target as HTMLInputElement; const file target.files?.[0]; if (file file.type.startsWith(video/)) { setVideoFile(file); } }; const handleDrop (e: DragEvent) { e.preventDefault(); const file e.dataTransfer?.files[0]; if (file file.type.startsWith(video/)) { setVideoFile(file); } }; const setVideoFile (file: File) { videoFile.value file; // 创建本地URL用于播放 videoUrl.value URL.createObjectURL(file); emit(file-selected, file); }; const handleTimeUpdate (player: any) { // 发射当前播放时间用于字幕高亮同步 emit(time-update, player.currentTime()); }; const resetVideo () { if (videoUrl.value) { URL.revokeObjectURL(videoUrl.value); // 释放内存 } videoUrl.value ; videoFile.value undefined; if (fileInput.value) { fileInput.value.value ; } }; /script style scoped .video-section { border: 2px dashed #ccc; border-radius: 8px; padding: 2rem; text-align: center; margin-bottom: 2rem; } .upload-area { cursor: pointer; color: #666; } .upload-area:hover { background-color: #f9f9f9; } .hint { font-size: 0.9rem; color: #999; } .player-container { max-width: 800px; margin: 0 auto; } .video-info { margin-top: 1rem; display: flex; justify-content: space-between; align-items: center; } /style3.2 调用字幕生成API这是与后端Qwen3服务交互的核心。我们创建一个可复用的Composition API函数useSubtitleGenerator。// composables/useSubtitleGenerator.js import { ref } from vue; import axios from axios; // 假设你的后端API地址这里需要替换成你实际部署的Qwen3字幕服务地址 const API_BASE_URL https://your-api-server.com/api; export function useSubtitleGenerator() { const isGenerating ref(false); const generationProgress ref(0); const error ref(null); const generateSubtitles async (videoFile, language zh) { isGenerating.value true; error.value null; generationProgress.value 10; // 开始 const formData new FormData(); formData.append(video, videoFile); formData.append(language, language); // 可以根据需要添加其他参数如模型选择、是否包含说话人分离等 // formData.append(model, qwen3-subtitle-large); // formData.append(diarization, true); try { generationProgress.value 30; // 上传中 const response await axios.post(${API_BASE_URL}/generate, formData, { headers: { Content-Type: multipart/form-data, }, // 用于模拟进度实际中可能需要服务器支持或使用WebSocket onUploadProgress: (progressEvent) { const percentCompleted Math.round((progressEvent.loaded * 100) / progressEvent.total); generationProgress.value 30 percentCompleted * 0.5; // 上传占50% }, }); // 假设后端返回一个任务ID我们需要轮询结果 const taskId response.data.task_id; generationProgress.value 80; // 处理中 // 轮询获取结果 const result await pollTaskResult(taskId); generationProgress.value 100; isGenerating.value false; return result; // 返回字幕数据 } catch (err) { console.error(生成字幕失败:, err); error.value err.response?.data?.message || 字幕生成失败请重试或检查网络。; isGenerating.value false; throw err; } }; const pollTaskResult async (taskId, maxAttempts 30, interval 2000) { for (let i 0; i maxAttempts; i) { await new Promise(resolve setTimeout(resolve, interval)); try { const response await axios.get(${API_BASE_URL}/task/${taskId}); const status response.data.status; if (status SUCCESS) { return response.data.result; // 返回字幕数组 } else if (status FAILED) { throw new Error(response.data.error || 任务处理失败); } // 如果状态是PENDING或PROCESSING继续轮询 } catch (err) { throw err; } } throw new Error(任务超时请稍后重试); }; return { isGenerating, generationProgress, error, generateSubtitles, }; }3.3 字幕轨道编辑器组件这是交互的核心用户在这里可以直观地看到每条字幕并拖动调整其开始和结束时间。!-- SubtitleTrack.vue -- template div classsubtitle-track div classtrack-header h3字幕轨道/h3 div classcontrols button clickplayFromStart :disabled!subtitles.length从头播放/button button clickexportSRT导出SRT/button /div /div div classtrack-container reftrackContainer !-- 时间轴刻度 -- div classtime-ruler div v-fortick in timeTicks :keytick classtick :style{ left: ${timeToPosition(tick)}% } span classtick-label{{ formatTime(tick) }}/span /div /div !-- 字幕条 -- div v-for(sub, index) in subtitles :keyindex classsubtitle-item :class{ active: isSubtitleActive(sub) } :style{ left: ${timeToPosition(sub.start)}%, width: ${timeToPosition(sub.end) - timeToPosition(sub.start)}% } mousedownstartDrag(sub, index, $event) dblclickeditText(index) div classsubtitle-handle left mousedown.stopstartResize(index, left)/div div classsubtitle-content {{ sub.text }} /div div classsubtitle-handle right mousedown.stopstartResize(index, right)/div div classsubtitle-time {{ formatTime(sub.start) }} → {{ formatTime(sub.end) }} /div /div /div !-- 文本编辑模态框 -- div v-ifeditingIndex ! null classedit-modal textarea v-modelsubtitles[editingIndex].text rows3/textarea div classmodal-actions button clicksaveEdit保存/button button clickcancelEdit取消/button /div /div /div /template script setup import { ref, computed, onMounted, onUnmounted } from vue; const props defineProps({ subtitles: { type: Array, required: true, default: () [] // 格式: [{ start: 0, end: 5, text: 你好 }, ...] }, currentTime: { type: Number, default: 0 } }); const emit defineEmits([update:subtitles, seek-to, export-srt]); const trackContainer ref(null); const editingIndex ref(null); const dragState ref(null); // { type: move | resize-left | resize-right, index, startX, startTime } // 计算时间轴刻度 const duration computed(() { if (!props.subtitles.length) return 60; const lastSub props.subtitles[props.subtitles.length - 1]; return Math.ceil(lastSub.end / 10) * 10; // 取整到10秒的倍数 }); const timeTicks computed(() { const ticks []; const interval duration.value 120 ? 30 : 10; // 根据总时长决定刻度间隔 for (let t 0; t duration.value; t interval) { ticks.push(t); } return ticks; }); // 工具函数 const timeToPosition (time) (time / duration.value) * 100; const positionToTime (positionPercent) (positionPercent / 100) * duration.value; const formatTime (seconds) { const mins Math.floor(seconds / 60); const secs Math.floor(seconds % 60); const ms Math.floor((seconds % 1) * 1000); return ${mins.toString().padStart(2, 0)}:${secs.toString().padStart(2, 0)}.${ms.toString().padStart(3, 0)}; }; const isSubtitleActive (sub) { return props.currentTime sub.start props.currentTime sub.end; }; // 交互功能 const startDrag (sub, index, e) { dragState.value { type: move, index, startX: e.clientX, startTime: sub.start }; document.addEventListener(mousemove, handleDragMove); document.addEventListener(mouseup, stopDrag); }; const startResize (index, side) { dragState.value { type: resize-${side}, index, startX: event.clientX, startTime: side left ? props.subtitles[index].start : props.subtitles[index].end }; document.addEventListener(mousemove, handleDragMove); document.addEventListener(mouseup, stopDrag); }; const handleDragMove (e) { if (!dragState.value || !trackContainer.value) return; const containerRect trackContainer.value.getBoundingClientRect(); const deltaX e.clientX - dragState.value.startX; const deltaTime (deltaX / containerRect.width) * duration.value; const newSubtitles [...props.subtitles]; const item newSubtitles[dragState.value.index]; if (dragState.value.type move) { const newStart dragState.value.startTime deltaTime; const durationItem item.end - item.start; // 简单边界检查 if (newStart 0 newStart durationItem duration.value) { item.start newStart; item.end newStart durationItem; } } else if (dragState.value.type resize-left) { const newStart dragState.value.startTime deltaTime; if (newStart 0 newStart item.end - 0.5) { // 至少保留0.5秒 item.start newStart; } } else if (dragState.value.type resize-right) { const newEnd dragState.value.startTime deltaTime; if (newEnd duration.value newEnd item.start 0.5) { item.end newEnd; } } emit(update:subtitles, newSubtitles); }; const stopDrag () { dragState.value null; document.removeEventListener(mousemove, handleDragMove); document.removeEventListener(mouseup, stopDrag); }; const editText (index) { editingIndex.value index; }; const saveEdit () { editingIndex.value null; // 数据已通过v-model自动更新 }; const cancelEdit () { editingIndex.value null; }; const playFromStart () { emit(seek-to, 0); }; const exportSRT () { emit(export-srt); }; onUnmounted(() { // 清理事件监听 document.removeEventListener(mousemove, handleDragMove); document.removeEventListener(mouseup, stopDrag); }); /script style scoped /* 样式代码较长主要实现轨道、字幕条、手柄的视觉和交互效果 */ .subtitle-track { border: 1px solid #e0e0e0; border-radius: 8px; padding: 1rem; background: #fafafa; } .track-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; } .track-container { position: relative; height: 120px; background: white; border: 1px solid #ddd; border-radius: 4px; overflow-x: auto; overflow-y: hidden; } .time-ruler { position: absolute; top: 0; width: 100%; height: 30px; border-bottom: 1px solid #ccc; } .tick { position: absolute; bottom: 0; transform: translateX(-50%); } .tick-label { font-size: 0.75rem; color: #666; white-space: nowrap; } .subtitle-item { position: absolute; top: 40px; height: 50px; background-color: #e3f2fd; border: 1px solid #90caf9; border-radius: 4px; cursor: move; user-select: none; overflow: hidden; } .subtitle-item.active { background-color: #bbdefb; border-color: #2196f3; } .subtitle-handle { position: absolute; top: 0; width: 6px; height: 100%; background-color: #64b5f6; cursor: ew-resize; } .subtitle-handle.left { left: 0; } .subtitle-handle.right { right: 0; } .subtitle-content { padding: 8px; height: 100%; display: flex; align-items: center; justify-content: center; text-align: center; font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; } .subtitle-time { position: absolute; bottom: -20px; font-size: 0.7rem; color: #777; white-space: nowrap; } .edit-modal { position: fixed; /* 模态框样式 */ } /style4. 整合与用户体验优化现在我们将所有组件整合到主页面中并添加一些提升用户体验的功能。!-- App.vue 或主页面组件 -- template div classapp-container header h1智能视频字幕工作台/h1 p上传视频自动生成并编辑字幕一键导出/p /header main !-- 视频上传与播放区域 -- VideoPlayer file-selectedhandleVideoSelected time-updatehandleTimeUpdate refvideoPlayer / !-- 控制区域生成字幕 -- div classcontrol-section v-ifvideoFile div classgenerate-controls label识别语言/label select v-modelselectedLanguage option valuezh中文/option option valueen英文/option option valueja日文/option !-- 更多语言选项 -- /select button clickhandleGenerateSubtitles :disabledisGenerating || !videoFile classbtn-primary {{ isGenerating ? 生成中... ${generationProgress}% : 开始生成字幕 }} /button button clickhandleExportWithHardSub :disabled!subtitles.length classbtn-success 合成带字幕视频 /button /div div v-iferror classerror-message {{ error }} /div /div !-- 字幕编辑区域 -- SubtitleTrack v-ifsubtitles.length :subtitlessubtitles :current-timecurrentVideoTime update:subtitlessubtitles $event seek-tohandleSeek export-srthandleExportSRT / !-- 状态提示 -- div v-if!videoFile classempty-state p请先上传一个视频文件开始工作。/p /div div v-else-if!subtitles.length !isGenerating classempty-state p视频已就绪。点击上方的“开始生成字幕”按钮来创建字幕。/p /div /main /div /template script setup import { ref } from vue; import VideoPlayer from ./components/VideoPlayer.vue; import SubtitleTrack from ./components/SubtitleTrack.vue; import { useSubtitleGenerator } from ./composables/useSubtitleGenerator; const videoFile ref(null); const subtitles ref([]); const currentVideoTime ref(0); const selectedLanguage ref(zh); const { isGenerating, generationProgress, error, generateSubtitles } useSubtitleGenerator(); const handleVideoSelected (file) { videoFile.value file; subtitles.value []; // 清除旧字幕 }; const handleTimeUpdate (time) { currentVideoTime.value time; }; const handleGenerateSubtitles async () { if (!videoFile.value) return; try { const result await generateSubtitles(videoFile.value, selectedLanguage.value); // 假设API返回的字幕格式为 [{start, end, text}, ...] subtitles.value result; } catch (err) { console.error(生成过程出错:, err); // 错误信息已由useSubtitleGenerator处理 } }; const handleSeek (time) { // 这里需要调用VideoPlayer组件的方法来跳转播放时间 // 可以通过ref调用子组件方法或使用事件总线/Pinia console.log(跳转到时间:, time); // 例如videoPlayer.value.seek(time); }; const handleExportSRT () { const srtContent convertToSRT(subtitles.value); downloadFile(srtContent, subtitles.srt, text/plain); }; const handleExportWithHardSub async () { // 这里需要调用后端API将视频、字幕和编辑后的时间轴信息发送过去 // 后端使用FFmpeg等工具合成硬字幕视频 alert(合成功能需要后端API支持。这里可以调用一个合成接口。); }; // 工具函数将字幕数组转换为SRT格式字符串 const convertToSRT (subs) { return subs.map((sub, index) { const start formatTimeForSRT(sub.start); const end formatTimeForSRT(sub.end); return ${index 1}\n${start} -- ${end}\n${sub.text}\n; }).join(\n); }; const formatTimeForSRT (seconds) { const hrs Math.floor(seconds / 3600); const mins Math.floor((seconds % 3600) / 60); const secs Math.floor(seconds % 60); const ms Math.floor((seconds % 1) * 1000); return ${hrs.toString().padStart(2, 0)}:${mins.toString().padStart(2, 0)}:${secs.toString().padStart(2, 0)},${ms.toString().padStart(3, 0)}; }; const downloadFile (content, filename, mimeType) { const blob new Blob([content], { type: mimeType }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; /script style /* 全局样式 */ .app-container { max-width: 1200px; margin: 0 auto; padding: 2rem; } .control-section { margin: 2rem 0; padding: 1.5rem; background: #f8f9fa; border-radius: 8px; } .generate-controls { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; } .btn-primary { background-color: #007bff; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; } .btn-primary:disabled { background-color: #ccc; cursor: not-allowed; } .btn-success { background-color: #28a745; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; } .empty-state { text-align: center; padding: 3rem; color: #6c757d; font-style: italic; } .error-message { margin-top: 1rem; padding: 0.75rem; background-color: #f8d7da; color: #721c24; border-radius: 4px; } /style5. 总结与扩展思路通过上面的步骤我们完成了一个具备核心功能的视频字幕前端工作台。从上传视频、调用AI生成字幕到可视化编辑时间轴和文本最后导出标准格式文件整个流程已经跑通。在实际使用中你会发现这种拖拽式编辑比手动输入时间码要直观和高效得多。当然这只是一个起点。在实际项目中你可能会考虑以下扩展方向来让它更强大、更稳定性能优化对于长视频字幕数据量可能很大。可以考虑虚拟滚动来渲染字幕轨道避免DOM节点过多导致卡顿。编辑功能增强添加字幕拆分、合并功能支持批量调整时间偏移以及更丰富的文本样式编辑如字体、颜色、位置为后续合成复杂字幕如ASS做准备。后端集成深化除了生成字幕后端API还可以提供视频切片、语音分离、翻译、以及最终的硬字幕合成服务。前端需要设计更完善的任务状态管理和进度展示。用户体验细节添加快捷键支持如空格键播放/暂停左右键微调时间轴自动保存草稿导入已有字幕文件进行编辑以及更清晰的错误处理和重试机制。部署与协作将前端应用部署到服务器并考虑加入用户系统实现项目保存、团队协作编辑等功能。整合AI能力到前端应用关键在于找到技术与用户体验的最佳结合点。Qwen3提供了强大的字幕生成能力而Vue.js让我们能够构建出响应迅速、交互友好的界面。希望这个实战项目能为你自己的创意应用提供一个坚实的起点。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。
前端集成实战:用JavaScript与Vue调用Qwen3字幕API
前端集成实战用JavaScript与Vue调用Qwen3字幕API对于视频内容创作者来说为视频添加字幕是一项耗时且繁琐的工作。手动听写、打轴、校对一个十分钟的视频可能就要花掉半小时。如果能有一个智能工具自动生成准确的字幕并允许我们快速调整那效率的提升将是巨大的。这正是我们今天要探讨的主题如何将强大的Qwen3字幕生成能力无缝集成到你的Vue.js前端应用中。我们将构建一个完整的视频字幕工作流从上传视频、调用API生成字幕到在前端界面中直观地预览、编辑时间轴最后打包下载带字幕的视频文件。整个过程你只需要一个现代浏览器和基础的JavaScript与Vue知识。1. 项目概述与核心价值想象一下这样一个场景你的用户上传了一段产品演示视频点击一个按钮几分钟后一个带有可编辑时间轴的字幕界面就出现了。用户可以拖动字幕块来微调时间修改识别有误的文字然后一键导出为标准的SRT字幕文件或者直接合成一个带硬字幕的新视频。这不仅能极大提升内容生产的效率也能为你的应用带来独特的竞争力。这个方案的核心价值在于提升效率将数十分钟的手工字幕制作过程压缩到几分钟的自动化流程。改善体验提供直观的可视化编辑界面让非专业用户也能轻松调整字幕。技术整合展示了如何将后端AI能力Qwen3 API与前端富交互框架Vue.js深度结合构建完整的应用功能。我们将使用Vue 3的Composition API和现代JavaScript来构建这个功能。即使你对Vue 3不太熟悉跟着步骤走也能理解核心思路。2. 环境准备与项目搭建首先确保你有一个可以运行的环境。我们将创建一个新的Vue项目并安装必要的依赖。如果你还没有Node.js和npm请先安装它们。然后打开终端运行以下命令来创建一个新的Vue项目# 使用Vite创建项目这是目前最快速轻量的方式 npm create vuelatest my-subtitle-app在创建过程中你可以根据提示选择需要的特性。对于本项目我们至少需要✅ TypeScript (可选但推荐用于更好的开发体验)✅ Vue Router (可选如果你的应用需要多页面)❌ Pinia (状态管理本项目简单可先不用)✅ ESLint (代码规范推荐)项目创建完成后进入目录并安装我们需要的额外库cd my-subtitle-app npm install # 安装我们需要的库用于HTTP请求的axios以及一个简单的视频播放器组件这里以video.js为例你也可以用其他 npm install axios video.js videojs-player/vue npm install --save-dev types/video.js # 如果使用TypeScript现在基本的项目骨架就准备好了。你可以运行npm run dev来启动开发服务器。3. 核心组件设计与实现我们的应用主要包含三个核心部分视频上传/播放器、字幕生成控制区、字幕轨道编辑器。我们将分别构建它们。3.1 视频上传与播放器组件这个组件负责让用户选择视频文件并提供一个播放器来预览视频和监听时间变化。我们创建一个VideoPlayer.vue组件template div classvideo-section div v-if!videoUrl classupload-area clicktriggerFileInput dragover.prevent drophandleDrop input typefile reffileInput changehandleFileChange acceptvideo/* hidden / p点击或拖拽视频文件到此区域上传/p p classhint支持MP4, WebM, MOV等常见格式/p /div div v-else classplayer-container !-- 这里使用video.js播放器你也可以用原生video标签 -- video-player refvideoPlayer :srcvideoUrl controls timeupdatehandleTimeUpdate classvideo-js vjs-big-play-centered / div classvideo-info p当前文件: {{ videoFile?.name }}/p button clickresetVideo classbtn-secondary更换视频/button /div /div /div /template script setup langts import { ref } from vue; import { VideoPlayer } from videojs-player/vue; import video.js/dist/video-js.css; const emit defineEmits([file-selected, time-update]); const fileInput refHTMLInputElement(); const videoFile refFile(); const videoUrl refstring(); const triggerFileInput () { fileInput.value?.click(); }; const handleFileChange (e: Event) { const target e.target as HTMLInputElement; const file target.files?.[0]; if (file file.type.startsWith(video/)) { setVideoFile(file); } }; const handleDrop (e: DragEvent) { e.preventDefault(); const file e.dataTransfer?.files[0]; if (file file.type.startsWith(video/)) { setVideoFile(file); } }; const setVideoFile (file: File) { videoFile.value file; // 创建本地URL用于播放 videoUrl.value URL.createObjectURL(file); emit(file-selected, file); }; const handleTimeUpdate (player: any) { // 发射当前播放时间用于字幕高亮同步 emit(time-update, player.currentTime()); }; const resetVideo () { if (videoUrl.value) { URL.revokeObjectURL(videoUrl.value); // 释放内存 } videoUrl.value ; videoFile.value undefined; if (fileInput.value) { fileInput.value.value ; } }; /script style scoped .video-section { border: 2px dashed #ccc; border-radius: 8px; padding: 2rem; text-align: center; margin-bottom: 2rem; } .upload-area { cursor: pointer; color: #666; } .upload-area:hover { background-color: #f9f9f9; } .hint { font-size: 0.9rem; color: #999; } .player-container { max-width: 800px; margin: 0 auto; } .video-info { margin-top: 1rem; display: flex; justify-content: space-between; align-items: center; } /style3.2 调用字幕生成API这是与后端Qwen3服务交互的核心。我们创建一个可复用的Composition API函数useSubtitleGenerator。// composables/useSubtitleGenerator.js import { ref } from vue; import axios from axios; // 假设你的后端API地址这里需要替换成你实际部署的Qwen3字幕服务地址 const API_BASE_URL https://your-api-server.com/api; export function useSubtitleGenerator() { const isGenerating ref(false); const generationProgress ref(0); const error ref(null); const generateSubtitles async (videoFile, language zh) { isGenerating.value true; error.value null; generationProgress.value 10; // 开始 const formData new FormData(); formData.append(video, videoFile); formData.append(language, language); // 可以根据需要添加其他参数如模型选择、是否包含说话人分离等 // formData.append(model, qwen3-subtitle-large); // formData.append(diarization, true); try { generationProgress.value 30; // 上传中 const response await axios.post(${API_BASE_URL}/generate, formData, { headers: { Content-Type: multipart/form-data, }, // 用于模拟进度实际中可能需要服务器支持或使用WebSocket onUploadProgress: (progressEvent) { const percentCompleted Math.round((progressEvent.loaded * 100) / progressEvent.total); generationProgress.value 30 percentCompleted * 0.5; // 上传占50% }, }); // 假设后端返回一个任务ID我们需要轮询结果 const taskId response.data.task_id; generationProgress.value 80; // 处理中 // 轮询获取结果 const result await pollTaskResult(taskId); generationProgress.value 100; isGenerating.value false; return result; // 返回字幕数据 } catch (err) { console.error(生成字幕失败:, err); error.value err.response?.data?.message || 字幕生成失败请重试或检查网络。; isGenerating.value false; throw err; } }; const pollTaskResult async (taskId, maxAttempts 30, interval 2000) { for (let i 0; i maxAttempts; i) { await new Promise(resolve setTimeout(resolve, interval)); try { const response await axios.get(${API_BASE_URL}/task/${taskId}); const status response.data.status; if (status SUCCESS) { return response.data.result; // 返回字幕数组 } else if (status FAILED) { throw new Error(response.data.error || 任务处理失败); } // 如果状态是PENDING或PROCESSING继续轮询 } catch (err) { throw err; } } throw new Error(任务超时请稍后重试); }; return { isGenerating, generationProgress, error, generateSubtitles, }; }3.3 字幕轨道编辑器组件这是交互的核心用户在这里可以直观地看到每条字幕并拖动调整其开始和结束时间。!-- SubtitleTrack.vue -- template div classsubtitle-track div classtrack-header h3字幕轨道/h3 div classcontrols button clickplayFromStart :disabled!subtitles.length从头播放/button button clickexportSRT导出SRT/button /div /div div classtrack-container reftrackContainer !-- 时间轴刻度 -- div classtime-ruler div v-fortick in timeTicks :keytick classtick :style{ left: ${timeToPosition(tick)}% } span classtick-label{{ formatTime(tick) }}/span /div /div !-- 字幕条 -- div v-for(sub, index) in subtitles :keyindex classsubtitle-item :class{ active: isSubtitleActive(sub) } :style{ left: ${timeToPosition(sub.start)}%, width: ${timeToPosition(sub.end) - timeToPosition(sub.start)}% } mousedownstartDrag(sub, index, $event) dblclickeditText(index) div classsubtitle-handle left mousedown.stopstartResize(index, left)/div div classsubtitle-content {{ sub.text }} /div div classsubtitle-handle right mousedown.stopstartResize(index, right)/div div classsubtitle-time {{ formatTime(sub.start) }} → {{ formatTime(sub.end) }} /div /div /div !-- 文本编辑模态框 -- div v-ifeditingIndex ! null classedit-modal textarea v-modelsubtitles[editingIndex].text rows3/textarea div classmodal-actions button clicksaveEdit保存/button button clickcancelEdit取消/button /div /div /div /template script setup import { ref, computed, onMounted, onUnmounted } from vue; const props defineProps({ subtitles: { type: Array, required: true, default: () [] // 格式: [{ start: 0, end: 5, text: 你好 }, ...] }, currentTime: { type: Number, default: 0 } }); const emit defineEmits([update:subtitles, seek-to, export-srt]); const trackContainer ref(null); const editingIndex ref(null); const dragState ref(null); // { type: move | resize-left | resize-right, index, startX, startTime } // 计算时间轴刻度 const duration computed(() { if (!props.subtitles.length) return 60; const lastSub props.subtitles[props.subtitles.length - 1]; return Math.ceil(lastSub.end / 10) * 10; // 取整到10秒的倍数 }); const timeTicks computed(() { const ticks []; const interval duration.value 120 ? 30 : 10; // 根据总时长决定刻度间隔 for (let t 0; t duration.value; t interval) { ticks.push(t); } return ticks; }); // 工具函数 const timeToPosition (time) (time / duration.value) * 100; const positionToTime (positionPercent) (positionPercent / 100) * duration.value; const formatTime (seconds) { const mins Math.floor(seconds / 60); const secs Math.floor(seconds % 60); const ms Math.floor((seconds % 1) * 1000); return ${mins.toString().padStart(2, 0)}:${secs.toString().padStart(2, 0)}.${ms.toString().padStart(3, 0)}; }; const isSubtitleActive (sub) { return props.currentTime sub.start props.currentTime sub.end; }; // 交互功能 const startDrag (sub, index, e) { dragState.value { type: move, index, startX: e.clientX, startTime: sub.start }; document.addEventListener(mousemove, handleDragMove); document.addEventListener(mouseup, stopDrag); }; const startResize (index, side) { dragState.value { type: resize-${side}, index, startX: event.clientX, startTime: side left ? props.subtitles[index].start : props.subtitles[index].end }; document.addEventListener(mousemove, handleDragMove); document.addEventListener(mouseup, stopDrag); }; const handleDragMove (e) { if (!dragState.value || !trackContainer.value) return; const containerRect trackContainer.value.getBoundingClientRect(); const deltaX e.clientX - dragState.value.startX; const deltaTime (deltaX / containerRect.width) * duration.value; const newSubtitles [...props.subtitles]; const item newSubtitles[dragState.value.index]; if (dragState.value.type move) { const newStart dragState.value.startTime deltaTime; const durationItem item.end - item.start; // 简单边界检查 if (newStart 0 newStart durationItem duration.value) { item.start newStart; item.end newStart durationItem; } } else if (dragState.value.type resize-left) { const newStart dragState.value.startTime deltaTime; if (newStart 0 newStart item.end - 0.5) { // 至少保留0.5秒 item.start newStart; } } else if (dragState.value.type resize-right) { const newEnd dragState.value.startTime deltaTime; if (newEnd duration.value newEnd item.start 0.5) { item.end newEnd; } } emit(update:subtitles, newSubtitles); }; const stopDrag () { dragState.value null; document.removeEventListener(mousemove, handleDragMove); document.removeEventListener(mouseup, stopDrag); }; const editText (index) { editingIndex.value index; }; const saveEdit () { editingIndex.value null; // 数据已通过v-model自动更新 }; const cancelEdit () { editingIndex.value null; }; const playFromStart () { emit(seek-to, 0); }; const exportSRT () { emit(export-srt); }; onUnmounted(() { // 清理事件监听 document.removeEventListener(mousemove, handleDragMove); document.removeEventListener(mouseup, stopDrag); }); /script style scoped /* 样式代码较长主要实现轨道、字幕条、手柄的视觉和交互效果 */ .subtitle-track { border: 1px solid #e0e0e0; border-radius: 8px; padding: 1rem; background: #fafafa; } .track-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; } .track-container { position: relative; height: 120px; background: white; border: 1px solid #ddd; border-radius: 4px; overflow-x: auto; overflow-y: hidden; } .time-ruler { position: absolute; top: 0; width: 100%; height: 30px; border-bottom: 1px solid #ccc; } .tick { position: absolute; bottom: 0; transform: translateX(-50%); } .tick-label { font-size: 0.75rem; color: #666; white-space: nowrap; } .subtitle-item { position: absolute; top: 40px; height: 50px; background-color: #e3f2fd; border: 1px solid #90caf9; border-radius: 4px; cursor: move; user-select: none; overflow: hidden; } .subtitle-item.active { background-color: #bbdefb; border-color: #2196f3; } .subtitle-handle { position: absolute; top: 0; width: 6px; height: 100%; background-color: #64b5f6; cursor: ew-resize; } .subtitle-handle.left { left: 0; } .subtitle-handle.right { right: 0; } .subtitle-content { padding: 8px; height: 100%; display: flex; align-items: center; justify-content: center; text-align: center; font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; } .subtitle-time { position: absolute; bottom: -20px; font-size: 0.7rem; color: #777; white-space: nowrap; } .edit-modal { position: fixed; /* 模态框样式 */ } /style4. 整合与用户体验优化现在我们将所有组件整合到主页面中并添加一些提升用户体验的功能。!-- App.vue 或主页面组件 -- template div classapp-container header h1智能视频字幕工作台/h1 p上传视频自动生成并编辑字幕一键导出/p /header main !-- 视频上传与播放区域 -- VideoPlayer file-selectedhandleVideoSelected time-updatehandleTimeUpdate refvideoPlayer / !-- 控制区域生成字幕 -- div classcontrol-section v-ifvideoFile div classgenerate-controls label识别语言/label select v-modelselectedLanguage option valuezh中文/option option valueen英文/option option valueja日文/option !-- 更多语言选项 -- /select button clickhandleGenerateSubtitles :disabledisGenerating || !videoFile classbtn-primary {{ isGenerating ? 生成中... ${generationProgress}% : 开始生成字幕 }} /button button clickhandleExportWithHardSub :disabled!subtitles.length classbtn-success 合成带字幕视频 /button /div div v-iferror classerror-message {{ error }} /div /div !-- 字幕编辑区域 -- SubtitleTrack v-ifsubtitles.length :subtitlessubtitles :current-timecurrentVideoTime update:subtitlessubtitles $event seek-tohandleSeek export-srthandleExportSRT / !-- 状态提示 -- div v-if!videoFile classempty-state p请先上传一个视频文件开始工作。/p /div div v-else-if!subtitles.length !isGenerating classempty-state p视频已就绪。点击上方的“开始生成字幕”按钮来创建字幕。/p /div /main /div /template script setup import { ref } from vue; import VideoPlayer from ./components/VideoPlayer.vue; import SubtitleTrack from ./components/SubtitleTrack.vue; import { useSubtitleGenerator } from ./composables/useSubtitleGenerator; const videoFile ref(null); const subtitles ref([]); const currentVideoTime ref(0); const selectedLanguage ref(zh); const { isGenerating, generationProgress, error, generateSubtitles } useSubtitleGenerator(); const handleVideoSelected (file) { videoFile.value file; subtitles.value []; // 清除旧字幕 }; const handleTimeUpdate (time) { currentVideoTime.value time; }; const handleGenerateSubtitles async () { if (!videoFile.value) return; try { const result await generateSubtitles(videoFile.value, selectedLanguage.value); // 假设API返回的字幕格式为 [{start, end, text}, ...] subtitles.value result; } catch (err) { console.error(生成过程出错:, err); // 错误信息已由useSubtitleGenerator处理 } }; const handleSeek (time) { // 这里需要调用VideoPlayer组件的方法来跳转播放时间 // 可以通过ref调用子组件方法或使用事件总线/Pinia console.log(跳转到时间:, time); // 例如videoPlayer.value.seek(time); }; const handleExportSRT () { const srtContent convertToSRT(subtitles.value); downloadFile(srtContent, subtitles.srt, text/plain); }; const handleExportWithHardSub async () { // 这里需要调用后端API将视频、字幕和编辑后的时间轴信息发送过去 // 后端使用FFmpeg等工具合成硬字幕视频 alert(合成功能需要后端API支持。这里可以调用一个合成接口。); }; // 工具函数将字幕数组转换为SRT格式字符串 const convertToSRT (subs) { return subs.map((sub, index) { const start formatTimeForSRT(sub.start); const end formatTimeForSRT(sub.end); return ${index 1}\n${start} -- ${end}\n${sub.text}\n; }).join(\n); }; const formatTimeForSRT (seconds) { const hrs Math.floor(seconds / 3600); const mins Math.floor((seconds % 3600) / 60); const secs Math.floor(seconds % 60); const ms Math.floor((seconds % 1) * 1000); return ${hrs.toString().padStart(2, 0)}:${mins.toString().padStart(2, 0)}:${secs.toString().padStart(2, 0)},${ms.toString().padStart(3, 0)}; }; const downloadFile (content, filename, mimeType) { const blob new Blob([content], { type: mimeType }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; /script style /* 全局样式 */ .app-container { max-width: 1200px; margin: 0 auto; padding: 2rem; } .control-section { margin: 2rem 0; padding: 1.5rem; background: #f8f9fa; border-radius: 8px; } .generate-controls { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; } .btn-primary { background-color: #007bff; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; } .btn-primary:disabled { background-color: #ccc; cursor: not-allowed; } .btn-success { background-color: #28a745; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; } .empty-state { text-align: center; padding: 3rem; color: #6c757d; font-style: italic; } .error-message { margin-top: 1rem; padding: 0.75rem; background-color: #f8d7da; color: #721c24; border-radius: 4px; } /style5. 总结与扩展思路通过上面的步骤我们完成了一个具备核心功能的视频字幕前端工作台。从上传视频、调用AI生成字幕到可视化编辑时间轴和文本最后导出标准格式文件整个流程已经跑通。在实际使用中你会发现这种拖拽式编辑比手动输入时间码要直观和高效得多。当然这只是一个起点。在实际项目中你可能会考虑以下扩展方向来让它更强大、更稳定性能优化对于长视频字幕数据量可能很大。可以考虑虚拟滚动来渲染字幕轨道避免DOM节点过多导致卡顿。编辑功能增强添加字幕拆分、合并功能支持批量调整时间偏移以及更丰富的文本样式编辑如字体、颜色、位置为后续合成复杂字幕如ASS做准备。后端集成深化除了生成字幕后端API还可以提供视频切片、语音分离、翻译、以及最终的硬字幕合成服务。前端需要设计更完善的任务状态管理和进度展示。用户体验细节添加快捷键支持如空格键播放/暂停左右键微调时间轴自动保存草稿导入已有字幕文件进行编辑以及更清晰的错误处理和重试机制。部署与协作将前端应用部署到服务器并考虑加入用户系统实现项目保存、团队协作编辑等功能。整合AI能力到前端应用关键在于找到技术与用户体验的最佳结合点。Qwen3提供了强大的字幕生成能力而Vue.js让我们能够构建出响应迅速、交互友好的界面。希望这个实战项目能为你自己的创意应用提供一个坚实的起点。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。