Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
A
ai_fundub
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
template admin
ai_fundub
Commits
cf7d8130
Commit
cf7d8130
authored
Oct 27, 2025
by
liujiangnan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat
parent
e73a0d60
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
210 additions
and
31 deletions
+210
-31
index.html
form/index.html
+47
-5
index.js
form/index.js
+163
-26
No files found.
form/index.html
View file @
cf7d8130
...
...
@@ -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>
...
...
form/index.js
View file @
cf7d8130
...
...
@@ -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
);
},
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment