111
This commit is contained in:
@@ -1,12 +1,34 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
import {
|
import {
|
||||||
SUBTITLE_FONT_OPTIONS,
|
SUBTITLE_FONT_OPTIONS,
|
||||||
buildSubtitlePreviewBoxStyle,
|
buildSubtitlePreviewBoxStyle,
|
||||||
} from "../../config/subtitleStylePresets.js";
|
} from "../../config/subtitleStylePresets.js";
|
||||||
|
import { useWorkflowStore } from "../../stores/workflow.js";
|
||||||
|
import {
|
||||||
|
BUILTIN_KEYWORD_GROUP_NAMES,
|
||||||
|
syncWorkflowKeywordsIntoConfig,
|
||||||
|
workflowStoreToKeywordFields,
|
||||||
|
} from "../../utils/workflowKeywordGroups.js";
|
||||||
|
|
||||||
const config = defineModel("config", { type: Object, required: true });
|
const config = defineModel("config", { type: Object, required: true });
|
||||||
|
|
||||||
|
const workflow = useWorkflowStore();
|
||||||
|
const { keywordsFocus, keywordsDescribe, keywordsAction, keywordsEmotion } =
|
||||||
|
storeToRefs(workflow);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[keywordsFocus, keywordsDescribe, keywordsAction, keywordsEmotion],
|
||||||
|
() => {
|
||||||
|
syncWorkflowKeywordsIntoConfig(
|
||||||
|
config.value,
|
||||||
|
workflowStoreToKeywordFields(workflow),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
const importInputRef = ref(null);
|
const importInputRef = ref(null);
|
||||||
const newGroupName = ref("");
|
const newGroupName = ref("");
|
||||||
const editingGroupIndex = ref(-1);
|
const editingGroupIndex = ref(-1);
|
||||||
@@ -17,7 +39,7 @@ const renderModeOptions = [
|
|||||||
{ label: "浮动特效", value: "floating" },
|
{ label: "浮动特效", value: "floating" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const builtinGroupNames = new Set(["行动词", "情感词", "描述词", "重点词/成语词"]);
|
const builtinGroupNames = BUILTIN_KEYWORD_GROUP_NAMES;
|
||||||
|
|
||||||
const enableKeywordEffects = computed({
|
const enableKeywordEffects = computed({
|
||||||
get: () => config.value.enableKeywordEffects !== false,
|
get: () => config.value.enableKeywordEffects !== false,
|
||||||
@@ -215,6 +237,9 @@ async function importGroups(event) {
|
|||||||
|
|
||||||
<div class="rounded-lg border border-slate-600/40 bg-slate-900/40 p-3">
|
<div class="rounded-lg border border-slate-600/40 bg-slate-900/40 p-3">
|
||||||
<h6 class="mb-3 text-sm font-medium text-slate-200">关键词分组管理</h6>
|
<h6 class="mb-3 text-sm font-medium text-slate-200">关键词分组管理</h6>
|
||||||
|
<p class="mb-3 text-xs text-amber-200/90">
|
||||||
|
重点词/描述词/行动词/情感词的内容来自步骤 04「标题标签关键词」,此处仅编辑各分组样式与特效;修改关键词请回到步骤 04。
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="mb-4 rounded bg-slate-800/60 p-3">
|
<div class="mb-4 rounded bg-slate-800/60 p-3">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -284,12 +309,21 @@ async function importGroups(event) {
|
|||||||
color: '#fff',
|
color: '#fff',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
}"
|
}"
|
||||||
class="cursor-pointer"
|
:class="
|
||||||
@click="removeKeyword(group, keyword)"
|
builtinGroupNames.has(group.groupName)
|
||||||
|
? 'cursor-default'
|
||||||
|
: 'cursor-pointer'
|
||||||
|
"
|
||||||
|
@click="
|
||||||
|
!builtinGroupNames.has(group.groupName) &&
|
||||||
|
removeKeyword(group, keyword)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-xs text-slate-500">暂无关键词</div>
|
<div v-else class="text-xs text-slate-500">
|
||||||
<div class="mt-2 flex gap-2">
|
{{ builtinGroupNames.has(group.groupName) ? "请先在步骤 04 生成关键词" : "暂无关键词" }}
|
||||||
|
</div>
|
||||||
|
<div v-if="!builtinGroupNames.has(group.groupName)" class="mt-2 flex gap-2">
|
||||||
<InputText
|
<InputText
|
||||||
v-model="keywordInputs[i]"
|
v-model="keywordInputs[i]"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ const props = defineProps({
|
|||||||
subtitleStyle: { type: Object, default: null },
|
subtitleStyle: { type: Object, default: null },
|
||||||
titleSubtitleConfig: { type: Object, default: null },
|
titleSubtitleConfig: { type: Object, default: null },
|
||||||
keywordGroupsStyles: { type: Array, default: () => [] },
|
keywordGroupsStyles: { type: Array, default: () => [] },
|
||||||
|
/** 步骤 04 关键词,用于预览高亮 */
|
||||||
|
accentKeywords: { type: Array, default: () => [] },
|
||||||
keywordRenderMode: { type: String, default: "inline-emphasis" },
|
keywordRenderMode: { type: String, default: "inline-emphasis" },
|
||||||
previewConfig: { type: Object, default: null },
|
previewConfig: { type: Object, default: null },
|
||||||
isSelected: { type: Boolean, default: false },
|
isSelected: { type: Boolean, default: false },
|
||||||
@@ -95,6 +97,10 @@ const keywordAccentStyle = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const accentList = computed(() =>
|
||||||
|
(props.accentKeywords || []).filter((k) => String(k || "").trim()),
|
||||||
|
);
|
||||||
|
|
||||||
const normalStyle = computed(() =>
|
const normalStyle = computed(() =>
|
||||||
buildBaseTextStyle(props.subtitleStyle, {
|
buildBaseTextStyle(props.subtitleStyle, {
|
||||||
previewScale: props.previewScale,
|
previewScale: props.previewScale,
|
||||||
@@ -135,7 +141,7 @@ const subtitleLines = computed(() => {
|
|||||||
? fallbackInline
|
? fallbackInline
|
||||||
: fallbackClassic;
|
: fallbackClassic;
|
||||||
const lines = inlineEmphasis.value
|
const lines = inlineEmphasis.value
|
||||||
? base.map((line) => injectAccentMarkers(line, true))
|
? base.map((line) => injectAccentMarkers(line, true, accentList.value))
|
||||||
: base;
|
: base;
|
||||||
return lines.map((line) => parseSubtitleLine(line, inlineEmphasis.value));
|
return lines.map((line) => parseSubtitleLine(line, inlineEmphasis.value));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,8 +16,15 @@ import {
|
|||||||
} from "../../services/subtitleTemplateCatalog.js";
|
} from "../../services/subtitleTemplateCatalog.js";
|
||||||
import {
|
import {
|
||||||
createEditDraft,
|
createEditDraft,
|
||||||
|
deepClone,
|
||||||
syncTitleLinesFromReference,
|
syncTitleLinesFromReference,
|
||||||
} from "../../utils/subtitleTemplateDraft.js";
|
} from "../../utils/subtitleTemplateDraft.js";
|
||||||
|
import {
|
||||||
|
collectWorkflowAccentKeywords,
|
||||||
|
syncWorkflowKeywordsIntoConfig,
|
||||||
|
syncWorkflowKeywordsIntoDraft,
|
||||||
|
workflowStoreToKeywordFields,
|
||||||
|
} from "../../utils/workflowKeywordGroups.js";
|
||||||
|
|
||||||
const visible = defineModel("visible", { type: Boolean, default: false });
|
const visible = defineModel("visible", { type: Boolean, default: false });
|
||||||
|
|
||||||
@@ -30,6 +37,8 @@ const emit = defineEmits(["select", "update:selectedId"]);
|
|||||||
const workflow = useWorkflowStore();
|
const workflow = useWorkflowStore();
|
||||||
const { titleGenerated } = storeToRefs(workflow);
|
const { titleGenerated } = storeToRefs(workflow);
|
||||||
|
|
||||||
|
const step4AccentKeywords = computed(() => collectWorkflowAccentKeywords(workflow));
|
||||||
|
|
||||||
const templates = ref([]);
|
const templates = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const feedback = ref({ severity: "", message: "" });
|
const feedback = ref({ severity: "", message: "" });
|
||||||
@@ -75,8 +84,13 @@ function showMsg(severity, message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectTemplate(template) {
|
function selectTemplate(template) {
|
||||||
selectedTemplateId.value = template.id;
|
const picked = deepClone(template);
|
||||||
emit("select", template);
|
syncWorkflowKeywordsIntoConfig(
|
||||||
|
picked.config,
|
||||||
|
workflowStoreToKeywordFields(workflow),
|
||||||
|
);
|
||||||
|
selectedTemplateId.value = picked.id;
|
||||||
|
emit("select", picked);
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +164,10 @@ function onDelete(template) {
|
|||||||
|
|
||||||
function enterEditMode(template, isNew = false) {
|
function enterEditMode(template, isNew = false) {
|
||||||
editingDraft.value = createEditDraft(template);
|
editingDraft.value = createEditDraft(template);
|
||||||
|
syncWorkflowKeywordsIntoDraft(
|
||||||
|
editingDraft.value,
|
||||||
|
workflowStoreToKeywordFields(workflow),
|
||||||
|
);
|
||||||
if (titleGenerated.value?.trim()) {
|
if (titleGenerated.value?.trim()) {
|
||||||
syncTitleLinesFromReference(editingDraft.value, titleGenerated.value);
|
syncTitleLinesFromReference(editingDraft.value, titleGenerated.value);
|
||||||
}
|
}
|
||||||
@@ -270,6 +288,7 @@ function confirmSaveTemplate() {
|
|||||||
:subtitle-style="tpl.config?.subtitleStyle"
|
:subtitle-style="tpl.config?.subtitleStyle"
|
||||||
:title-subtitle-config="tpl.config?.titleSubtitleConfig"
|
:title-subtitle-config="tpl.config?.titleSubtitleConfig"
|
||||||
:keyword-groups-styles="tpl.config?.keywordGroupsStyles"
|
:keyword-groups-styles="tpl.config?.keywordGroupsStyles"
|
||||||
|
:accent-keywords="step4AccentKeywords"
|
||||||
:keyword-render-mode="tpl.config?.keywordRenderMode"
|
:keyword-render-mode="tpl.config?.keywordRenderMode"
|
||||||
:preview-config="tpl.config?.previewConfig"
|
:preview-config="tpl.config?.previewConfig"
|
||||||
:is-selected="selectedTemplateId === tpl.id"
|
:is-selected="selectedTemplateId === tpl.id"
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
import { useWorkflowStore } from "../../stores/workflow.js";
|
||||||
|
import { collectWorkflowAccentKeywords } from "../../utils/workflowKeywordGroups.js";
|
||||||
import SubtitlePreviewCard from "./SubtitlePreviewCard.vue";
|
import SubtitlePreviewCard from "./SubtitlePreviewCard.vue";
|
||||||
import SubtitleStyleSelector from "./SubtitleStyleSelector.vue";
|
import SubtitleStyleSelector from "./SubtitleStyleSelector.vue";
|
||||||
import TitleSubtitleSettings from "./TitleSubtitleSettings.vue";
|
import TitleSubtitleSettings from "./TitleSubtitleSettings.vue";
|
||||||
@@ -7,6 +9,9 @@ import KeywordEffectsSettings from "./KeywordEffectsSettings.vue";
|
|||||||
|
|
||||||
const draft = defineModel("draft", { type: Object, required: true });
|
const draft = defineModel("draft", { type: Object, required: true });
|
||||||
|
|
||||||
|
const workflow = useWorkflowStore();
|
||||||
|
const step4AccentKeywords = computed(() => collectWorkflowAccentKeywords(workflow));
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
referenceTitle: { type: String, default: "" },
|
referenceTitle: { type: String, default: "" },
|
||||||
});
|
});
|
||||||
@@ -39,6 +44,7 @@ const config = computed({
|
|||||||
:subtitle-style="config.subtitleStyle"
|
:subtitle-style="config.subtitleStyle"
|
||||||
:title-subtitle-config="config.titleSubtitleConfig"
|
:title-subtitle-config="config.titleSubtitleConfig"
|
||||||
:keyword-groups-styles="config.keywordGroupsStyles"
|
:keyword-groups-styles="config.keywordGroupsStyles"
|
||||||
|
:accent-keywords="step4AccentKeywords"
|
||||||
:keyword-render-mode="config.keywordRenderMode"
|
:keyword-render-mode="config.keywordRenderMode"
|
||||||
:preview-config="config.previewConfig"
|
:preview-config="config.previewConfig"
|
||||||
:preview-scale="0.42"
|
:preview-scale="0.42"
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ async function onOpenVideoInExplorer() {
|
|||||||
用步骤 01 文案替换识别文本,保留语音时间轴
|
用步骤 01 文案替换识别文本,保留语音时间轴
|
||||||
</p>
|
</p>
|
||||||
<p class="dashboard-muted mt-1 pl-6 text-xs">
|
<p class="dashboard-muted mt-1 pl-6 text-xs">
|
||||||
步骤 04 生成的关键词将以高亮 drawtext 叠加(pulse 缩放特效)
|
步骤 04 关键词按模板分组样式烧录(重点词/描述词/行动词/情感词)
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-2 flex flex-wrap items-center gap-2">
|
<div class="mt-2 flex flex-wrap items-center gap-2">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ensureAppConfigReady } from "../config/videoPipeline.js";
|
|||||||
import { getSubtitleTemplate } from "../config/subtitleTemplates.js";
|
import { getSubtitleTemplate } from "../config/subtitleTemplates.js";
|
||||||
import { getSubtitleTemplateById } from "./subtitleTemplateCatalog.js";
|
import { getSubtitleTemplateById } from "./subtitleTemplateCatalog.js";
|
||||||
import { buildSubtitleBurnStyle } from "../utils/subtitlePreviewStyle.js";
|
import { buildSubtitleBurnStyle } from "../utils/subtitlePreviewStyle.js";
|
||||||
|
import { buildKeywordGroupsForBurn } from "../utils/workflowKeywordGroups.js";
|
||||||
import { localAudioToPlayableUrl } from "./localAudio.js";
|
import { localAudioToPlayableUrl } from "./localAudio.js";
|
||||||
import { localVideoToPlayableUrl } from "./localVideo.js";
|
import { localVideoToPlayableUrl } from "./localVideo.js";
|
||||||
|
|
||||||
@@ -82,6 +83,10 @@ export async function executeGenerateSubtitleAndBgm(store) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enableKeywordEffects =
|
||||||
|
fullTemplate?.config?.enableKeywordEffects !== false;
|
||||||
|
const keywordGroups = buildKeywordGroupsForBurn(fullTemplate, store);
|
||||||
|
|
||||||
store.subtitleBgmGenerating = true;
|
store.subtitleBgmGenerating = true;
|
||||||
clearSubtitlePreview(store);
|
clearSubtitlePreview(store);
|
||||||
|
|
||||||
@@ -94,7 +99,8 @@ export async function executeGenerateSubtitleAndBgm(store) {
|
|||||||
smartSubtitle: store.smartSubtitle,
|
smartSubtitle: store.smartSubtitle,
|
||||||
scriptContent: store.scriptContent || "",
|
scriptContent: store.scriptContent || "",
|
||||||
subtitleStyle,
|
subtitleStyle,
|
||||||
enableKeywordEffects: true,
|
enableKeywordEffects,
|
||||||
|
keywordGroups,
|
||||||
keywordsFocus: store.keywordsFocus || "",
|
keywordsFocus: store.keywordsFocus || "",
|
||||||
keywordsDescribe: store.keywordsDescribe || "",
|
keywordsDescribe: store.keywordsDescribe || "",
|
||||||
keywordsAction: store.keywordsAction || "",
|
keywordsAction: store.keywordsAction || "",
|
||||||
|
|||||||
@@ -195,9 +195,13 @@ export function buildTitleLineStyle(style, index, total, previewScale = 0.33) {
|
|||||||
* @param {string} line
|
* @param {string} line
|
||||||
* @param {boolean} inlineEmphasis
|
* @param {boolean} inlineEmphasis
|
||||||
*/
|
*/
|
||||||
export function injectAccentMarkers(line, inlineEmphasis) {
|
export function injectAccentMarkers(line, inlineEmphasis, accentKeywords = []) {
|
||||||
if (!line || line.includes("[") || line.includes("]")) return line;
|
if (!line || line.includes("[") || line.includes("]")) return line;
|
||||||
const keyword = ACCENT_KEYWORDS.find((k) => line.includes(k));
|
const pool =
|
||||||
|
Array.isArray(accentKeywords) && accentKeywords.length
|
||||||
|
? accentKeywords
|
||||||
|
: ACCENT_KEYWORDS;
|
||||||
|
const keyword = pool.find((k) => k && line.includes(k));
|
||||||
if (keyword) return line.replace(keyword, `[${keyword}]`);
|
if (keyword) return line.replace(keyword, `[${keyword}]`);
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (trimmed.length < 4) return trimmed;
|
if (trimmed.length < 4) return trimmed;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ensureBuiltinKeywordGroups } from "./workflowKeywordGroups.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
* @param {T} value
|
* @param {T} value
|
||||||
@@ -52,6 +54,7 @@ export function createEditDraft(template) {
|
|||||||
if (!draft.config.keywordRenderMode) {
|
if (!draft.config.keywordRenderMode) {
|
||||||
draft.config.keywordRenderMode = "inline-emphasis";
|
draft.config.keywordRenderMode = "inline-emphasis";
|
||||||
}
|
}
|
||||||
|
ensureBuiltinKeywordGroups(draft.config);
|
||||||
return draft;
|
return draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
208
src/utils/workflowKeywordGroups.js
Normal file
208
src/utils/workflowKeywordGroups.js
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
/**
|
||||||
|
* 步骤 04 关键词 ↔ 字幕模板 keywordGroupsStyles(对齐 Electron 四分组)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @template T @param {T} value @returns {T} */
|
||||||
|
function deepClone(value) {
|
||||||
|
return JSON.parse(JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @typedef {{ id: string, groupName: string, storeField: string }} Step4GroupDef */
|
||||||
|
|
||||||
|
/** @type {Step4GroupDef[]} */
|
||||||
|
export const STEP4_KEYWORD_GROUP_DEFS = [
|
||||||
|
{ id: "focus", groupName: "重点词/成语词", storeField: "keywordsFocus" },
|
||||||
|
{ id: "describe", groupName: "描述词", storeField: "keywordsDescribe" },
|
||||||
|
{ id: "action", groupName: "行动词", storeField: "keywordsAction" },
|
||||||
|
{ id: "emotion", groupName: "情感词", storeField: "keywordsEmotion" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BUILTIN_KEYWORD_GROUP_NAMES = new Set(
|
||||||
|
STEP4_KEYWORD_GROUP_DEFS.map((d) => d.groupName),
|
||||||
|
);
|
||||||
|
|
||||||
|
const DEFAULT_BUILTIN_STYLES = {
|
||||||
|
"重点词/成语词": {
|
||||||
|
fontColor: "#FFD700",
|
||||||
|
outlineColor: "#FF8C00",
|
||||||
|
outlineWidth: 4,
|
||||||
|
fontSize: 28,
|
||||||
|
},
|
||||||
|
描述词: {
|
||||||
|
fontColor: "#5AC8FA",
|
||||||
|
outlineColor: "#007AFF",
|
||||||
|
outlineWidth: 3,
|
||||||
|
fontSize: 26,
|
||||||
|
},
|
||||||
|
行动词: {
|
||||||
|
fontColor: "#34C759",
|
||||||
|
outlineColor: "#248A3D",
|
||||||
|
outlineWidth: 3,
|
||||||
|
fontSize: 26,
|
||||||
|
},
|
||||||
|
情感词: {
|
||||||
|
fontColor: "#FF6B6B",
|
||||||
|
outlineColor: "#C92A2A",
|
||||||
|
outlineWidth: 3,
|
||||||
|
fontSize: 26,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function parseKeywordLines(text) {
|
||||||
|
return String(text || "")
|
||||||
|
.split(/[\n,,、]+/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('../services/subtitleTemplateCatalog.js').SubtitleTemplateItem | Record<string, unknown>} templateOrConfig
|
||||||
|
* @returns {Record<string, unknown>}
|
||||||
|
*/
|
||||||
|
function getConfig(templateOrConfig) {
|
||||||
|
if (!templateOrConfig) return {};
|
||||||
|
if (templateOrConfig.config) return templateOrConfig.config;
|
||||||
|
return templateOrConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Step4GroupDef} def
|
||||||
|
* @param {Record<string, unknown>} [subtitleStyle]
|
||||||
|
*/
|
||||||
|
function createDefaultBuiltinGroup(def, subtitleStyle) {
|
||||||
|
const base = DEFAULT_BUILTIN_STYLES[def.groupName] || DEFAULT_BUILTIN_STYLES["重点词/成语词"];
|
||||||
|
return {
|
||||||
|
groupName: def.groupName,
|
||||||
|
description: `步骤04 · ${def.groupName}`,
|
||||||
|
keywords: [],
|
||||||
|
color: base.fontColor,
|
||||||
|
effectId: "pulse-scale",
|
||||||
|
effectDuration: null,
|
||||||
|
styleOverride: {
|
||||||
|
fontName: subtitleStyle?.fontName || "Microsoft YaHei",
|
||||||
|
fontSize: base.fontSize,
|
||||||
|
fontColor: base.fontColor,
|
||||||
|
outlineColor: base.outlineColor,
|
||||||
|
outlineWidth: base.outlineWidth,
|
||||||
|
},
|
||||||
|
soundEffectId: null,
|
||||||
|
soundEffectConfig: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} config
|
||||||
|
*/
|
||||||
|
export function ensureBuiltinKeywordGroups(config) {
|
||||||
|
const cfg = config || {};
|
||||||
|
if (!Array.isArray(cfg.keywordGroupsStyles)) {
|
||||||
|
cfg.keywordGroupsStyles = [];
|
||||||
|
}
|
||||||
|
const list = cfg.keywordGroupsStyles;
|
||||||
|
const subtitleStyle = cfg.subtitleStyle || {};
|
||||||
|
|
||||||
|
for (const def of STEP4_KEYWORD_GROUP_DEFS) {
|
||||||
|
let group = list.find((g) => g?.groupName === def.groupName);
|
||||||
|
if (!group) {
|
||||||
|
group = createDefaultBuiltinGroup(def, subtitleStyle);
|
||||||
|
list.push(group);
|
||||||
|
}
|
||||||
|
if (!group.styleOverride) {
|
||||||
|
group.styleOverride = createDefaultBuiltinGroup(def, subtitleStyle).styleOverride;
|
||||||
|
}
|
||||||
|
if (!group.color && group.styleOverride?.fontColor) {
|
||||||
|
group.color = group.styleOverride.fontColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将步骤 04 关键词写入模板分组(仅更新 keywords,保留样式)
|
||||||
|
* @param {Record<string, unknown>} config
|
||||||
|
* @param {Record<string, string>} storeFields keywordsFocus 等
|
||||||
|
*/
|
||||||
|
export function syncWorkflowKeywordsIntoConfig(config, storeFields) {
|
||||||
|
const groups = ensureBuiltinKeywordGroups(config);
|
||||||
|
for (const def of STEP4_KEYWORD_GROUP_DEFS) {
|
||||||
|
const group = groups.find((g) => g.groupName === def.groupName);
|
||||||
|
if (!group) continue;
|
||||||
|
const lines = parseKeywordLines(storeFields[def.storeField]);
|
||||||
|
group.keywords = lines;
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('../services/subtitleTemplateCatalog.js').SubtitleTemplateItem} draft
|
||||||
|
* @param {Record<string, string>} storeFields
|
||||||
|
*/
|
||||||
|
export function syncWorkflowKeywordsIntoDraft(draft, storeFields) {
|
||||||
|
if (!draft?.config) return;
|
||||||
|
syncWorkflowKeywordsIntoConfig(draft.config, storeFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} store pinia workflow state
|
||||||
|
*/
|
||||||
|
export function workflowStoreToKeywordFields(store) {
|
||||||
|
return {
|
||||||
|
keywordsFocus: store.keywordsFocus || "",
|
||||||
|
keywordsDescribe: store.keywordsDescribe || "",
|
||||||
|
keywordsAction: store.keywordsAction || "",
|
||||||
|
keywordsEmotion: store.keywordsEmotion || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预览用:合并四组全部关键词
|
||||||
|
* @param {Record<string, unknown>} store
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function collectWorkflowAccentKeywords(store) {
|
||||||
|
const fields = workflowStoreToKeywordFields(store);
|
||||||
|
const all = [];
|
||||||
|
for (const def of STEP4_KEYWORD_GROUP_DEFS) {
|
||||||
|
all.push(...parseKeywordLines(fields[def.storeField]));
|
||||||
|
}
|
||||||
|
return [...new Set(all)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成字幕时:步骤04 词 + 模板分组样式
|
||||||
|
* @param {import('../services/subtitleTemplateCatalog.js').SubtitleTemplateItem | null} template
|
||||||
|
* @param {Record<string, unknown>} store
|
||||||
|
* @returns {Array<Record<string, unknown>>}
|
||||||
|
*/
|
||||||
|
export function buildKeywordGroupsForBurn(template, store) {
|
||||||
|
const config = getConfig(template);
|
||||||
|
if (config.enableKeywordEffects === false) return [];
|
||||||
|
|
||||||
|
const fields = workflowStoreToKeywordFields(store);
|
||||||
|
const groups = syncWorkflowKeywordsIntoConfig(deepClone(config), fields);
|
||||||
|
|
||||||
|
return groups
|
||||||
|
.filter((g) => Array.isArray(g.keywords) && g.keywords.length > 0)
|
||||||
|
.map((g) => ({
|
||||||
|
id: g.id || g.groupName,
|
||||||
|
groupName: g.groupName,
|
||||||
|
name: g.groupName,
|
||||||
|
effectId: g.effectId || "pulse-scale",
|
||||||
|
styleOverride: g.styleOverride || {},
|
||||||
|
styleConfig: g.styleOverride || {},
|
||||||
|
keywords: [...g.keywords],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<Record<string, unknown>>} keywordGroupsStyles
|
||||||
|
* @param {string} groupName
|
||||||
|
*/
|
||||||
|
export function findKeywordGroupStyle(keywordGroupsStyles, groupName) {
|
||||||
|
const list = Array.isArray(keywordGroupsStyles) ? keywordGroupsStyles : [];
|
||||||
|
return list.find((g) => g?.groupName === groupName) || null;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user