This commit is contained in:
949036910@qq.com
2026-05-30 18:45:08 +08:00
commit 16b9a007a6
150 changed files with 26852 additions and 0 deletions

View File

@@ -0,0 +1,211 @@
<script setup>
import { computed, ref } from "vue";
import { storeToRefs } from "pinia";
import { useWorkflowStore } from "../../stores/workflow.js";
import SubtitleTemplateDialog from "../subtitle/SubtitleTemplateDialog.vue";
import { revealLocalFileInFolder } from "../../services/fileExport.js";
const workflow = useWorkflowStore();
const {
autoSubtitle,
smartSubtitle,
bgmEnabled,
bgmVolume,
subtitleTemplateId,
subtitleBgmGenerating,
generatedVideoPath,
subtitlePreviewVideoPath,
subtitlePreviewVideoSrc,
bgmPreviewSrc,
} = storeToRefs(workflow);
const feedback = ref({ severity: "", message: "" });
const templateDialogVisible = ref(false);
const hasSourceVideo = computed(() => Boolean(generatedVideoPath.value));
const hasSubtitleVideoPreview = computed(() =>
Boolean(subtitlePreviewVideoSrc.value),
);
const canGenerate = computed(
() =>
hasSourceVideo.value &&
!subtitleBgmGenerating.value &&
(autoSubtitle.value || bgmEnabled.value) &&
(!bgmEnabled.value || Boolean(workflow.bgmPath)),
);
function showFeedback(result) {
if (!result?.message) return;
feedback.value = {
severity: result.ok ? "success" : "error",
message: result.message,
};
}
function onSelectTemplate(template) {
workflow.subtitleTemplateId = template.id;
workflow.subtitleTemplate = template;
}
async function onGenerate() {
feedback.value = { severity: "", message: "" };
showFeedback(await workflow.generateSubtitleAndBgm());
}
async function onPickBgm() {
feedback.value = { severity: "", message: "" };
showFeedback(await workflow.chooseBgm());
}
async function onPreviewBgm() {
feedback.value = { severity: "", message: "" };
const result = await workflow.previewBgm();
if (result?.message) showFeedback(result);
}
async function onOpenVideoInExplorer() {
if (!subtitlePreviewVideoPath.value) {
feedback.value = { severity: "error", message: "暂无字幕预览视频" };
return;
}
try {
await revealLocalFileInFolder(subtitlePreviewVideoPath.value);
} catch (err) {
feedback.value = {
severity: "error",
message: err instanceof Error ? err.message : String(err),
};
}
}
</script>
<template>
<DashboardCard title="字幕和音乐" step="05" :grow="true">
<label class="flex items-center gap-2 text-sm text-slate-200">
<Checkbox v-model="autoSubtitle" binary />
自动生成字幕
</label>
<p class="dashboard-muted mt-1 pl-6 text-xs">
提取视频音频并识别烧录 SRT 字幕到画面
</p>
<label class="mt-2 flex items-center gap-2 text-sm text-slate-200">
<Checkbox v-model="smartSubtitle" binary :disabled="!autoSubtitle" />
启用智能字幕
</label>
<p class="dashboard-muted mt-1 pl-6 text-xs">
用步骤 01 文案替换识别文本保留语音时间轴
</p>
<div class="mt-2 flex flex-wrap items-center gap-2">
<Button
label="模板选择"
size="small"
outlined
icon="pi pi-palette"
:disabled="!autoSubtitle"
@click="templateDialogVisible = true"
/>
<span class="dashboard-muted truncate text-sm">
已选模板: {{ workflow.subtitleTemplateLabel }}
</span>
</div>
<label class="mt-3 flex items-center gap-2 text-sm text-slate-200">
<Checkbox v-model="bgmEnabled" binary />
添加背景音乐
</label>
<div class="mt-2 flex flex-wrap items-center gap-2">
<InputNumber
v-model="bgmVolume"
:min="0"
:max="100"
:disabled="!bgmEnabled"
size="small"
class="w-20"
/>
<span class="text-sm text-slate-400">%</span>
<Button
label="选择音乐"
size="small"
severity="secondary"
:disabled="!bgmEnabled || subtitleBgmGenerating"
@click="onPickBgm"
/>
<Button
label="试听"
size="small"
outlined
:disabled="!bgmEnabled || !workflow.bgmPath"
@click="onPreviewBgm"
/>
</div>
<p v-if="workflow.bgmFileName" class="mt-1 truncate text-xs text-slate-400">
{{ workflow.bgmFileName }}
</p>
<audio
v-if="bgmPreviewSrc"
:src="bgmPreviewSrc"
controls
class="mt-2 h-8 w-full"
/>
<p
v-if="!hasSourceVideo"
class="mt-2 text-xs text-amber-400/90"
>
请先在步骤 02 生成口播视频
</p>
<p
v-if="feedback.message"
class="mt-2 text-sm"
:class="feedback.severity === 'error' ? 'text-red-400' : 'text-emerald-400'"
>
{{ feedback.message }}
</p>
<Button
label="自动生成字幕和BGM"
size="small"
class="mt-3 w-full"
:loading="subtitleBgmGenerating"
:disabled="!canGenerate"
@click="onGenerate"
/>
<div class="dashboard-preview-box mt-3">
<div class="mb-2 flex items-center gap-0.5">
<span class="text-xs text-slate-400">字幕视频预览</span>
<Button
icon="pi pi-folder-open"
size="small"
severity="secondary"
text
:disabled="!subtitlePreviewVideoPath"
aria-label="在资源管理器中打开"
@click="onOpenVideoInExplorer"
/>
</div>
<div class="dashboard-aspect-video">
<video
v-if="hasSubtitleVideoPreview"
:key="subtitlePreviewVideoSrc"
:src="subtitlePreviewVideoSrc"
controls
class="h-full w-full rounded object-contain"
/>
<span v-else class="text-xs text-gray-400">
{{ hasSourceVideo ? "生成字幕后将在此预览" : "请先在步骤 02 生成口播视频" }}
</span>
</div>
</div>
<SubtitleTemplateDialog
v-model:visible="templateDialogVisible"
v-model:selected-id="subtitleTemplateId"
@select="onSelectTemplate"
/>
</DashboardCard>
</template>