This commit is contained in:
2026-06-30 15:02:20 +08:00
commit 3948b5a48a
306 changed files with 77275 additions and 0 deletions
@@ -0,0 +1,877 @@
import { ref, reactive, watch, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { characterAPI } from '@/api/characters'
import { characterLibraryAPI } from '@/api/characterLibrary'
import { dramaAPI } from '@/api/drama'
import { generationAPI } from '@/api/generation'
import { uploadAPI } from '@/api/upload'
import { useGenerationTaskStore, GEN_RESOURCE } from '@/stores/generationTaskStore'
import { buildExtractTaskMeta, isEpisodeExtractRunning } from '@/composables/useGenerationTaskSync'
/**
* 角色管理 Composable
* @param {object} deps - 共享依赖
* @param {object} deps.store - Pinia store
* @param {import('vue').ComputedRef} deps.dramaId
* @param {import('vue').ComputedRef} deps.currentEpisodeId
* @param {Function} deps.getSelectedStyle - 获取当前生成风格
* @param {Function} deps.loadDrama - 重新加载剧集数据
* @param {Function} deps.pollTask - 轮询异步任务
* @param {Function} deps.pollUntilResourceHasImage - 等待资源有图片
* @param {Function} deps.hasAssetImage - 判断资源是否有图片
*/
export function useCharacters(deps) {
const { store, dramaId, currentEpisodeId, getSelectedStyle, loadDrama, pollTask, pollUntilResourceHasImage, hasAssetImage } = deps
const genStore = useGenerationTaskStore()
function buildCharImageMeta(char) {
const dramaTitle = store.drama?.title || ''
const epNum = store.currentEpisode?.episode_number
const epLabel = dramaTitle ? `${dramaTitle} · 第${epNum ?? ''}` : `${epNum ?? ''}`
return {
dramaId: dramaId.value,
episodeId: currentEpisodeId.value,
dramaTitle,
episodeNumber: epNum,
resourceType: GEN_RESOURCE.CHAR_IMAGE,
resourceId: char.id,
label: `${epLabel} 角色图: ${char.name || char.id}`,
}
}
function dataUrlToFile(dataUrl, filename) {
const arr = dataUrl.split(',')
const mime = (arr[0].match(/:(.*?);/) || [])[1] || 'image/png'
const bstr = atob(arr[1])
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) u8arr[n] = bstr.charCodeAt(n)
return new File([u8arr], filename || 'reference.png', { type: mime })
}
// ── 角色弹窗状态 ─────────────────────────────────────
const showEditCharacter = ref(false)
const editCharacterForm = ref(null)
const editCharacterSaving = ref(false)
const editCharacterPromptGenerating = ref(false)
const extractingCharAppearance = ref(false)
const extractingAnchors = ref(false)
const addCharRefImage = ref(null) // { dataUrl, filename }
const addCharRefFileInput = ref(null)
let editCharacterPollTimer = null
// ── 角色生成状态 ──────────────────────────────────────
/** 仅当前集「提取角色」进行中时为 true(按集隔离,切集不误显示 loading) */
const charactersGenerating = computed(() =>
isEpisodeExtractRunning(genStore, dramaId.value, currentEpisodeId.value, GEN_RESOURCE.EXTRACT_CHARACTERS)
)
const generatingCharIds = reactive(new Set())
const sd2CertifyingId = ref(null)
const showCharSd2Cert = ref(false)
const charSd2CertPayload = ref(null)
const sd2VoiceUploadingId = ref(null)
// ── 角色库状态 ────────────────────────────────────────
const showCharLibrary = ref(false)
const charLibraryList = ref([])
const charLibraryLoading = ref(false)
const charLibraryPage = ref(1)
const charLibraryPageSize = ref(20)
const charLibraryTotal = ref(0)
const charLibraryKeyword = ref('')
const showEditCharLibrary = ref(false)
const editCharLibraryForm = ref(null)
const editCharLibrarySaving = ref(false)
const addingCharToLibraryId = ref(null)
const addingCharToMaterialId = ref(null)
const addingCharFromLibraryId = ref(null)
let charLibraryKeywordTimer = null
/** 角色库弹窗 Tablibrary | drama | team */
const charLibraryTab = ref('library')
const dramaAllCharList = ref([])
const dramaAllCharLoading = ref(false)
const dramaAllCharPage = ref(1)
const dramaAllCharPageSize = ref(20)
const dramaAllCharTotal = ref(0)
const dramaAllCharKeyword = ref('')
let dramaAllCharKeywordTimer = null
// ── 常量 ──────────────────────────────────────────────
const CHAR_ROLE_LABEL = { main: '主角', supporting: '配角', minor: '次要角色' }
function charRoleLabel(role) { return CHAR_ROLE_LABEL[role] || role || '' }
// ── 核心函数 ──────────────────────────────────────────
async function onGenerateCharacters() {
if (!store.dramaId) return
const epId = currentEpisodeId.value
if (!epId) {
ElMessage.warning('请先选择集次')
return
}
const meta = buildExtractTaskMeta(store, dramaId.value, epId, GEN_RESOURCE.EXTRACT_CHARACTERS, '提取角色')
genStore.markRunning(meta)
try {
const outline =
(store.scriptContent || '').toString().trim() || undefined
const res = await generationAPI.generateCharacters(store.dramaId, {
episode_id: epId,
outline: outline || undefined
})
const taskId = res?.task_id
if (taskId) {
await pollTask(taskId, () => loadDrama(), meta)
ElMessage.success('角色生成完成')
} else {
await loadDrama()
}
} catch (e) {
ElMessage.error(e.message || '生成失败')
} finally {
genStore.markDone(meta)
}
}
function openAddCharacter() {
editCharacterForm.value = {
name: '',
role: '',
appearance: '',
personality: '',
description: '',
polished_prompt: ''
}
showEditCharacter.value = true
}
function stopCharacterPromptPoll() {
if (editCharacterPollTimer) {
clearInterval(editCharacterPollTimer)
editCharacterPollTimer = null
}
}
function editCharacter(char) {
stopCharacterPromptPoll()
editCharacterForm.value = {
id: char.id,
name: char.name || '',
role: char.role || '',
appearance: char.appearance || '',
personality: char.personality || '',
description: char.description || '',
polished_prompt: char.polished_prompt || '',
image_url: char.image_url || '',
local_path: char.local_path || '',
ref_image: char.ref_image || '',
identity_anchors: char.identity_anchors || '',
stages: char.stages ? (typeof char.stages === 'string' ? char.stages : JSON.stringify(char.stages, null, 2)) : '',
}
showEditCharacter.value = true
if (!char.polished_prompt && char.id && (char.appearance || char.description)) {
editCharacterPromptGenerating.value = true
let elapsed = 0
editCharacterPollTimer = setInterval(async () => {
elapsed += 3
try {
const res = await characterAPI.get(char.id)
const prompt = res?.character?.polished_prompt
if (prompt) {
if (editCharacterForm.value?.id === char.id) {
editCharacterForm.value.polished_prompt = prompt
}
stopCharacterPromptPoll()
editCharacterPromptGenerating.value = false
} else if (elapsed >= 60) {
stopCharacterPromptPoll()
editCharacterPromptGenerating.value = false
}
} catch (_) {
stopCharacterPromptPoll()
editCharacterPromptGenerating.value = false
}
}, 3000)
}
}
async function saveCharRefImageIfAny(characterId) {
const refImg = addCharRefImage.value
if (!refImg || !characterId) return
try {
const file = dataUrlToFile(refImg.dataUrl, refImg.filename || 'reference.png')
const uploadRes = await uploadAPI.uploadImage(file, { dramaId: dramaId.value })
const refPath = uploadRes.local_path || uploadRes.url || ''
await characterAPI.putRefImage(characterId, refPath)
} catch (e) {
console.warn('[saveCharRefImage] 保存参考图失败:', e.message)
}
}
async function submitEditCharacter() {
const form = editCharacterForm.value
if (!form?.name?.trim() || !store.dramaId) return
editCharacterSaving.value = true
try {
if (form.id) {
await characterAPI.update(form.id, {
name: form.name.trim(),
role: form.role || undefined,
appearance: form.appearance || undefined,
personality: form.personality || undefined,
description: form.description || undefined,
polished_prompt: form.polished_prompt || undefined,
stages: form.stages ? form.stages.trim() || undefined : undefined
})
await saveCharRefImageIfAny(form.id)
ElMessage.success('角色已保存')
} else {
const existing = (store.drama?.characters || []).map((c) => ({
id: c.id,
name: c.name || '',
role: c.role || undefined,
description: c.description || undefined,
personality: c.personality || undefined,
appearance: c.appearance || undefined,
image_url: c.image_url || undefined,
local_path: c.local_path || undefined
}))
await dramaAPI.saveCharacters(store.dramaId, {
characters: [...existing, {
name: form.name.trim(),
role: form.role || undefined,
appearance: form.appearance || undefined,
personality: form.personality || undefined,
description: form.description || undefined
}],
episode_id: currentEpisodeId.value ?? undefined
})
await loadDrama()
if (addCharRefImage.value) {
const newChar = (store.drama?.characters || []).find(c => c.name === form.name.trim())
if (newChar?.id) await saveCharRefImageIfAny(newChar.id)
}
ElMessage.success('角色已添加')
}
await loadDrama()
showEditCharacter.value = false
} catch (e) {
ElMessage.error(e.message || (form.id ? '保存失败' : '添加失败'))
} finally {
editCharacterSaving.value = false
}
}
async function doGenerateCharacterPrompt() {
const form = editCharacterForm.value
if (!form?.id) return
editCharacterPromptGenerating.value = true
try {
const res = await characterAPI.generatePrompt(form.id)
if (res?.polished_prompt) {
form.polished_prompt = res.polished_prompt
ElMessage.success('提示词已生成')
await loadDrama()
}
} catch (e) {
ElMessage.error(e.message || '生成提示词失败')
} finally {
editCharacterPromptGenerating.value = false
}
}
async function doExtractCharFromImage() {
const form = editCharacterForm.value
if (!form?.id) return
extractingCharAppearance.value = true
try {
const res = await characterAPI.extractFromImage(form.id)
if (res?.appearance) {
form.appearance = res.appearance
ElMessage.success('已从图片提取外貌描述')
}
} catch (e) {
ElMessage.error(e.message || '提取失败,请检查角色是否已上传参考图片')
} finally {
extractingCharAppearance.value = false
}
}
async function clearCharRefImage() {
const form = editCharacterForm.value
if (!form?.id) return
try {
await characterAPI.putRefImage(form.id, null)
form.ref_image = ''
ElMessage.success('参考图已移除')
} catch (e) {
ElMessage.error('移除失败')
}
}
function onCloseCharDialog() {
showEditCharacter.value = false
stopCharacterPromptPoll()
editCharacterPromptGenerating.value = false
addCharRefImage.value = null
}
async function onDeleteCharacter(char) {
try {
await ElMessageBox.confirm(
`确定要删除角色「${(char.name || '未命名').slice(0, 20)}」吗?此操作不可恢复。`,
'删除确认',
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }
)
await characterAPI.delete(char.id)
await loadDrama()
ElMessage.success('角色已删除')
} catch (e) {
if (e === 'cancel') return
ElMessage.error(e.message || '删除失败')
}
}
async function onGenerateCharacterImage(char) {
char.errorMsg = ''
char.error_msg = ''
const meta = buildCharImageMeta(char)
generatingCharIds.add(char.id)
genStore.markRunning(meta)
try {
const res = await characterAPI.generateImage(char.id, undefined, getSelectedStyle())
const taskId = res?.image_generation?.task_id ?? res?.task_id
if (taskId) {
const pollRes = await pollTask(taskId, () => loadDrama(), meta)
if (pollRes?.status === 'failed') {
char.errorMsg = pollRes.error || '生成失败'
} else {
ElMessage.success('角色图片已生成')
}
} else {
await loadDrama()
await pollUntilResourceHasImage(() => {
const list = store.drama?.characters ?? store.currentEpisode?.characters ?? []
const c = list.find((x) => Number(x.id) === Number(char.id))
return !!(c && (c.image_url || c.local_path))
})
ElMessage.success('角色图片已生成')
}
} catch (e) {
console.error(e)
char.errorMsg = e.message || '生成失败'
ElMessage.error(e.message || '提交失败')
} finally {
generatingCharIds.delete(char.id)
genStore.markDone(meta)
}
}
// ── 角色库函数 ────────────────────────────────────────
async function loadCharLibraryList() {
charLibraryLoading.value = true
try {
const res = await characterLibraryAPI.list({
drama_id: dramaId.value,
page: charLibraryPage.value,
page_size: charLibraryPageSize.value,
keyword: charLibraryKeyword.value || undefined
})
charLibraryList.value = res?.items ?? []
const pagination = res?.pagination ?? {}
charLibraryTotal.value = pagination.total ?? 0
if (pagination.page != null) charLibraryPage.value = pagination.page
if (pagination.page_size != null) charLibraryPageSize.value = pagination.page_size
} catch (e) {
charLibraryList.value = []
} finally {
charLibraryLoading.value = false
}
}
function debouncedLoadCharLibrary() {
if (charLibraryKeywordTimer) clearTimeout(charLibraryKeywordTimer)
charLibraryKeywordTimer = setTimeout(() => {
charLibraryPage.value = 1
loadCharLibraryList()
}, 300)
}
async function loadDramaAllCharList() {
if (!dramaId.value) {
dramaAllCharList.value = []
dramaAllCharTotal.value = 0
return
}
dramaAllCharLoading.value = true
try {
const res = await dramaAPI.getCharacters(dramaId.value)
let list = Array.isArray(res) ? res : (res?.characters ?? res?.items ?? [])
const kw = (dramaAllCharKeyword.value || '').trim().toLowerCase()
if (kw) {
list = list.filter((c) => {
const name = (c.name || '').toLowerCase()
const desc = (c.description || '').toLowerCase()
const app = (c.appearance || '').toLowerCase()
return name.includes(kw) || desc.includes(kw) || app.includes(kw)
})
}
dramaAllCharTotal.value = list.length
const start = (dramaAllCharPage.value - 1) * dramaAllCharPageSize.value
dramaAllCharList.value = list.slice(start, start + dramaAllCharPageSize.value)
} catch {
dramaAllCharList.value = []
dramaAllCharTotal.value = 0
} finally {
dramaAllCharLoading.value = false
}
}
function debouncedLoadDramaAllCharList() {
if (dramaAllCharKeywordTimer) clearTimeout(dramaAllCharKeywordTimer)
dramaAllCharKeywordTimer = setTimeout(() => {
dramaAllCharPage.value = 1
loadDramaAllCharList()
}, 300)
}
function onCharLibraryDialogOpen() {
if (charLibraryTab.value === 'library') loadCharLibraryList()
else if (charLibraryTab.value === 'drama') loadDramaAllCharList()
}
function onCharLibraryTabChange() {
if (charLibraryTab.value === 'library') {
charLibraryPage.value = 1
loadCharLibraryList()
} else if (charLibraryTab.value === 'drama') {
dramaAllCharPage.value = 1
loadDramaAllCharList()
}
}
function charAddToEpisodeLoadingKey(scope, id) {
return `${scope}-${id}`
}
function isCharAddToEpisodeLoading(scope, id) {
return addingCharFromLibraryId.value === charAddToEpisodeLoadingKey(scope, id)
}
function openEditCharLibrary(item) {
editCharLibraryForm.value = {
id: item.id,
name: item.name ?? '',
category: item.category ?? '',
description: item.description ?? '',
tags: item.tags ?? ''
}
showEditCharLibrary.value = true
}
async function submitEditCharLibrary() {
if (!editCharLibraryForm.value?.id) return
editCharLibrarySaving.value = true
try {
await characterLibraryAPI.update(editCharLibraryForm.value.id, {
name: editCharLibraryForm.value.name,
category: editCharLibraryForm.value.category || null,
description: editCharLibraryForm.value.description || null,
tags: editCharLibraryForm.value.tags || null
})
ElMessage.success('已保存')
showEditCharLibrary.value = false
loadCharLibraryList()
} catch (e) {
ElMessage.error(e.message || '保存失败')
} finally {
editCharLibrarySaving.value = false
}
}
async function onDeleteCharLibrary(item) {
try {
await ElMessageBox.confirm(
`确定删除公共角色「${(item.name || '未命名').slice(0, 20)}」吗?`,
'删除确认',
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }
)
await characterLibraryAPI.delete(item.id)
ElMessage.success('已删除')
loadCharLibraryList()
} catch (e) {
if (e === 'cancel') return
ElMessage.error(e.message || '删除失败')
}
}
async function onAddCharacterToLibrary(char) {
if (!hasAssetImage(char)) { ElMessage.warning('请先为该角色生成或上传图片'); return }
addingCharToLibraryId.value = char.id
try {
await characterAPI.addToLibrary(char.id, {})
ElMessage.success('已加入本剧角色库')
if (showCharLibrary.value) loadCharLibraryList()
} catch (e) {
ElMessage.error(e.message || '加入失败')
} finally {
addingCharToLibraryId.value = null
}
}
async function onAddCharacterToMaterialLibrary(char) {
if (!hasAssetImage(char)) { ElMessage.warning('请先为该角色生成或上传图片'); return }
addingCharToMaterialId.value = char.id
try {
await characterAPI.addToMaterialLibrary(char.id)
ElMessage.success('已加入全局素材库')
} catch (e) {
ElMessage.error(e.message || '加入失败')
} finally {
addingCharToMaterialId.value = null
}
}
async function addCharToEpisode(item, scope) {
if (!store.dramaId) return
if (!currentEpisodeId.value) {
ElMessage.warning('请先选择本集')
return
}
const loadingKey = charAddToEpisodeLoadingKey(scope, item.id)
addingCharFromLibraryId.value = loadingKey
try {
const existing = (store.characters || []).map((c) => ({
id: c.id,
name: c.name || '',
role: c.role || undefined,
appearance: c.appearance || undefined,
personality: c.personality || undefined,
description: c.description || undefined,
image_url: c.image_url || undefined,
local_path: c.local_path || undefined,
}))
const newCharacters = [...existing]
const existingChar = newCharacters.find((c) => c.name === (item.name || '未命名'))
if (existingChar) {
existingChar.description = item.description || existingChar.description
existingChar.appearance = item.appearance || existingChar.appearance
existingChar.image_url = item.image_url || existingChar.image_url
existingChar.local_path = item.local_path || existingChar.local_path
if (item.role && !existingChar.role) existingChar.role = item.role
} else {
newCharacters.push({
name: item.name || '未命名',
role: item.role || undefined,
description: item.description || undefined,
appearance: item.appearance || undefined,
personality: item.personality || undefined,
image_url: item.image_url || undefined,
local_path: item.local_path || undefined,
})
}
await dramaAPI.saveCharacters(store.dramaId, {
characters: newCharacters,
episode_id: currentEpisodeId.value ?? undefined,
})
await loadDrama()
ElMessage.success(`${item.name || '角色'}」已加入本集`)
} catch (e) {
ElMessage.error(e.message || '加入失败')
} finally {
addingCharFromLibraryId.value = null
}
}
function onAddCharFromLibrary(item) {
return addCharToEpisode(item, 'library')
}
function onAddDramaCharToEpisode(item) {
return addCharToEpisode(item, 'drama')
}
async function extractIdentityAnchors() {
const form = editCharacterForm.value
if (!form?.id) return
if (!form.appearance) {
ElMessage.warning('请先填写角色外貌描述')
return
}
extractingAnchors.value = true
try {
await characterAPI.extractAnchors(form.id)
ElMessage.success('视觉锚点提炼已启动,请稍后查看')
// 轮询等待锚点写入
let elapsed = 0
const timer = setInterval(async () => {
elapsed += 3
try {
const res = await characterAPI.get(form.id)
const anchors = res?.character?.identity_anchors
if (anchors && editCharacterForm.value?.id === form.id) {
editCharacterForm.value.identity_anchors = anchors
clearInterval(timer)
extractingAnchors.value = false
} else if (elapsed >= 60) {
clearInterval(timer)
extractingAnchors.value = false
}
} catch (_) {
clearInterval(timer)
extractingAnchors.value = false
}
}, 3000)
} catch (e) {
ElMessage.error(e.message || '提炼失败')
extractingAnchors.value = false
}
}
async function onSd2CertifyCharacter(char) {
if (!char?.id) return
if (!hasAssetImage(char)) {
ElMessage.warning('请先为该角色生成或上传图片')
return
}
sd2CertifyingId.value = char.id
try {
await characterAPI.sd2Certify(char.id)
await loadDrama()
ElMessage.success('SD2 认证请求已提交')
} catch (e) {
const msg = e?.message || ''
if (/已存在|已认证|already/i.test(msg)) {
try {
await characterAPI.sd2CertifyRefresh(char.id)
await loadDrama()
ElMessage.success('SD2 认证状态已刷新')
return
} catch (_) {
// fall through
}
}
ElMessage.error(msg || 'SD2 认证失败')
} finally {
sd2CertifyingId.value = null
}
}
async function onSd2CertifyRefresh(char) {
if (!char?.id) return
sd2CertifyingId.value = char.id
try {
await characterAPI.sd2CertifyRefresh(char.id)
await loadDrama()
ElMessage.success('SD2 认证状态已刷新')
} catch (e) {
ElMessage.error(e?.message || '刷新失败')
} finally {
sd2CertifyingId.value = null
}
}
function sd2ActionLabel(char) {
const status = String(char?.seedance2_asset?.status || '').toLowerCase()
if (status === 'active') return '查看认证'
if (status === 'processing') return '刷新认证'
if (status === 'failed') return '重新认证'
return 'sd2认证'
}
async function onSd2PrimaryAction(char) {
const status = String(char?.seedance2_asset?.status || '').toLowerCase()
if (status === 'active') {
openCharSd2CertDialog(char)
return
}
if (status === 'processing') {
await onSd2CertifyRefresh(char)
return
}
await onSd2CertifyCharacter(char)
}
function openCharSd2CertDialog(char) {
charSd2CertPayload.value = char?.seedance2_asset ? { ...char.seedance2_asset } : null
showCharSd2Cert.value = true
}
function sd2VoiceActionLabel(char) {
const status = String(char?.seedance2_voice_asset?.status || '').toLowerCase()
if (status === 'active') return '音色参考'
if (status === 'processing') return '刷新音色'
if (status === 'failed') return '重新上传'
return '上传音色'
}
async function onSd2VoicePrimaryAction(char) {
const status = String(char?.seedance2_voice_asset?.status || '').toLowerCase()
if (status === 'active') {
ElMessage.info('音色参考已设置,将在 Seedance 2.0 模型中使用')
return
}
if (status === 'processing' || status === 'stale') {
await onSd2VoiceRefresh(char)
return
}
// 触发文件选择上传
await triggerSd2VoiceUpload(char)
}
// 专门用于“更换”:无论当前是否 active,都直接触发文件选择上传(覆盖)
async function onSd2VoiceReplace(char) {
await triggerSd2VoiceUpload(char)
}
async function onSd2VoiceRefresh(char) {
if (!char?.id) return
sd2VoiceUploadingId.value = char.id
try {
const res = await characterAPI.sd2VoiceRefresh(char.id)
await loadDrama()
ElMessage.success(res?.data?.message || '音色状态已刷新')
} catch (e) {
ElMessage.error(e?.message || '刷新失败')
} finally {
sd2VoiceUploadingId.value = null
}
}
async function triggerSd2VoiceUpload(char) {
if (!char?.id) return
// 创建隐藏的 file input
const input = document.createElement('input')
input.type = 'file'
input.accept = 'audio/*'
input.onchange = async () => {
const file = input.files && input.files[0]
if (!file) return
sd2VoiceUploadingId.value = char.id
try {
const res = await characterAPI.sd2VoiceUpload(char.id, file)
ElMessage.success('Seedance 2.0 音色参考已上传')
// 强制重新加载整个剧本数据,确保 seedance2_voice_asset 被正确解析并更新到 store
await loadDrama()
} catch (e) {
ElMessage.error(e?.message || '音色上传失败')
} finally {
sd2VoiceUploadingId.value = null
}
}
input.click()
}
// 播放 Seedance 2.0 音色参考(仅 active 状态)
function playSd2Voice(char) {
const url = char?.seedance2_voice_asset?.url
if (!url) {
ElMessage.warning('该角色暂无音色参考音频')
return
}
try {
// 统一使用相对 /static/...(与图片 assetImageUrl 一致),由当前页面 origin + Vite/后端代理或静态服务处理
const audio = new Audio(url)
audio.onerror = () => {
// 常见原因:文件不在 static 根目录下(后端写盘路径与 express.static(storageRoot) 不一致)、404、格式不支持
ElMessage.error('音频播放失败:文件可能不存在或路径不匹配,请尝试重新上传该音色参考')
}
audio.play().catch((err) => {
ElMessage.error('音频播放失败,请检查文件或稍后重试')
})
} catch (e) {
ElMessage.error('无法播放音频')
}
}
return {
// 弹窗状态
showEditCharacter,
editCharacterForm,
editCharacterSaving,
editCharacterPromptGenerating,
extractingCharAppearance,
extractingAnchors,
addCharRefImage,
addCharRefFileInput,
// 生成状态
charactersGenerating,
generatingCharIds,
sd2CertifyingId,
showCharSd2Cert,
charSd2CertPayload,
sd2VoiceUploadingId,
// 库状态
showCharLibrary,
charLibraryList,
charLibraryLoading,
charLibraryPage,
charLibraryPageSize,
charLibraryTotal,
charLibraryKeyword,
charLibraryTab,
dramaAllCharList,
dramaAllCharLoading,
dramaAllCharPage,
dramaAllCharPageSize,
dramaAllCharTotal,
dramaAllCharKeyword,
showEditCharLibrary,
editCharLibraryForm,
editCharLibrarySaving,
addingCharToLibraryId,
addingCharToMaterialId,
addingCharFromLibraryId,
// 函数
charRoleLabel,
onGenerateCharacters,
openAddCharacter,
stopCharacterPromptPoll,
editCharacter,
saveCharRefImageIfAny,
submitEditCharacter,
doGenerateCharacterPrompt,
doExtractCharFromImage,
extractIdentityAnchors,
clearCharRefImage,
onCloseCharDialog,
onDeleteCharacter,
onGenerateCharacterImage,
onSd2CertifyCharacter,
onSd2CertifyRefresh,
sd2ActionLabel,
onSd2PrimaryAction,
openCharSd2CertDialog,
onSd2VoicePrimaryAction,
onSd2VoiceReplace,
sd2VoiceActionLabel,
playSd2Voice,
loadCharLibraryList,
debouncedLoadCharLibrary,
loadDramaAllCharList,
debouncedLoadDramaAllCharList,
onCharLibraryDialogOpen,
onCharLibraryTabChange,
isCharAddToEpisodeLoading,
openEditCharLibrary,
submitEditCharLibrary,
onDeleteCharLibrary,
onAddCharacterToLibrary,
onAddCharacterToMaterialLibrary,
onAddCharFromLibrary,
onAddDramaCharToEpisode,
}
}