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? // 用户配置的视频模型
  audioModel      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? // 用户配置的视频模型
  audioModel      String? // 用户配置的语音模型
  lipSyncModel    String? // 用户配置的口型同步模型
  voiceDesignModel String? // 用户配置的音色设计模型
  analysisConcurrency Int? // 分析流程并发上限
  imageConcurrency Int? // 图像流程并发上限
  videoConcurrency Int? // 视频流程并发上限
  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")
  artStyle             String?
  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
  artStyle  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")
}
