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")
}

829
prisma/schema.sqlit.prisma Normal file
View File

@@ -0,0 +1,829 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(uuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
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?
descriptions String?
imageUrl String?
imageUrls String?
selectedIndex Int?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
previousImageUrl String?
previousImageUrls String?
previousDescription String? // 上一次的描述词(用于撤回)
previousDescriptions String? // 上一次的描述词数组(用于撤回)
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?
imageUrl String?
isSelected Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
previousImageUrl String?
previousDescription String? // 上一次的描述词(用于撤回)
imageMediaId String?
imageMedia MediaObject? @relation("LocationImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
location NovelPromotionLocation @relation(fields: [locationId], references: [id], onDelete: Cascade)
@@unique([locationId, imageIndex])
@@index([locationId])
@@index([imageMediaId])
@@map("location_images")
}
model NovelPromotionCharacter {
id String @id @default(uuid())
novelPromotionProjectId String
name String
aliases String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
customVoiceUrl String?
customVoiceMediaId String?
customVoiceMedia MediaObject? @relation("NovelPromotionCharacterVoiceMedia", fields: [customVoiceMediaId], references: [id], onDelete: SetNull)
voiceId String?
voiceType String?
profileData String?
profileConfirmed Boolean @default(false)
introduction String? // 角色介绍(身份、关系、称呼映射,如"我"对应此角色)
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? // 场景简要描述(用途/人物关联)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
sourceGlobalLocationId String? // 🆕 来源全局场景ID复制时记录
images LocationImage[]
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?
novelText String?
audioUrl String?
audioMediaId String?
audioMedia MediaObject? @relation("NovelPromotionEpisodeAudioMedia", fields: [audioMediaId], references: [id], onDelete: SetNull)
srtContent String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
speakerVoices String?
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 // JSON 存储编辑项目数据
renderStatus String? // pending | rendering | completed | failed
renderTaskId String?
outputUrl String?
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
location String?
content String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
characters String?
endText String?
shotCount Int?
startText String?
screenplay String?
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?
cameraMove String?
description String?
location String?
characters String?
srtSegment String?
srtStart Float?
srtEnd Float?
duration Float?
imagePrompt String?
imageUrl String?
imageMediaId String?
imageMedia MediaObject? @relation("NovelPromotionPanelImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
imageHistory String?
videoPrompt String?
firstLastFramePrompt String?
videoUrl String?
videoGenerationMode String? // 视频生成方式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?
linkedToNextPanel Boolean @default(false)
lipSyncTaskId String?
lipSyncVideoUrl String?
lipSyncVideoMediaId String?
lipSyncVideoMedia MediaObject? @relation("NovelPromotionPanelLipSyncVideoMedia", fields: [lipSyncVideoMediaId], references: [id], onDelete: SetNull)
sketchImageUrl String?
sketchImageMediaId String?
sketchImageMedia MediaObject? @relation("NovelPromotionPanelSketchMedia", fields: [sketchImageMediaId], references: [id], onDelete: SetNull)
photographyRules String?
actingNotes String? // 演技指导数据 JSON
previousImageUrl String?
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?
artStyle String @default("american-comic")
artStylePrompt String?
characterModel String? // 用户配置的角色图片模型
locationModel String? // 用户配置的场景图片模型
storyboardModel String? // 用户配置的分镜图片模型
editModel String? // 用户配置的修图/编辑模型
videoResolution String @default("720p")
capabilityOverrides String?
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?
locations String?
characters String?
plot String?
imagePrompt String?
scale String?
module String?
focus String?
zhSummarize String?
imageUrl String?
imageMediaId String?
imageMedia MediaObject? @relation("NovelPromotionShotImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
pov String?
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?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
panelCount Int @default(9)
storyboardTextJson String?
imageHistory String?
candidateImages String?
lastError String?
photographyPlan String?
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?
imagePrompt String?
imageUrl String?
imageMediaId String?
imageMedia MediaObject? @relation("SupplementaryPanelImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
characters String?
location String?
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?
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
metadata String?
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?
// API Key 配置(极简版)
llmBaseUrl String? @default("https://openrouter.ai/api/v1")
llmApiKey String? // 加密存储
falApiKey String? // FAL图片+视频+语音)
googleAiKey String? // Google AIGemini 图片)
arkApiKey String? // 火山引擎Seedream+Seedance
qwenApiKey String? // 阿里百炼(声音设计)
// 自定义模型列表 + 价格JSON
customModels String?
// 自定义 OpenAI 兼容提供商列表JSON包含加密的 API Key
customProviders String?
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
voicePresetId String?
audioUrl String?
audioMediaId String?
audioMedia MediaObject? @relation("NovelPromotionVoiceLineAudioMedia", fields: [audioMediaId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
emotionPrompt String?
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
audioMediaId String?
audioMedia MediaObject? @relation("VoicePresetAudioMedia", fields: [audioMediaId], references: [id], onDelete: SetNull)
description String?
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)
frozenAmount Decimal @default(0)
totalSpent Decimal @default(0)
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
status String @default("pending")
source String?
taskId String?
requestId String?
idempotencyKey String? @unique
metadata String?
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
balanceAfter Decimal
description String?
relatedId String?
freezeId String?
operatorId String?
externalOrderId String?
idempotencyKey String?
createdAt DateTime @default(now())
@@index([userId])
@@index([type])
@@index([createdAt])
@@index([freezeId])
@@index([externalOrderId])
@@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?
billingInfo Json?
billedAt DateTime?
queuedAt DateTime @default(now())
startedAt DateTime?
finishedAt DateTime?
heartbeatAt DateTime?
enqueuedAt DateTime?
enqueueAttempts Int @default(0)
lastEnqueueError String?
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?
profileData String?
profileConfirmed Boolean @default(false)
voiceId String?
voiceType String?
customVoiceUrl String?
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?
descriptions String?
imageUrl String?
imageMediaId String?
imageMedia MediaObject? @relation("GlobalCharacterAppearanceImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
imageUrls String?
selectedIndex Int?
previousImageUrl String?
previousImageMediaId String?
previousImageMedia MediaObject? @relation("GlobalCharacterAppearancePreviousImageMedia", fields: [previousImageMediaId], references: [id], onDelete: SetNull)
previousImageUrls String?
previousDescription String? // 上一次的描述词(用于撤回)
previousDescriptions String? // 上一次的描述词数组(用于撤回)
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?
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?
imageUrl String?
imageMediaId String?
imageMedia MediaObject? @relation("GlobalLocationImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
isSelected Boolean @default(false)
previousImageUrl String?
previousImageMediaId String?
previousImageMedia MediaObject? @relation("GlobalLocationImagePreviousImageMedia", fields: [previousImageMediaId], references: [id], onDelete: SetNull)
previousDescription String? // 上一次的描述词(用于撤回)
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? // 详细描述
voiceId String? // qwen-tts-vd 的 voice ID
voiceType String @default("qwen-designed") // qwen-designed | custom
customVoiceUrl String? // 上传的音频 URL预览用
customVoiceMediaId String?
customVoiceMedia MediaObject? @relation("GlobalVoiceCustomVoiceMedia", fields: [customVoiceMediaId], references: [id], onDelete: SetNull)
voicePrompt String? // 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
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
checksum String
createdAt DateTime @default(now())
@@index([runId])
@@index([tableName, fieldName])
@@map("legacy_media_refs_backup")
}