From 0ff2ee623c5ca0ffe6e21782f3d5706cce0d1b9b Mon Sep 17 00:00:00 2001 From: du <13220750630.163.com> Date: 星期三, 09 四月 2025 15:42:26 +0800 Subject: [PATCH] 字幕 --- easegen-front/src/views/myCourse/index.vue | 453 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 files changed, 371 insertions(+), 82 deletions(-) diff --git a/easegen-front/src/views/myCourse/index.vue b/easegen-front/src/views/myCourse/index.vue index c34c3e6..0977127 100644 --- a/easegen-front/src/views/myCourse/index.vue +++ b/easegen-front/src/views/myCourse/index.vue @@ -31,7 +31,6 @@ </ContentWrap> <!-- 鍒楄〃 --> - <!-- 鍒楄〃 --> <ContentWrap> <el-table v-loading="loading" :data="list"> <el-table-column :label="t('myCourse.videoCode')" align="center" prop="id" /> @@ -60,11 +59,6 @@ width="120" :formatter="dateFormatter" /> -<!-- <el-table-column :label="t('myCourse.progress')" align="center" prop="progress">--> -<!-- <template #default="scope">--> -<!-- <el-progress :percentage="getProgress(scope.row.status, scope.row.progress)" />--> -<!-- </template>--> -<!-- </el-table-column>--> <el-table-column :label="t('myCourse.SynthesisTime')" align="center"> <template #default="scope"> {{ calculateDuration(scope.row.createTime, scope.row.finishTime) }} @@ -121,6 +115,13 @@ </template> <el-button link + type="primary" + @click="openSubtitleDialog(scope.row.id)" + > + 瀛楀箷 + </el-button> + <el-button + link type="danger" @click="handleDelete(scope.row.id)" > @@ -137,8 +138,89 @@ @pagination="getList" /> </ContentWrap> + <!-- 瑙嗛鎾斁寮规 --> <videoDialog ref="videoRef" /> + + <!-- 瀛楀箷鐢熸垚寮规 --> + <el-dialog + v-model="subtitleDialogVisible" + title="瀛楀箷鐢熸垚" + width="60%" + @closed="resetSubtitleForm" + > + <el-form :model="subtitleForm" ref="subtitleFormRef"> + <el-row> + <el-col :span="8"> + <el-form-item label="鏂彞鏃堕棿闃堝��" prop="timeThreshold" :rules="[ + { required: true, message: '璇疯緭鍏ユ柇鍙ユ椂闂撮槇鍊�', trigger: 'blur' }, + { pattern: /^\d+(\.\d+)?$/, message: '璇疯緭鍏ユ湁鏁堟暟瀛�', trigger: 'blur' } + ]"> + <el-input v-model="subtitleForm.timeThreshold" placeholder="渚嬪锛�0.05" clearable /> + </el-form-item> + </el-col> + <el-col :span="8"> + <el-form-item label="璇█" prop="language" :rules="[ + { required: true, message: '璇烽�夋嫨璇█', trigger: 'change' } + ]"> + <el-select v-model="subtitleForm.language" placeholder="璇烽�夋嫨璇█" clearable> + <el-option label="涓枃" value="zh" /> + <el-option label="鑻辨枃" value="en" /> + </el-select> + </el-form-item> + </el-col> + <el-col :span="8"> + <el-form-item> + <el-button + type="primary" + @click="generateSubtitles" + :loading="generating || polling" + > + 鐢熸垚瀛楀箷 + </el-button> + <el-button + type="primary" + @click="triggerFileUpload" + > + 涓婁紶SRT鏂囦欢 + <input + ref="fileInput" + type="file" + accept=".srt" + style="display: none" + @change="handleFileUpload" + /> + </el-button> + </el-form-item> + </el-col> + </el-row> + <el-form-item label="瀛楀箷鍐呭" prop="content" :rules="[ + { required: true, message: '璇峰厛鐢熸垚鎴栦笂浼犲瓧骞曞唴瀹�', trigger: 'blur' } + ]"> + <el-input + v-model="subtitleForm.content" + type="textarea" + :rows="10" + placeholder="瀛楀箷鍐呭灏嗘樉绀哄湪杩欓噷锛圫RT鏍煎紡锛�" + resize="none" + /> + </el-form-item> + </el-form> + + <template #footer> + <div class="dialog-footer"> + <el-button @click="subtitleDialogVisible = false">鍙栨秷</el-button> + <el-button + type="primary" + @click="saveSubtitles" + :loading="saving" + :disabled="!subtitleForm.content" + > + 淇濆瓨瀛楀箷 + </el-button> + </div> + </template> + </el-dialog> </template> <script lang="ts" setup> import { DICT_TYPE } from '@/utils/dict' @@ -147,21 +229,43 @@ import * as pptTemplateApi from '@/api/pptTemplate' import { useRouter } from 'vue-router' import videoDialog from "./videoDialog.vue" -const router = useRouter() // 璺敱 -const message = useMessage() // 娑堟伅寮圭獥 -const { t } = useI18n() // 鍥介檯鍖� +import { getAccessToken, getTenantId } from "@/utils/auth" +import axios from 'axios' +import { config } from '@/config/axios/config' +const router = useRouter() +const message = useMessage() +const { t } = useI18n() +const polling = ref(false) +let pollingTimer: number | null = null -const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑 -const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁� -const list = ref([]) // 鍒楄〃鐨勬暟鎹� +// 瑙嗛鍒楄〃鐩稿叧鏁版嵁 +const loading = ref(true) +const total = ref(0) +const list = ref([]) const queryParams = reactive({ pageNo: 1, pageSize: 20, name: undefined }) -const queryFormRef = ref() // 鎼滅储鐨勮〃鍗� +const queryFormRef = ref() -/** 鏌ヨ鍒楄〃 */ +// 瑙嗛棰勮鐩稿叧 +const videoRef = ref() + +// 瀛楀箷寮规鐩稿叧 +const subtitleDialogVisible = ref(false) +const subtitleFormRef = ref() +const fileInput = ref<HTMLInputElement | null>(null) +const subtitleForm = reactive({ + videoId: null as number | null, + timeThreshold: '0.05', + language: 'zh', + content: '' +}) +const generating = ref(false) +const saving = ref(false) + +// 鑾峰彇瑙嗛鍒楄〃 const getList = async () => { loading.value = true try { @@ -173,54 +277,51 @@ } } -/** 鎼滅储鎸夐挳鎿嶄綔 */ +// 鎼滅储瑙嗛 const handleQuery = () => { queryParams.pageNo = 1 getList() } -/** 閲嶇疆鎸夐挳鎿嶄綔 */ +// 閲嶇疆鎼滅储 const resetQuery = () => { queryFormRef.value.resetFields() handleQuery() } -/** 棰勮鎸夐挳*/ -const videoRef = ref() +// 棰勮瑙嗛 const openPreview = (row) => { if(row){ - videoRef.value.open(row.previewUrl, row.subtitlesVttUrl); + videoRef.value.open(row.previewUrl, row.subtitlesVttUrl) } } -/** 鍒犻櫎鎸夐挳鎿嶄綔 */ +// 鍒犻櫎瑙嗛 const handleDelete = async (id: number) => { try { - // 鍒犻櫎鐨勪簩娆$‘璁� await message.delConfirm() - // 鍙戣捣鍒犻櫎 await pptTemplateApi.deleteMyCourse(id) message.success(t('common.delSuccess')) - // 鍒锋柊鍒楄〃 await getList() } catch {} } -/** 涓嬭浇鎸夐挳鎿嶄綔 */ +// 涓嬭浇鏂囦欢 const handleDownload = (url, courseName) => { - //濡傛灉url涓虹┖锛屽垯鎻愮ず鏈壘鍒拌祫婧愭枃浠� if (!url) { - message.warning("鏈壘鍒拌祫婧愭枃浠讹紒"); - return; + message.warning("鏈壘鍒拌祫婧愭枃浠讹紒") + return } - const link = document.createElement('a'); - link.href = url; - link.download = courseName; - link.target = '_blank'; // 寮哄埗鏂版爣绛鹃〉涓嬭浇 - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); -}; + const link = document.createElement('a') + link.href = url + link.download = courseName + link.target = '_blank' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) +} + +// 璺宠浆鍒拌绋嬭鎯� const goDetail = (id) => { pptTemplateApi.coursesDetail(id).then((res) => { if (!res) { @@ -228,73 +329,261 @@ return } if (res.pageMode === 2 || res.pageMode === 0) { - router.push({ path: '/chooseTemplate/index', query: { id } }); + router.push({ path: '/chooseTemplate/index', query: { id } }) } else if (res.pageMode === 3) { - router.push({ path: '/chooseTemplate/speakvideo', query: { id } }); + router.push({ path: '/chooseTemplate/speakvideo', query: { id } }) } }) } -/** 鏍煎紡鍖栬棰戞椂闀� */ +// 鏍煎紡鍖栬棰戞椂闀� const formatDuration = (milliseconds: number) => { - const seconds = Math.floor(milliseconds / 1000); - const hrs = Math.floor(seconds / 3600); - const mins = Math.floor((seconds % 3600) / 60); - const secs = seconds % 60; - return `${hrs > 0 ? `${hrs}鏃禶 : ''}${mins > 0 ? `${mins}鍒哷 : ''}${secs}绉抈; + const seconds = Math.floor(milliseconds / 1000) + const hrs = Math.floor(seconds / 3600) + const mins = Math.floor((seconds % 3600) / 60) + const secs = seconds % 60 + return `${hrs > 0 ? `${hrs}鏃禶 : ''}${mins > 0 ? `${mins}鍒哷 : ''}${secs}绉抈 } -/** 閲嶆柊鍚堟垚鎸夐挳鎿嶄綔 */ +// 閲嶆柊鍚堟垚瑙嗛 const reMegerMedia = async (id: number) => { try { - // 寮�鍚� loading loading.value = true - - // 閲嶆柊鍚堟垚瑙嗛 - const res = await pptTemplateApi.reMegerMedia({ id }); - - console.log("---------", res); + const res = await pptTemplateApi.reMegerMedia({ id }) if (res) { - message.success("鍚堟垚瑙嗛浠诲姟鎻愪氦鎴愬姛锛岃鍒版垜鐨勮棰戜腑鏌ョ湅锛�"); + message.success("鍚堟垚瑙嗛浠诲姟鎻愪氦鎴愬姛锛岃鍒版垜鐨勮棰戜腑鏌ョ湅锛�") + } + } catch (error) { + console.error(error) + } finally { + getList() + loading.value = false + } +} + +// 璁$畻鍚堟垚鑰楁椂 +const calculateDuration = (createTime: string, finishTime: string) => { + if (!createTime || !finishTime) return '鏈畬鎴�' + + const start = new Date(createTime).getTime() + const end = new Date(finishTime).getTime() + + const duration = (end - start) / 1000 + const hrs = Math.floor(duration / 3600) + const mins = Math.floor((duration % 3600) / 60) + const secs = Math.floor(duration % 60) + + return `${hrs > 0 ? `${hrs}鏃禶 : ''}${mins > 0 ? `${mins}鍒哷 : ''}${secs}绉抈 +} + +// 鎵撳紑瀛楀箷寮规 +const openSubtitleDialog = (videoId: number) => { + subtitleForm.videoId = videoId + subtitleDialogVisible.value = true +} + +// 閲嶇疆瀛楀箷琛ㄥ崟 +const resetSubtitleForm = () => { + subtitleFormRef.value?.resetFields() + subtitleForm.videoId = null + subtitleForm.content = '' +} + +// 瑙﹀彂鏂囦欢涓婁紶 +const triggerFileUpload = () => { + fileInput.value?.click() +} + +// 澶勭悊鏂囦欢涓婁紶 +const handleFileUpload = async (event: Event) => { + const input = event.target as HTMLInputElement + if (!input.files?.length) return + + const file = input.files[0] + if (!file.name.endsWith('.srt')) { + message.warning('璇蜂笂浼燬RT鏍煎紡鐨勫瓧骞曟枃浠�') + return + } + + try { + const content = await readFileAsText(file) + subtitleForm.content = content + message.success('瀛楀箷鏂囦欢涓婁紶鎴愬姛') + } catch (error) { + message.error('璇诲彇瀛楀箷鏂囦欢澶辫触') + console.error(error) + } finally { + input.value = '' + } +} + +// 璇诲彇鏂囦欢涓烘枃鏈� +const readFileAsText = (file: File): Promise<string> => { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = (e) => resolve(e.target?.result as string) + reader.onerror = (e) => reject(e) + reader.readAsText(file) + }) +} + +// 鐢熸垚瀛楀箷 +const generateSubtitles = async () => { + try { + await subtitleFormRef.value.validateField(['timeThreshold', 'language']) + + if (!subtitleForm.videoId) { + message.warning('瑙嗛ID涓嶈兘涓虹┖') + return } + generating.value = true + + const params = { + videoId: subtitleForm.videoId, + timeThreshold: parseFloat(subtitleForm.timeThreshold), + language: subtitleForm.language + } + await pptTemplateApi.generateSubtitles(params) + message.success('瀛楀箷鐢熸垚浠诲姟宸插紑濮�') + + const maxAttempts = 20 + const interval = 3000 + let attempts = 0 + + const poll = async () => { + polling.value = true + attempts++ + + try { + const videoDetail = await pptTemplateApi.myCourseDetail(subtitleForm.videoId!) + console.log('杞缁撴灉:', videoDetail) + if (videoDetail.subtitlesStatus === 2) { + if (videoDetail.subtitlesUrl) { + try { + const response = await fetch(videoDetail.subtitlesUrl) + if (response.ok) { + const srtContent = await response.text() + subtitleForm.content = srtContent + } + } catch (error) { + console.error('Error fetching SRT file:', error) + } + } else if (videoDetail.subtitlesContent) { + subtitleForm.content = videoDetail.subtitlesContent + } + message.success('瀛楀箷鐢熸垚鎴愬姛') + stopPolling() + } else if (videoDetail.subtitlesStatus === 3) { + message.error(`瀛楀箷鐢熸垚澶辫触: ${videoDetail.errorReason || '鏈煡鍘熷洜'}`) + stopPolling() + } else if (attempts >= maxAttempts) { + message.warning('瀛楀箷鐢熸垚瓒呮椂锛岃绋嶅悗鎵嬪姩妫�鏌�') + stopPolling() + } else { + pollingTimer = window.setTimeout(poll, interval) + } + } catch (error) { + console.error('杞鍑洪敊:', error) + if (attempts >= maxAttempts) { + message.error('瀛楀箷鐘舵�佹鏌ヨ秴鏃�') + stopPolling() + } else { + pollingTimer = window.setTimeout(poll, interval) + } + } + } + + poll() } catch (error) { - console.error(error); + console.error('鐢熸垚瀛楀箷鍑洪敊:', error) + message.error(`鐢熸垚瀛楀箷澶辫触: ${error.message || '鏈煡閿欒'}`) + stopPolling() } finally { + generating.value = false + } +} + +const saveSubtitles = async () => { + try { + saving.value = true + + // 1. 灏嗗瓧骞曞唴瀹硅浆鎹负SRT鏍煎紡 + const srtContent = formatToSrt(subtitleForm.content) + + // 2. 鍒涘缓Blob瀵硅薄琛ㄧずSRT鏂囦欢 + const blob = new Blob([srtContent], { type: 'text/plain' }) + const file = new File([blob], 'subtitles.srt', { type: 'text/plain' }) + + // 3. 鍒涘缓FormData骞舵坊鍔犳枃浠� + const formData = new FormData() + formData.append('file', file) + + // 4. 涓婁紶鏂囦欢 - 浣跨敤 axios 鏇夸唬 request + const uploadResponse = await axios({ + url: config.base_url+'/infra/file/upload', + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data', + 'Authorization': `Bearer ${getAccessToken()}`, + 'tenant-id': getTenantId() + } + }) + + // 5. 璋冪敤淇濆瓨瀛楀箷鎺ュ彛 + const params = { + id: subtitleForm.videoId, + subtitlesUrl: uploadResponse.data.data, + } + + await pptTemplateApi.saveSubtitles(params) + message.success('瀛楀箷淇濆瓨鎴愬姛') + subtitleDialogVisible.value = false + // 鍒锋柊鍒楄〃 - getList(); - // 鏃犺鎴愬姛杩樻槸澶辫触锛岄兘闇�瑕佸叧闂� loading - loading.value = false; + getList() + } catch (error) { + console.error('淇濆瓨瀛楀箷澶辫触:', error) + message.error(`淇濆瓨瀛楀箷澶辫触: ${error.message || '鏈煡閿欒'}`) + } finally { + saving.value = false } } - -/** 鏍规嵁鍒涘缓鏃堕棿鍜屽畬鎴愭椂闂磋绠楀悎鎴愯�楁椂 */ -const calculateDuration = (createTime: string, finishTime: string) => { - if (!createTime || !finishTime) return '鏈畬鎴�'; - - const start = new Date(createTime).getTime(); - const end = new Date(finishTime).getTime(); - - const duration = (end - start) / 1000; // 杞崲涓虹 - const hrs = Math.floor(duration / 3600); - const mins = Math.floor((duration % 3600) / 60); - const secs = Math.floor(duration % 60); - - return `${hrs > 0 ? `${hrs}鏃禶 : ''}${mins > 0 ? `${mins}鍒哷 : ''}${secs}绉抈; -} - -/** 鑾峰彇鍚堟垚杩涘害锛屽叧鑱旂姸鎬� */ -const getProgress = (status: number, completionPercentage: number) => { - if (status === 2) { - return 100; // 宸插畬鎴愶紝杩涘害鍥哄畾涓�100% - } else if (status === 0) { - return 0; // 鏈惎鍔紝杩涘害鍥哄畾涓�0% - } else { - return completionPercentage || 0; // 鍚堟垚涓垨鍚堟垚澶辫触锛屾寜瀹為檯杩涘害灞曠ず +// 灏嗘枃鏈唴瀹规牸寮忓寲涓篠RT鏍煎紡 +const formatToSrt = (content: string): string => { + if (content.trim().match(/^\d+\s+\d{2}:\d{2}:\d{2},\d{3}\s-->\s\d{2}:\d{2}:\d{2},\d{3}/)) { + return content } + + const lines = content.split('\n').filter(line => line.trim()) + let srtContent = '' + + lines.forEach((line, index) => { + srtContent += `${index + 1}\n` + srtContent += `00:00:${String(index).padStart(2, '0')},000 --> 00:00:${String(index + 1).padStart(2, '0')},000\n` + srtContent += `${line}\n\n` + }) + + return srtContent } -/** 鍒濆鍖� **/ -onMounted(async () => { - await getList() + +// 鍋滄杞 +const stopPolling = () => { + if (pollingTimer) { + clearTimeout(pollingTimer) + pollingTimer = null + } + polling.value = false +} + +// 娓呯悊瀹氭椂鍣� +onBeforeUnmount(() => { + stopPolling() +}) + +// 鍒濆鍖� +onMounted(() => { + getList() }) </script> -- Gitblit v1.9.3