release: opensource snapshot 2026-02-27 19:25:00

This commit is contained in:
saturn
2026-02-27 19:25:00 +08:00
commit 5de9622c8b
1055 changed files with 164772 additions and 0 deletions

837
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,837 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(uuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@index([userId])
@@map("account")
}
model CharacterAppearance {
id String @id @default(uuid())
characterId String
appearanceIndex Int
changeReason String
description String? @db.Text
descriptions String? @db.Text
imageUrl String? @db.Text
imageUrls String? @db.Text
selectedIndex Int?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
previousImageUrl String? @db.Text
previousImageUrls String? @db.Text
previousDescription String? @db.Text // 上一次的描述词(用于撤回)
previousDescriptions String? @db.Text // 上一次的描述词数组(用于撤回)
imageMediaId String?
imageMedia MediaObject? @relation("CharacterAppearanceImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
character NovelPromotionCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade)
@@unique([characterId, appearanceIndex])
@@index([characterId])
@@index([imageMediaId])
@@map("character_appearances")
}
model LocationImage {
id String @id @default(uuid())
locationId String
imageIndex Int
description String? @db.Text
imageUrl String? @db.Text
isSelected Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
previousImageUrl String? @db.Text
previousDescription String? @db.Text // 上一次的描述词(用于撤回)
imageMediaId String?
imageMedia MediaObject? @relation("LocationImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
location NovelPromotionLocation @relation("LocationImages", fields: [locationId], references: [id], onDelete: Cascade)
selectedByLocations NovelPromotionLocation[] @relation("SelectedLocationImage")
@@unique([locationId, imageIndex])
@@index([locationId])
@@index([imageMediaId])
@@map("location_images")
}
model NovelPromotionCharacter {
id String @id @default(uuid())
novelPromotionProjectId String
name String
aliases String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
customVoiceUrl String? @db.Text
customVoiceMediaId String?
customVoiceMedia MediaObject? @relation("NovelPromotionCharacterVoiceMedia", fields: [customVoiceMediaId], references: [id], onDelete: SetNull)
voiceId String?
voiceType String?
profileData String? @db.Text
profileConfirmed Boolean @default(false)
introduction String? @db.Text // 角色介绍(身份、关系、称呼映射,如"我"对应此角色)
sourceGlobalCharacterId String? // 🆕 来源全局角色ID复制时记录
appearances CharacterAppearance[]
novelPromotionProject NovelPromotionProject @relation(fields: [novelPromotionProjectId], references: [id], onDelete: Cascade)
@@index([novelPromotionProjectId])
@@index([customVoiceMediaId])
@@map("novel_promotion_characters")
}
model NovelPromotionLocation {
id String @id @default(uuid())
novelPromotionProjectId String
name String
summary String? @db.Text // 场景简要描述(用途/人物关联)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
sourceGlobalLocationId String? // 🆕 来源全局场景ID复制时记录
selectedImageId String?
selectedImage LocationImage? @relation("SelectedLocationImage", fields: [selectedImageId], references: [id], onDelete: SetNull)
images LocationImage[] @relation("LocationImages")
novelPromotionProject NovelPromotionProject @relation(fields: [novelPromotionProjectId], references: [id], onDelete: Cascade)
@@index([novelPromotionProjectId])
@@map("novel_promotion_locations")
}
model NovelPromotionEpisode {
id String @id @default(uuid())
novelPromotionProjectId String
episodeNumber Int
name String
description String? @db.Text
novelText String? @db.Text
audioUrl String? @db.Text
audioMediaId String?
audioMedia MediaObject? @relation("NovelPromotionEpisodeAudioMedia", fields: [audioMediaId], references: [id], onDelete: SetNull)
srtContent String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
speakerVoices String? @db.Text
clips NovelPromotionClip[]
novelPromotionProject NovelPromotionProject @relation(fields: [novelPromotionProjectId], references: [id], onDelete: Cascade)
shots NovelPromotionShot[]
storyboards NovelPromotionStoryboard[]
voiceLines NovelPromotionVoiceLine[]
editorProject VideoEditorProject?
@@unique([novelPromotionProjectId, episodeNumber])
@@index([novelPromotionProjectId])
@@index([audioMediaId])
@@map("novel_promotion_episodes")
}
// 视频编辑器项目 - 存储剪辑数据
model VideoEditorProject {
id String @id @default(uuid())
episodeId String @unique
projectData String @db.Text // JSON 存储编辑项目数据
renderStatus String? // pending | rendering | completed | failed
renderTaskId String?
outputUrl String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
episode NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
@@map("video_editor_projects")
}
model NovelPromotionClip {
id String @id @default(uuid())
episodeId String
start Int?
end Int?
duration Int?
summary String @db.Text
location String? @db.Text
content String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
characters String? @db.Text
endText String? @db.Text
shotCount Int?
startText String? @db.Text
screenplay String? @db.Text
episode NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
shots NovelPromotionShot[]
storyboard NovelPromotionStoryboard?
@@index([episodeId])
@@map("novel_promotion_clips")
}
model NovelPromotionPanel {
id String @id @default(uuid())
storyboardId String
panelIndex Int
panelNumber Int?
shotType String? @db.Text
cameraMove String? @db.Text
description String? @db.Text
location String? @db.Text
characters String? @db.Text
srtSegment String? @db.Text
srtStart Float?
srtEnd Float?
duration Float?
imagePrompt String? @db.Text
imageUrl String? @db.Text
imageMediaId String?
imageMedia MediaObject? @relation("NovelPromotionPanelImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
imageHistory String? @db.Text
videoPrompt String? @db.Text
firstLastFramePrompt String? @db.Text
videoUrl String? @db.Text
videoGenerationMode String? @db.Text // 视频生成方式normal | firstlastframe
videoMediaId String?
videoMedia MediaObject? @relation("NovelPromotionPanelVideoMedia", fields: [videoMediaId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
sceneType String?
candidateImages String? @db.Text
linkedToNextPanel Boolean @default(false)
lipSyncTaskId String?
lipSyncVideoUrl String?
lipSyncVideoMediaId String?
lipSyncVideoMedia MediaObject? @relation("NovelPromotionPanelLipSyncVideoMedia", fields: [lipSyncVideoMediaId], references: [id], onDelete: SetNull)
sketchImageUrl String? @db.Text
sketchImageMediaId String?
sketchImageMedia MediaObject? @relation("NovelPromotionPanelSketchMedia", fields: [sketchImageMediaId], references: [id], onDelete: SetNull)
photographyRules String? @db.Text
actingNotes String? @db.Text // 演技指导数据 JSON
previousImageUrl String? @db.Text
previousImageMediaId String?
previousImageMedia MediaObject? @relation("NovelPromotionPanelPreviousImageMedia", fields: [previousImageMediaId], references: [id], onDelete: SetNull)
storyboard NovelPromotionStoryboard @relation(fields: [storyboardId], references: [id], onDelete: Cascade)
matchedVoiceLines NovelPromotionVoiceLine[]
@@unique([storyboardId, panelIndex])
@@index([storyboardId])
@@index([imageMediaId])
@@index([videoMediaId])
@@index([lipSyncVideoMediaId])
@@index([sketchImageMediaId])
@@index([previousImageMediaId])
@@map("novel_promotion_panels")
}
model NovelPromotionProject {
id String @id @default(uuid())
projectId String @unique
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
analysisModel String? // 用户配置的分析模型nullable必须配置后才能使用
imageModel String? // 用户配置的图片模型
videoModel String? // 用户配置的视频模型
videoRatio String @default("9:16")
ttsRate String @default("+50%")
globalAssetText String? @db.Text
artStyle String @default("american-comic")
artStylePrompt String? @db.Text
characterModel String? // 用户配置的角色图片模型
locationModel String? // 用户配置的场景图片模型
storyboardModel String? // 用户配置的分镜图片模型
editModel String? // 用户配置的修图/编辑模型
videoResolution String @default("720p")
capabilityOverrides String? @db.Text
workflowMode String @default("srt")
lastEpisodeId String?
imageResolution String @default("2K")
importStatus String?
characters NovelPromotionCharacter[]
episodes NovelPromotionEpisode[]
locations NovelPromotionLocation[]
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@map("novel_promotion_projects")
}
model NovelPromotionShot {
id String @id @default(uuid())
episodeId String
clipId String?
shotId String
srtStart Int
srtEnd Int
srtDuration Float
sequence String? @db.Text
locations String? @db.Text
characters String? @db.Text
plot String? @db.Text
imagePrompt String? @db.Text
scale String? @db.Text
module String? @db.Text
focus String? @db.Text
zhSummarize String? @db.Text
imageUrl String? @db.Text
imageMediaId String?
imageMedia MediaObject? @relation("NovelPromotionShotImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
pov String? @db.Text
clip NovelPromotionClip? @relation(fields: [clipId], references: [id], onDelete: Cascade)
episode NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
@@index([clipId])
@@index([episodeId])
@@index([shotId])
@@index([imageMediaId])
@@map("novel_promotion_shots")
}
model NovelPromotionStoryboard {
id String @id @default(uuid())
episodeId String
clipId String @unique
storyboardImageUrl String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
panelCount Int @default(9)
storyboardTextJson String? @db.Text
imageHistory String? @db.Text
candidateImages String? @db.Text
lastError String?
photographyPlan String? @db.Text
panels NovelPromotionPanel[]
clip NovelPromotionClip @relation(fields: [clipId], references: [id], onDelete: Cascade)
episode NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
supplementaryPanels SupplementaryPanel[]
@@index([clipId])
@@index([episodeId])
@@map("novel_promotion_storyboards")
}
model SupplementaryPanel {
id String @id @default(uuid())
storyboardId String
sourceType String
sourcePanelId String?
description String? @db.Text
imagePrompt String? @db.Text
imageUrl String? @db.Text
imageMediaId String?
imageMedia MediaObject? @relation("SupplementaryPanelImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
characters String? @db.Text
location String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
storyboard NovelPromotionStoryboard @relation(fields: [storyboardId], references: [id], onDelete: Cascade)
@@index([storyboardId])
@@index([imageMediaId])
@@map("supplementary_panels")
}
model Project {
id String @id @default(uuid())
name String
description String? @db.Text
mode String @default("novel-promotion")
userId String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
lastAccessedAt DateTime?
novelPromotionData NovelPromotionProject?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
usageCosts UsageCost[]
@@index([userId])
@@map("projects")
}
model Session {
id String @id @default(uuid())
sessionToken String @unique(map: "Session_sessionToken_key")
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@map("session")
}
model UsageCost {
id String @id @default(uuid())
projectId String
userId String
apiType String
model String
action String
quantity Int
unit String
cost Decimal @db.Decimal(18, 6)
metadata String? @db.Text
createdAt DateTime @default(now())
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([apiType])
@@index([createdAt])
@@index([projectId])
@@index([userId])
@@map("usage_costs")
}
model User {
id String @id @default(uuid())
name String @unique(map: "User_name_key")
email String?
emailVerified DateTime?
image String?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
accounts Account[]
projects Project[]
sessions Session[]
usageCosts UsageCost[]
balance UserBalance?
preferences UserPreference?
// 资产中心
globalAssetFolders GlobalAssetFolder[]
globalCharacters GlobalCharacter[]
globalLocations GlobalLocation[]
globalVoices GlobalVoice[]
tasks Task[]
taskEvents TaskEvent[]
@@map("user")
}
model UserPreference {
id String @id @default(uuid())
userId String @unique
analysisModel String? // 用户配置的分析模型nullable必须配置后才能使用
characterModel String? // 用户配置的角色图片模型
locationModel String? // 用户配置的场景图片模型
storyboardModel String? // 用户配置的分镜图片模型
editModel String? // 用户配置的修图模型
videoModel String? // 用户配置的视频模型
lipSyncModel String? // 用户配置的口型同步模型
videoRatio String @default("9:16")
videoResolution String @default("720p")
artStyle String @default("american-comic")
ttsRate String @default("+50%")
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
imageResolution String @default("2K")
capabilityDefaults String? @db.Text
// API Key 配置(极简版)
llmBaseUrl String? @default("https://openrouter.ai/api/v1")
llmApiKey String? @db.Text // 加密存储
falApiKey String? @db.Text // FAL图片+视频+语音)
googleAiKey String? @db.Text // Google AIGemini 图片)
arkApiKey String? @db.Text // 火山引擎Seedream+Seedance
qwenApiKey String? @db.Text // 阿里百炼(声音设计)
// 自定义模型列表 + 价格JSON
customModels String? @db.Text
// 自定义 OpenAI 兼容提供商列表JSON包含加密的 API Key
customProviders String? @db.Text
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("user_preferences")
}
model VerificationToken {
identifier String
token String @unique(map: "VerificationToken_token_key")
expires DateTime
@@unique([identifier, token])
@@map("verificationtoken")
}
model NovelPromotionVoiceLine {
id String @id @default(uuid())
episodeId String
lineIndex Int
speaker String
content String @db.Text
voicePresetId String?
audioUrl String? @db.Text
audioMediaId String?
audioMedia MediaObject? @relation("NovelPromotionVoiceLineAudioMedia", fields: [audioMediaId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
emotionPrompt String? @db.Text
emotionStrength Float? @default(0.4)
matchedPanelIndex Int?
matchedStoryboardId String?
audioDuration Int?
matchedPanelId String?
episode NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
matchedPanel NovelPromotionPanel? @relation(fields: [matchedPanelId], references: [id])
@@unique([episodeId, lineIndex])
@@index([episodeId])
@@index([matchedPanelId])
@@index([audioMediaId])
@@map("novel_promotion_voice_lines")
}
model VoicePreset {
id String @id @default(uuid())
name String
audioUrl String @db.Text
audioMediaId String?
audioMedia MediaObject? @relation("VoicePresetAudioMedia", fields: [audioMediaId], references: [id], onDelete: SetNull)
description String? @db.Text
gender String?
isSystem Boolean @default(true)
createdAt DateTime @default(now())
@@index([audioMediaId])
@@map("voice_presets")
}
model UserBalance {
id String @id @default(uuid())
userId String @unique
balance Decimal @default(0) @db.Decimal(18, 6)
frozenAmount Decimal @default(0) @db.Decimal(18, 6)
totalSpent Decimal @default(0) @db.Decimal(18, 6)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("user_balances")
}
model BalanceFreeze {
id String @id @default(uuid())
userId String
amount Decimal @db.Decimal(18, 6)
status String @default("pending")
source String? @db.VarChar(64)
taskId String?
requestId String?
idempotencyKey String? @unique
metadata String? @db.Text
expiresAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@index([userId])
@@index([status])
@@index([taskId])
@@map("balance_freezes")
}
model BalanceTransaction {
id String @id @default(uuid())
userId String
type String
amount Decimal @db.Decimal(18, 6)
balanceAfter Decimal @db.Decimal(18, 6)
description String? @db.Text
relatedId String?
freezeId String?
operatorId String? @db.VarChar(64)
externalOrderId String? @db.VarChar(128)
idempotencyKey String? @db.VarChar(128)
projectId String? @db.VarChar(128) // 关联项目 ID用于流水展示项目名
episodeId String? @db.VarChar(128) // 关联集数 ID用于流水展示集数
taskType String? @db.VarChar(64) // 任务类型 key与 action 一致),用于前端 i18n
billingMeta String? @db.Text // 计费详情 JSON: { quantity, unit, model, resolution, duration, tokens... }
createdAt DateTime @default(now())
@@index([userId])
@@index([type])
@@index([createdAt])
@@index([freezeId])
@@index([externalOrderId])
@@index([projectId])
@@unique([userId, type, idempotencyKey])
@@map("balance_transactions")
}
model Task {
id String @id @default(uuid())
userId String
projectId String
episodeId String?
type String
targetType String
targetId String
status String @default("queued")
progress Int @default(0)
attempt Int @default(0)
maxAttempts Int @default(5)
priority Int @default(0)
dedupeKey String? @unique
externalId String?
payload Json?
result Json?
errorCode String?
errorMessage String? @db.Text
billingInfo Json?
billedAt DateTime?
queuedAt DateTime @default(now())
startedAt DateTime?
finishedAt DateTime?
heartbeatAt DateTime?
enqueuedAt DateTime?
enqueueAttempts Int @default(0)
lastEnqueueError String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
events TaskEvent[]
@@index([status])
@@index([type])
@@index([targetType, targetId])
@@index([projectId])
@@index([userId])
@@index([heartbeatAt])
@@map("tasks")
}
model TaskEvent {
id Int @id @default(autoincrement())
taskId String
projectId String
userId String
eventType String
payload Json?
createdAt DateTime @default(now())
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([projectId, id])
@@index([taskId])
@@index([userId])
@@map("task_events")
}
// ==================== 资产中心 ====================
// 资产文件夹(一层,不支持嵌套)
model GlobalAssetFolder {
id String @id @default(uuid())
userId String
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
characters GlobalCharacter[]
locations GlobalLocation[]
voices GlobalVoice[]
@@index([userId])
@@map("global_asset_folders")
}
// 全局角色(结构与 NovelPromotionCharacter 一致)
model GlobalCharacter {
id String @id @default(uuid())
userId String
folderId String?
name String
aliases String? @db.Text
profileData String? @db.Text
profileConfirmed Boolean @default(false)
voiceId String?
voiceType String?
customVoiceUrl String? @db.Text
customVoiceMediaId String?
customVoiceMedia MediaObject? @relation("GlobalCharacterVoiceMedia", fields: [customVoiceMediaId], references: [id], onDelete: SetNull)
globalVoiceId String? // 绑定的全局音色 ID
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
folder GlobalAssetFolder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
appearances GlobalCharacterAppearance[]
@@index([userId])
@@index([folderId])
@@index([customVoiceMediaId])
@@map("global_characters")
}
// 全局角色形象(结构与 CharacterAppearance 一致)
model GlobalCharacterAppearance {
id String @id @default(uuid())
characterId String
appearanceIndex Int
changeReason String @default("default")
description String? @db.Text
descriptions String? @db.Text
imageUrl String? @db.Text
imageMediaId String?
imageMedia MediaObject? @relation("GlobalCharacterAppearanceImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
imageUrls String? @db.Text
selectedIndex Int?
previousImageUrl String? @db.Text
previousImageMediaId String?
previousImageMedia MediaObject? @relation("GlobalCharacterAppearancePreviousImageMedia", fields: [previousImageMediaId], references: [id], onDelete: SetNull)
previousImageUrls String? @db.Text
previousDescription String? @db.Text // 上一次的描述词(用于撤回)
previousDescriptions String? @db.Text // 上一次的描述词数组(用于撤回)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
character GlobalCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade)
@@unique([characterId, appearanceIndex])
@@index([characterId])
@@index([imageMediaId])
@@index([previousImageMediaId])
@@map("global_character_appearances")
}
// 全局场景(结构与 NovelPromotionLocation 一致)
model GlobalLocation {
id String @id @default(uuid())
userId String
folderId String?
name String
summary String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
folder GlobalAssetFolder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
images GlobalLocationImage[]
@@index([userId])
@@index([folderId])
@@map("global_locations")
}
// 全局场景图片(结构与 LocationImage 一致)
model GlobalLocationImage {
id String @id @default(uuid())
locationId String
imageIndex Int
description String? @db.Text
imageUrl String? @db.Text
imageMediaId String?
imageMedia MediaObject? @relation("GlobalLocationImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
isSelected Boolean @default(false)
previousImageUrl String? @db.Text
previousImageMediaId String?
previousImageMedia MediaObject? @relation("GlobalLocationImagePreviousImageMedia", fields: [previousImageMediaId], references: [id], onDelete: SetNull)
previousDescription String? @db.Text // 上一次的描述词(用于撤回)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
location GlobalLocation @relation(fields: [locationId], references: [id], onDelete: Cascade)
@@unique([locationId, imageIndex])
@@index([locationId])
@@index([imageMediaId])
@@index([previousImageMediaId])
@@map("global_location_images")
}
// 全局音色库
model GlobalVoice {
id String @id @default(uuid())
userId String
folderId String?
name String // 音色名称
description String? @db.Text // 详细描述
voiceId String? // qwen-tts-vd 的 voice ID
voiceType String @default("qwen-designed") // qwen-designed | custom
customVoiceUrl String? @db.Text // 上传的音频 URL预览用
customVoiceMediaId String?
customVoiceMedia MediaObject? @relation("GlobalVoiceCustomVoiceMedia", fields: [customVoiceMediaId], references: [id], onDelete: SetNull)
voicePrompt String? @db.Text // AI 设计时的提示词
gender String? // male | female | neutral
language String @default("zh")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
folder GlobalAssetFolder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
@@index([userId])
@@index([folderId])
@@index([customVoiceMediaId])
@@map("global_voices")
}
model MediaObject {
id String @id @default(uuid())
publicId String @unique
storageKey String @unique @db.VarChar(512)
sha256 String?
mimeType String?
sizeBytes BigInt?
width Int?
height Int?
durationMs Int?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
characterAppearanceImages CharacterAppearance[] @relation("CharacterAppearanceImageMedia")
locationImages LocationImage[] @relation("LocationImageMedia")
novelPromotionCharacterVoices NovelPromotionCharacter[] @relation("NovelPromotionCharacterVoiceMedia")
novelPromotionEpisodeAudios NovelPromotionEpisode[] @relation("NovelPromotionEpisodeAudioMedia")
novelPromotionPanelImages NovelPromotionPanel[] @relation("NovelPromotionPanelImageMedia")
novelPromotionPanelVideos NovelPromotionPanel[] @relation("NovelPromotionPanelVideoMedia")
novelPromotionPanelLipSyncVideos NovelPromotionPanel[] @relation("NovelPromotionPanelLipSyncVideoMedia")
novelPromotionPanelSketchImages NovelPromotionPanel[] @relation("NovelPromotionPanelSketchMedia")
novelPromotionPanelPreviousImages NovelPromotionPanel[] @relation("NovelPromotionPanelPreviousImageMedia")
novelPromotionShotImages NovelPromotionShot[] @relation("NovelPromotionShotImageMedia")
supplementaryPanelImages SupplementaryPanel[] @relation("SupplementaryPanelImageMedia")
novelPromotionVoiceLineAudios NovelPromotionVoiceLine[] @relation("NovelPromotionVoiceLineAudioMedia")
voicePresetAudios VoicePreset[] @relation("VoicePresetAudioMedia")
globalCharacterVoices GlobalCharacter[] @relation("GlobalCharacterVoiceMedia")
globalCharacterAppearanceImages GlobalCharacterAppearance[] @relation("GlobalCharacterAppearanceImageMedia")
globalCharacterAppearancePreviousImgs GlobalCharacterAppearance[] @relation("GlobalCharacterAppearancePreviousImageMedia")
globalLocationImageImages GlobalLocationImage[] @relation("GlobalLocationImageMedia")
globalLocationImagePreviousImages GlobalLocationImage[] @relation("GlobalLocationImagePreviousImageMedia")
globalVoiceCustomVoices GlobalVoice[] @relation("GlobalVoiceCustomVoiceMedia")
@@index([createdAt])
@@map("media_objects")
}
model LegacyMediaRefBackup {
id String @id @default(uuid())
runId String
tableName String
rowId String
fieldName String
legacyValue String @db.Text
checksum String
createdAt DateTime @default(now())
@@index([runId])
@@index([tableName, fieldName])
@@map("legacy_media_refs_backup")
}