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 AI(Gemini 图片) 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") }