Commit cf7d8130 authored by liujiangnan's avatar liujiangnan

feat

parent e73a0d60
......@@ -39,6 +39,8 @@
<el-button type="primary" icon="el-icon-upload">上传视频</el-button>
</el-upload>
<el-button v-if="videoUrl" type="warning" icon="el-icon-refresh" @click="confirmReupload">重新上传</el-button>
<el-button v-if="videoUrl" type="primary" @click="generateSubtitles">{{ subtitlesResult ? '重新生成字幕' : '生成字幕' }}</el-button>
<el-button v-if="videoUrl" type="success" @click="removeVocals">去人声</el-button>
<!-- 上传进度条 -->
<div v-if="uploading" class="upload-progress">
......@@ -60,13 +62,53 @@
<video
ref="videoPlayer"
:src="videoUrl"
controls
style="width: 100%; max-width: 800px; height: auto;">
</video>
<!-- 当前播放时间显示 -->
<div class="time-display">
<span>当前播放时间: {{ currentTime }}</span>
</div>
</div>
<!-- 任务状态看板 -->
<div class="task-board" v-if="(loadingText || task_uuid) && !subtitlesResult" style="max-width:800px;margin-top:16px;">
<el-card shadow="hover">
<div slot="header" class="clearfix">
<span>任务状态</span>
<el-tag v-if="task_uuid" size="mini" type="info" style="float:right;">进行中</el-tag>
</div>
<div class="board-row" style="margin-bottom:8px;">
<span style="color:#909399;">任务UUID:</span>
<span>{{ task_uuid || '—' }}</span>
</div>
<div class="board-row">
<span style="color:#909399;">状态与进度:</span>
<span>{{ loadingText || '—' }}</span>
</div>
</el-card>
</div>
<!-- 生成结果看板 -->
<div class="result-board" v-if="subtitlesResult" style="max-width:800px;margin-top:16px;">
<el-card shadow="hover">
<div slot="header" class="clearfix">
<span>字幕生成结果</span>
<el-tag size="mini" type="success" style="float:right;">已完成</el-tag>
</div>
<div>
<el-alert type="info" :closable="false" show-icon title="点击以下链接可下载或预览对应资源"></el-alert>
<el-table :data="Object.keys(subtitlesResult).map(k => ({ key: k, value: subtitlesResult[k] }))" style="width: 100%; margin-top: 12px;" size="mini" :border="true">
<el-table-column label="生成资源" width="220">
<template slot-scope="scope">
{{ getSubtitleKeyZh(scope.row.key) }}
</template>
</el-table-column>
<el-table-column label="链接">
<template slot-scope="scope">
<a v-if="typeof scope.row.value === 'string' && /^https?:\/\//.test(scope.row.value)" :href="scope.row.value" target="_blank">{{ scope.row.value }}</a>
<span v-else></span>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
</div>
</div>
......
......@@ -63,6 +63,9 @@ function initVueApp(savedData) {
uploading: false,
uploadProgress: 0,
playbackRate: 1,
task_uuid: '',
subtitlesResult: null,
subtitlePollTimer: null,
},
mounted() {
// 如果有保存的数据,恢复状态
......@@ -74,6 +77,15 @@ function initVueApp(savedData) {
});
}
}
// 如果存在未完成的任务,恢复轮询
if (savedData && savedData.task_uuid) {
this.task_uuid = savedData.task_uuid;
this.startSubtitlePolling();
}
// 如果已保存的字幕结果,恢复到内存
if (savedData && savedData.subtitlesResult) {
this.subtitlesResult = savedData.subtitlesResult;
}
},
beforeDestroy() {
......@@ -81,8 +93,29 @@ function initVueApp(savedData) {
if (this.syncInterval) {
clearInterval(this.syncInterval);
}
if (this.subtitlePollTimer) {
clearInterval(this.subtitlePollTimer);
this.subtitlePollTimer = null;
}
},
methods: {
// 结果 key -> 中文名称
getSubtitleKeyZh(key) {
const map = {
audioUrl: '提取音频',
assUrl: 'ASS 字幕',
srtUrl: 'SRT 字幕',
vttUrl: 'VTT 字幕',
chineseAssUrl: '中文字幕(ASS)',
chineseSrtUrl: '中文字幕(SRT)',
chineseVttUrl: '中文字幕(VTT)',
bilingualAssUrl: '双语字幕(ASS)',
subtitleJsonUrl: '字幕 JSON',
translatedJsonUrl: '翻译 JSON',
outputVideoUrl: '双语字幕合成视频'
};
return map[key] || key;
},
// 检查 URL 资源是否有效
async checkUrlValid(url) {
try {
......@@ -104,7 +137,68 @@ function initVueApp(savedData) {
},
onContainerClick() {
},
// 环境基地址
getApiBase() {
const host = (window.location && window.location.hostname || '').toLowerCase();
const isStaging = host.includes('staging') || host.includes('localhost') || host.includes('127.0.0.1');
return isStaging ? 'https://stagingopensource.iteachabc.com' : 'https://opensource.iteachabc.com';
},
// 开始字幕任务轮询
startSubtitlePolling(taskUuid) {
const uuid = taskUuid || this.task_uuid;
if (!uuid) return;
// 避免重复轮询
this.stopSubtitlePolling();
const base = this.getApiBase();
this.loadingText = '字幕任务已创建,正在处理...';
this.subtitlePollTimer = setInterval(async () => {
try {
const resp = await fetch(`${base}/open/api/ai/dpe/v1/common/task?task_uuid=${encodeURIComponent(uuid)}`, {
method: 'GET',
mode: 'cors',
cache: 'no-cache'
});
const json = await resp.json();
if (json && json.code === 200 && json.data) {
const row = json.data;
const status = row.status_;
const progress = row.progress || 0;
const progressMsg = row.progress_msg || '';
this.loadingText = `字幕处理状态:${status==0?'未开始':status==1?'进行中':status==2?'已完成':status==3?'失败':'未知'} 进度:${progress}% ${progressMsg}`;
if (status === 2) {
// 完成
this.stopSubtitlePolling();
this.$message.success('字幕生成完成');
this.subtitlesResult = row.res_data || null;
this.loadingText = '';
this.saveData(); // 存储结果
// 移除 task_uuid
this.task_uuid = '';
this.saveData();
} else if (status === 3) {
// 失败
this.stopSubtitlePolling();
const err = row.error_msg || '字幕生成失败';
this.$message.error(err);
}
} else {
// 非200或无数据,不终止,继续轮询
}
} catch (e) {
// 网络错误,不终止,继续轮询
}
}, 1000);
},
stopSubtitlePolling() {
if (this.subtitlePollTimer) {
clearInterval(this.subtitlePollTimer);
this.subtitlePollTimer = null;
}
},
async deepCheckUtilHave(url) {
......@@ -336,40 +430,83 @@ function initVueApp(savedData) {
togglePlayPause() {
},
// 设置播放速度 - 波形图控制视频速度
setPlaybackRate() {
// 同步视频的播放速度
if (this.$refs.videoPlayer) {
this.$refs.videoPlayer.playbackRate = parseFloat(this.playbackRate);
// 生成字幕
async generateSubtitles() {
const videoUrl = (this.videoUrl || '').replace('_mini.mp4', '.mp4');
if (!videoUrl) {
this.$message.warning('请先上传或选择视频');
return;
}
if (this.task_uuid) {
this.$message.warning('字幕任务正在处理中,请稍后再试');
return;
}
if (this.subtitlesResult != null) {
// 提示框已有字幕是否重新生成
this.$confirm('字幕已生成,重新生成将覆盖当前字幕,是否重新生成?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.task_uuid = null;
this.subtitlesResult = null;
this.loadingText = '';
this.saveData();
this.generateSubtitles();
}).catch(() => {
// 用户取消操作
});
return;
}
// 新任务启动前,清空旧结果与文案
this.subtitlesResult = null;
this.loadingText = '';
const base = this.getApiBase();
const loading = this.$loading({ lock: true, text: '正在提交字幕任务...', spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)' });
try {
const resp = await fetch(`${base}/open/api/ai/dpe/v1/video/subtitles`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ video_url: videoUrl })
});
const json = await resp.json();
if (json && json.code === 200 && json.task_uuid) {
this.$message.success('字幕任务创建成功');
this.task_uuid = json.task_uuid;
this.saveData(); // 存储 task_uuid
this.startSubtitlePolling();
} else {
this.$message.error(json && (json.message || json.msg) ? (json.message || json.msg) : '创建字幕任务失败');
}
} catch (e) {
this.$message.error('创建字幕任务失败');
} finally {
loading.close();
}
},
// 绑定视频元素事件监听器
bindVideoEvents() {
if (!this.$refs.videoPlayer) return;
const video = this.$refs.videoPlayer;
// 监听视频结束事件,同步波形图状态
video.addEventListener('ended', () => {
this.isPlaying = false;
});
// 监听视频加载事件,确保波形图静音
video.addEventListener('loadedmetadata', () => {
});
// 去人声
async removeVocals() {
try {
this.$message.info('开始去人声处理...');
// TODO: 在此处接入去人声的业务逻辑(例如调用后端音频分离服务)
} catch (e) {
this.$message.error('去人声失败');
}
},
// 监听视频播放事件,确保波形图静音
video.addEventListener('play', () => {
});
},
// 数据持久化
saveData() {
const data = {
videoUrl: this.videoUrl.replace('_mini.mp4', '.mp4'),
videoUrl: this.videoUrl ? this.videoUrl.replace('_mini.mp4', '.mp4') : '',
playbackRate: this.playbackRate,
};
if (this.task_uuid) {
data.task_uuid = this.task_uuid;
}
if (this.subtitlesResult != null) {
data.subtitlesResult = this.subtitlesResult;
}
window.courseware.setData(data, function () {
}, key);
},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment