forked from natuka/web.puabadge.com
192 lines
5.8 KiB
TypeScript
192 lines
5.8 KiB
TypeScript
/**
|
||
* 图片主体识别结果(由后端 jd/subject/classify 返回后规范化)
|
||
*/
|
||
|
||
export type ImageKind = 'cartoon' | 'real_photo' | 'other' | 'uncertain'
|
||
|
||
export interface ImageInspect {
|
||
imageType: ImageKind
|
||
isCartoon: boolean
|
||
personCount: number
|
||
petCount: number
|
||
/** 道具数量(若接口有返回) */
|
||
propCount: number
|
||
reviewStatus: string
|
||
conclusion: string
|
||
/** 主体识别 check_id,后续下单可用 */
|
||
checkId: number
|
||
}
|
||
|
||
export class InspectCancelled extends Error {
|
||
constructor() {
|
||
super('cancelled')
|
||
this.name = 'InspectCancelled'
|
||
}
|
||
}
|
||
|
||
function asRecord(value: unknown) {
|
||
return value && typeof value === 'object' && !Array.isArray(value)
|
||
? (value as Record<string, unknown>)
|
||
: null
|
||
}
|
||
|
||
export function mapInspectImageType(raw: unknown): ImageKind {
|
||
const text = String(raw ?? '')
|
||
.trim()
|
||
.toLowerCase()
|
||
if (text === 'cartoon' || text === '1') return 'cartoon'
|
||
if (text === 'real_photo' || text === 'real' || text === '2') return 'real_photo'
|
||
if (text === 'other') return 'other'
|
||
return 'uncertain'
|
||
}
|
||
|
||
function countField(data: Record<string, unknown>, keys: string[]) {
|
||
for (const key of keys) {
|
||
if (!(key in data)) continue
|
||
const value = Number(data[key])
|
||
if (Number.isFinite(value)) return Math.max(0, Math.trunc(value))
|
||
}
|
||
return 0
|
||
}
|
||
|
||
function hasOwnCount(data: Record<string, unknown>, keys: string[]) {
|
||
return keys.some((key) => key in data && Number.isFinite(Number(data[key])))
|
||
}
|
||
|
||
type KeywordBucket = 'person' | 'pet' | 'prop' | 'other'
|
||
|
||
/** 按 category / 中英文关键词区分人物、宠物、道具 */
|
||
function classifyKeywordBucket(item: Record<string, unknown>): KeywordBucket {
|
||
const category = Number(item.category ?? item.Category ?? 0)
|
||
if (category === 1) return 'person'
|
||
if (category === 2) return 'pet'
|
||
if (category === 3) return 'prop'
|
||
|
||
const text = `${item.cn || ''} ${item.en || ''} ${item.name || ''} ${item.label || ''}`.toLowerCase()
|
||
if (/人|人物|人脸|儿童|成人|person|people|human|face|child|man|woman|boy|girl/.test(text)) {
|
||
return 'person'
|
||
}
|
||
if (
|
||
/宠|宠物|猫|狗|兔|鸟|鼠|仓鼠|龙猫|鹦鹉|pet|cat|dog|rabbit|bird|hamster|animal/.test(
|
||
text,
|
||
)
|
||
) {
|
||
return 'pet'
|
||
}
|
||
if (/道|具|prop|道具|物件|object/.test(text)) return 'prop'
|
||
return 'other'
|
||
}
|
||
|
||
function countFromKeywords(raw: unknown): {
|
||
personCount: number
|
||
petCount: number
|
||
propCount: number
|
||
hasKeywords: boolean
|
||
} {
|
||
if (!Array.isArray(raw) || raw.length === 0) {
|
||
return { personCount: 0, petCount: 0, propCount: 0, hasKeywords: false }
|
||
}
|
||
let personCount = 0
|
||
let petCount = 0
|
||
let propCount = 0
|
||
let useful = false
|
||
raw.forEach((entry) => {
|
||
const item = asRecord(entry)
|
||
if (!item) return
|
||
const n = Number(item.count ?? item.num ?? 1)
|
||
const count = Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : 1
|
||
if (count <= 0) return
|
||
const bucket = classifyKeywordBucket(item)
|
||
if (bucket === 'other') return
|
||
useful = true
|
||
if (bucket === 'person') personCount += count
|
||
else if (bucket === 'pet') petCount += count
|
||
else if (bucket === 'prop') propCount += count
|
||
})
|
||
return { personCount, petCount, propCount, hasKeywords: useful }
|
||
}
|
||
|
||
/** 规范化 classify 接口 data(拦截器已解包 code=1000 的 data) */
|
||
export function normalizeInspect(body: unknown): ImageInspect {
|
||
const root = asRecord(body) || {}
|
||
const nested = asRecord(root.data)
|
||
// 兼容完整响应 { code, data } / 已解包的 data 本体
|
||
const data =
|
||
nested &&
|
||
(nested.image_type != null ||
|
||
nested.person_count != null ||
|
||
nested.is_cartoon != null ||
|
||
nested.check_id != null ||
|
||
nested.keywords != null)
|
||
? nested
|
||
: root
|
||
const imageType = mapInspectImageType(data.image_type ?? data.imageType)
|
||
const isCartoonRaw = data.is_cartoon ?? data.isCartoon
|
||
const isCartoon =
|
||
typeof isCartoonRaw === 'boolean' ? isCartoonRaw : imageType === 'cartoon'
|
||
|
||
const fieldPerson = countField(data, [
|
||
'person_count',
|
||
'personCount',
|
||
'headcount',
|
||
'head_count',
|
||
])
|
||
const fieldPet = countField(data, ['pet_count', 'petCount'])
|
||
const fieldProp = countField(data, ['prop_count', 'propCount'])
|
||
const hasFieldPerson = hasOwnCount(data, [
|
||
'person_count',
|
||
'personCount',
|
||
'headcount',
|
||
'head_count',
|
||
])
|
||
const hasFieldPet = hasOwnCount(data, ['pet_count', 'petCount'])
|
||
const hasFieldProp = hasOwnCount(data, ['prop_count', 'propCount'])
|
||
|
||
const fromKeywords = countFromKeywords(data.keywords)
|
||
|
||
/**
|
||
* 严格按类别取数:
|
||
* - keywords 能分出人物/宠物时优先用(避免把宠物误记成人物)
|
||
* - 否则回退 person_count / pet_count
|
||
*/
|
||
let personCount = fieldPerson
|
||
let petCount = fieldPet
|
||
let propCount = fieldProp
|
||
if (fromKeywords.hasKeywords) {
|
||
personCount = fromKeywords.personCount
|
||
petCount = fromKeywords.petCount
|
||
propCount = fromKeywords.propCount
|
||
} else {
|
||
if (!hasFieldPerson) personCount = 0
|
||
if (!hasFieldPet) petCount = 0
|
||
if (!hasFieldProp) propCount = 0
|
||
}
|
||
|
||
return {
|
||
imageType,
|
||
isCartoon,
|
||
personCount,
|
||
petCount,
|
||
propCount,
|
||
reviewStatus: String(data.review_status || ''),
|
||
conclusion: String(data.conclusion || ''),
|
||
checkId: countField(data, ['check_id', 'checkId']),
|
||
}
|
||
}
|
||
|
||
export function isCartoonImage(result: ImageInspect) {
|
||
return result.isCartoon || result.imageType === 'cartoon'
|
||
}
|
||
|
||
export function figureTypeOf(result: ImageInspect) {
|
||
return result.imageType === 'real_photo' ? 1 : 0
|
||
}
|
||
|
||
export function formatSubjectFromInspect(result: ImageInspect) {
|
||
const parts: string[] = []
|
||
if (result.personCount > 0) parts.push(`${result.personCount}人`)
|
||
if (result.petCount > 0) parts.push(`${result.petCount}宠物`)
|
||
if (result.propCount > 0) parts.push(`${result.propCount}道具`)
|
||
return parts.join('+')
|
||
}
|