Files
waooplus/src/lib/workers/handlers/image-task-handler-shared.ts

230 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { type Job } from 'bullmq'
import { prisma } from '@/lib/prisma'
import { type TaskJobData } from '@/lib/task/types'
import { decodeImageUrlsFromDb } from '@/lib/contracts/image-urls-contract'
import {
resolveImageSourceFromGeneration,
toSignedUrlIfCos,
uploadImageSourceToCos,
withLabelBar,
} from '../utils'
export type AnyObj = Record<string, unknown>
interface CharacterAppearanceLike {
appearanceIndex?: number
changeReason: string | null
description?: string | null
descriptions?: string | null
imageUrls: string | null
imageUrl: string | null
selectedIndex: number | null
}
interface CharacterLike {
name: string
appearances?: CharacterAppearanceLike[]
}
interface LocationImageLike {
description?: string | null
availableSlots?: string | null
imageIndex?: number
isSelected: boolean
imageUrl: string | null
}
interface LocationLike {
name: string
images?: LocationImageLike[]
}
interface NovelProjectData {
videoRatio?: string | null
characters?: CharacterLike[]
locations?: LocationLike[]
}
interface PanelLike {
sketchImageUrl?: string | null
characters?: string | null
location?: string | null
}
export interface PanelCharacterReference {
name: string
appearance?: string
slot?: string
}
interface NovelDataDb {
novelPromotionProject: {
findUnique(args: Record<string, unknown>): Promise<NovelProjectData | null>
}
}
export function parseJsonStringArray(value: unknown): string[] {
if (!value) return []
if (Array.isArray(value)) {
return value.filter((item): item is string => typeof item === 'string')
}
if (typeof value !== 'string') return []
try {
const parsed = JSON.parse(value)
if (!Array.isArray(parsed)) return []
return parsed.filter((item): item is string => typeof item === 'string')
} catch {
return []
}
}
export function parseImageUrls(value: string | null | undefined, fieldName: string): string[] {
return decodeImageUrlsFromDb(value, fieldName)
}
export function clampCount(value: unknown, min: number, max: number, fallback: number) {
const n = Number(value)
if (!Number.isFinite(n)) return fallback
return Math.max(min, Math.min(max, Math.floor(n)))
}
export function pickFirstString(...values: unknown[]) {
for (const value of values) {
if (typeof value === 'string' && value.trim()) return value
}
return null
}
export async function generateLabeledImageToCos(params: {
job: Job<TaskJobData>
userId: string
modelId: string
prompt: string
label: string
targetId: string
keyPrefix: string
options?: {
referenceImages?: string[]
aspectRatio?: string
size?: string
}
}) {
const source = await resolveImageSourceFromGeneration(params.job, {
userId: params.userId,
modelId: params.modelId,
prompt: params.prompt,
options: params.options,
})
const labeled = await withLabelBar(source, params.label)
const cosKey = await uploadImageSourceToCos(labeled, params.keyPrefix, params.targetId)
return cosKey
}
export async function resolveNovelData(projectId: string) {
const db = prisma as unknown as NovelDataDb
const data = await db.novelPromotionProject.findUnique({
where: { projectId },
include: {
characters: { include: { appearances: { orderBy: { appearanceIndex: 'asc' } } } },
locations: { include: { images: { orderBy: { imageIndex: 'asc' } } } },
},
})
if (!data) {
throw new Error(`NovelPromotionProject not found: ${projectId}`)
}
return data
}
export function parsePanelCharacterReferences(value: string | null | undefined): PanelCharacterReference[] {
if (!value) return []
try {
const parsed = JSON.parse(value)
if (!Array.isArray(parsed)) return []
return parsed
.map((item: unknown) => {
if (typeof item === 'string') return { name: item }
if (!item || typeof item !== 'object') return null
const candidate = item as { name?: unknown; appearance?: unknown; slot?: unknown }
if (typeof candidate.name === 'string') {
return {
name: candidate.name,
appearance: typeof candidate.appearance === 'string' ? candidate.appearance : undefined,
slot: typeof candidate.slot === 'string' ? candidate.slot : undefined,
}
}
return null
})
.filter(Boolean) as PanelCharacterReference[]
} catch {
return []
}
}
/**
* 按角色名查找角色(支持别名匹配)
* 优先级1. 精确全名匹配 2. 按 '/' 拆分后别名精确匹配
* 例:引用名 "顾娘子" 可匹配角色 "顾娘子/顾盼之"
*/
export function findCharacterByName<T extends { name: string }>(characters: T[], referenceName: string): T | undefined {
const refLower = referenceName.toLowerCase().trim()
if (!refLower) return undefined
// 优先级 1精确全名匹配
const exact = characters.find((c) => c.name.toLowerCase().trim() === refLower)
if (exact) return exact
// 优先级 2别名匹配 — 按 '/' 拆分后任一别名精确匹配
const refAliases = refLower.split('/').map((s) => s.trim()).filter(Boolean)
for (const character of characters) {
const charAliases = character.name.toLowerCase().split('/').map((s) => s.trim()).filter(Boolean)
const hasOverlap = refAliases.some((refAlias) => charAliases.includes(refAlias))
if (hasOverlap) return character
}
return undefined
}
export async function collectPanelReferenceImages(projectData: NovelProjectData, panel: PanelLike) {
const refs: string[] = []
const sketch = toSignedUrlIfCos(panel.sketchImageUrl, 3600)
if (sketch) refs.push(sketch)
const panelCharacters = parsePanelCharacterReferences(panel.characters)
for (const item of panelCharacters) {
const character = findCharacterByName(projectData.characters || [], item.name)
if (!character) continue
const appearances = character.appearances || []
let appearance = appearances[0]
if (item.appearance) {
const matched = appearances.find((a) => (a.changeReason || '').toLowerCase() === item.appearance!.toLowerCase())
if (matched) appearance = matched
}
if (!appearance) continue
const imageUrls = parseImageUrls(appearance.imageUrls, 'characterAppearance.imageUrls')
const selectedIndex = appearance.selectedIndex
const selectedUrl = selectedIndex !== null && selectedIndex !== undefined ? imageUrls[selectedIndex] : null
const key = selectedUrl || imageUrls[0] || appearance.imageUrl
const signed = toSignedUrlIfCos(key, 3600)
if (signed) refs.push(signed)
}
if (panel.location) {
const location = (projectData.locations || []).find((loc) => loc.name.toLowerCase() === panel.location!.toLowerCase())
if (location) {
const images = location.images || []
const selected = images.find((img) => img.isSelected) || images[0]
const signed = toSignedUrlIfCos(selected?.imageUrl, 3600)
if (signed) refs.push(signed)
}
}
return refs
}