forked from natuka/web.puabadge.com
修改:主体识别严格对比
This commit is contained in:
1 parent
2f9d443a6a
commit
500bdbbf66
3 files changed
+152
-36
No files matched your search
@@ -9,6 +9,8 @@ export interface ImageInspect {
|
|||||||
isCartoon: boolean
|
isCartoon: boolean
|
||||||
personCount: number
|
personCount: number
|
||||||
petCount: number
|
petCount: number
|
||||||
|
/** 道具数量(若接口有返回) */
|
||||||
|
propCount: number
|
||||||
reviewStatus: string
|
reviewStatus: string
|
||||||
conclusion: string
|
conclusion: string
|
||||||
/** 主体识别 check_id,后续下单可用 */
|
/** 主体识别 check_id,后续下单可用 */
|
||||||
@@ -40,12 +42,70 @@ export function mapInspectImageType(raw: unknown): ImageKind {
|
|||||||
|
|
||||||
function countField(data: Record<string, unknown>, keys: string[]) {
|
function countField(data: Record<string, unknown>, keys: string[]) {
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
|
if (!(key in data)) continue
|
||||||
const value = Number(data[key])
|
const value = Number(data[key])
|
||||||
if (Number.isFinite(value)) return Math.max(0, Math.trunc(value))
|
if (Number.isFinite(value)) return Math.max(0, Math.trunc(value))
|
||||||
}
|
}
|
||||||
return 0
|
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) */
|
/** 规范化 classify 接口 data(拦截器已解包 code=1000 的 data) */
|
||||||
export function normalizeInspect(body: unknown): ImageInspect {
|
export function normalizeInspect(body: unknown): ImageInspect {
|
||||||
const root = asRecord(body) || {}
|
const root = asRecord(body) || {}
|
||||||
@@ -56,23 +116,58 @@ export function normalizeInspect(body: unknown): ImageInspect {
|
|||||||
(nested.image_type != null ||
|
(nested.image_type != null ||
|
||||||
nested.person_count != null ||
|
nested.person_count != null ||
|
||||||
nested.is_cartoon != null ||
|
nested.is_cartoon != null ||
|
||||||
nested.check_id != null)
|
nested.check_id != null ||
|
||||||
|
nested.keywords != null)
|
||||||
? nested
|
? nested
|
||||||
: root
|
: root
|
||||||
const imageType = mapInspectImageType(data.image_type ?? data.imageType)
|
const imageType = mapInspectImageType(data.image_type ?? data.imageType)
|
||||||
const isCartoonRaw = data.is_cartoon ?? data.isCartoon
|
const isCartoonRaw = data.is_cartoon ?? data.isCartoon
|
||||||
const isCartoon =
|
const isCartoon =
|
||||||
typeof isCartoonRaw === 'boolean' ? isCartoonRaw : imageType === 'cartoon'
|
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 {
|
return {
|
||||||
imageType,
|
imageType,
|
||||||
isCartoon,
|
isCartoon,
|
||||||
personCount: countField(data, [
|
personCount,
|
||||||
'person_count',
|
petCount,
|
||||||
'personCount',
|
propCount,
|
||||||
'headcount',
|
|
||||||
'head_count',
|
|
||||||
]),
|
|
||||||
petCount: countField(data, ['pet_count', 'petCount']),
|
|
||||||
reviewStatus: String(data.review_status || ''),
|
reviewStatus: String(data.review_status || ''),
|
||||||
conclusion: String(data.conclusion || ''),
|
conclusion: String(data.conclusion || ''),
|
||||||
checkId: countField(data, ['check_id', 'checkId']),
|
checkId: countField(data, ['check_id', 'checkId']),
|
||||||
@@ -91,5 +186,6 @@ export function formatSubjectFromInspect(result: ImageInspect) {
|
|||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
if (result.personCount > 0) parts.push(`${result.personCount}人`)
|
if (result.personCount > 0) parts.push(`${result.personCount}人`)
|
||||||
if (result.petCount > 0) parts.push(`${result.petCount}宠物`)
|
if (result.petCount > 0) parts.push(`${result.petCount}宠物`)
|
||||||
|
if (result.propCount > 0) parts.push(`${result.propCount}道具`)
|
||||||
return parts.join('+')
|
return parts.join('+')
|
||||||
}
|
}
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
class="composer__box"
|
class="composer__box"
|
||||||
:class="{ 'is-editing': editing, 'is-disabled': disabled, 'has-custom': !!titleSpec }"
|
:class="{ 'is-editing': editing, 'is-disabled': disabled, 'has-custom': !!titleSpec }"
|
||||||
>
|
>
|
||||||
<template v-if="titleSpec">
|
<div v-if="titleSpec" class="composer__custom-block" :class="{ 'is-invalid': titleInvalid }">
|
||||||
<div class="composer__custom" :class="{ 'is-invalid': titleInvalid }">
|
<div class="composer__custom">
|
||||||
<label class="composer__custom-label" :for="titleInputId">
|
<label class="composer__custom-label" :for="titleInputId">
|
||||||
<el-icon class="composer__custom-icon" :size="14"><EditPen /></el-icon>
|
<el-icon class="composer__custom-icon" :size="14"><EditPen /></el-icon>
|
||||||
<span>{{ customLabel }}</span>
|
<span>{{ customLabel }}</span>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<p v-if="titleInvalid" class="composer__custom-error" role="alert">
|
<p v-if="titleInvalid" class="composer__custom-error" role="alert">
|
||||||
请填写{{ customLabel }}
|
请填写{{ customLabel }}
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
<div v-if="previewUrl" ref="previewRef" class="composer__preview">
|
<div v-if="previewUrl" ref="previewRef" class="composer__preview">
|
||||||
<img class="composer__preview-img" :src="previewUrl" alt="已选图片" />
|
<img class="composer__preview-img" :src="previewUrl" alt="已选图片" />
|
||||||
@@ -370,11 +370,7 @@ defineExpose({ focus, getPreviewEl, getFlyTargetEl })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer__custom {
|
.composer__custom-block {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
min-width: 0;
|
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
padding: 0 0 10px;
|
padding: 0 0 10px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
@@ -385,6 +381,13 @@ defineExpose({ focus, getPreviewEl, getFlyTargetEl })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.composer__custom {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.composer__custom-label {
|
.composer__custom-label {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -441,14 +444,14 @@ defineExpose({ focus, getPreviewEl, getFlyTargetEl })
|
|||||||
}
|
}
|
||||||
|
|
||||||
.composer__custom-hint {
|
.composer__custom-hint {
|
||||||
margin: -4px 0 10px;
|
margin: 6px 0 0;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer__custom-error {
|
.composer__custom-error {
|
||||||
margin: -4px 0 10px;
|
margin: 6px 0 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
color: #c45c26;
|
color: #c45c26;
|
||||||
|
|||||||
@@ -85,9 +85,9 @@
|
|||||||
:placeholder="config.messages.placeholder"
|
:placeholder="config.messages.placeholder"
|
||||||
:placeholder-need-photo="config.messages.placeholderNeedPhoto"
|
:placeholder-need-photo="config.messages.placeholderNeedPhoto"
|
||||||
:placeholder-editing="config.messages.placeholderEditing"
|
:placeholder-editing="config.messages.placeholderEditing"
|
||||||
:custom-text-label="titleSpec?.label || config.messages.customTextLabel"
|
:custom-text-label="composerTitleSpec?.label || config.messages.customTextLabel"
|
||||||
:custom-text-placeholder="titleSpec?.placeholder || config.messages.customTextPlaceholder"
|
:custom-text-placeholder="composerTitleSpec?.placeholder || config.messages.customTextPlaceholder"
|
||||||
:custom-text-max="titleSpec?.max || config.messages.customTextMax"
|
:custom-text-max="composerTitleSpec?.max || config.messages.customTextMax"
|
||||||
:preview-url="pendingPreview"
|
:preview-url="pendingPreview"
|
||||||
:tags="composerTags"
|
:tags="composerTags"
|
||||||
:editing="isEditing"
|
:editing="isEditing"
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
:can-send="canSend"
|
:can-send="canSend"
|
||||||
:status-tip="composerStatusTip"
|
:status-tip="composerStatusTip"
|
||||||
:tip-warn="composerTipWarn"
|
:tip-warn="composerTipWarn"
|
||||||
:title-spec="titleSpec"
|
:title-spec="composerTitleSpec"
|
||||||
:title-invalid="titleInvalid"
|
:title-invalid="titleInvalid"
|
||||||
@upload="triggerUpload"
|
@upload="triggerUpload"
|
||||||
@clear-preview="onClearPreview"
|
@clear-preview="onClearPreview"
|
||||||
@@ -331,7 +331,11 @@ function resetRemainQuota() {
|
|||||||
remainBaselineUsed.value = 0
|
remainBaselineUsed.value = 0
|
||||||
}
|
}
|
||||||
const titleSpec = computed(() => productTitleSpecFromOrder(orderStat.value))
|
const titleSpec = computed(() => productTitleSpecFromOrder(orderStat.value))
|
||||||
|
/** 编辑效果图不展示子标题输入;导入生图仍按产品规则展示 */
|
||||||
|
const composerTitleSpec = computed(() => (isEditing.value ? null : titleSpec.value))
|
||||||
const hasRequiredTitle = computed(() => {
|
const hasRequiredTitle = computed(() => {
|
||||||
|
// 编辑态不要求填写场景道具 / 飘带标题 / 相框标题
|
||||||
|
if (isEditing.value) return true
|
||||||
if (!titleSpec.value) return true
|
if (!titleSpec.value) return true
|
||||||
return productTitle.value.trim().length > 0
|
return productTitle.value.trim().length > 0
|
||||||
})
|
})
|
||||||
@@ -417,11 +421,21 @@ function checkSubjectMatch(inspectResult: ImageInspect) {
|
|||||||
const detected = {
|
const detected = {
|
||||||
headcount: Number(inspectResult.personCount) || 0,
|
headcount: Number(inspectResult.personCount) || 0,
|
||||||
petCount: Number(inspectResult.petCount) || 0,
|
petCount: Number(inspectResult.petCount) || 0,
|
||||||
|
propCount: Number(inspectResult.propCount) || 0,
|
||||||
}
|
}
|
||||||
// 严格比对:订单「1人」不允许识别出「1人+1宠物」
|
/**
|
||||||
|
* 严格按类别比对:
|
||||||
|
* - 人物数量必须相等(订单「1人」不能用纯宠物图冒充)
|
||||||
|
* - 宠物数量必须相等(订单「1人」不允许多出宠物;订单「1宠物」也不允许用人图)
|
||||||
|
* - 订单写了道具时,道具数量也要一致
|
||||||
|
*/
|
||||||
const peopleOk = detected.headcount === orderSubject.headcount
|
const peopleOk = detected.headcount === orderSubject.headcount
|
||||||
const petOk = detected.petCount === orderSubject.petCount
|
const petOk = detected.petCount === orderSubject.petCount
|
||||||
const matched = peopleOk && petOk
|
const propOk =
|
||||||
|
orderSubject.propCount > 0
|
||||||
|
? detected.propCount === orderSubject.propCount
|
||||||
|
: true
|
||||||
|
const matched = peopleOk && petOk && propOk
|
||||||
subjectMatched.value = matched
|
subjectMatched.value = matched
|
||||||
if (!matched) {
|
if (!matched) {
|
||||||
const need = subjectNameFromStat.value || formatSubjectCountsLabel(orderSubject)
|
const need = subjectNameFromStat.value || formatSubjectCountsLabel(orderSubject)
|
||||||
@@ -685,8 +699,9 @@ async function enterEdit(resultId: string, sourceRect?: FlyRect | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
editingResultId.value = resultId
|
editingResultId.value = resultId
|
||||||
// 保留用户已输入的提示词,不清空 draft
|
// 保留用户已输入的提示词,不清空 draft;编辑态不展示/不校验子标题
|
||||||
clearPendingPreview()
|
clearPendingPreview()
|
||||||
|
titleInvalid.value = false
|
||||||
|
|
||||||
pendingPreview.value = msg.image
|
pendingPreview.value = msg.image
|
||||||
subjectMatched.value = true
|
subjectMatched.value = true
|
||||||
@@ -931,7 +946,8 @@ function onSend() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (titleSpec.value && !productTitle.value.trim()) {
|
// 仅导入生图校验子标题必填;编辑效果图不展示、不校验
|
||||||
|
if (!isEditing.value && titleSpec.value && !productTitle.value.trim()) {
|
||||||
titleInvalid.value = true
|
titleInvalid.value = true
|
||||||
showToast(
|
showToast(
|
||||||
config.messages.needProductTitle(
|
config.messages.needProductTitle(
|
||||||
@@ -943,13 +959,6 @@ function onSend() {
|
|||||||
titleInvalid.value = false
|
titleInvalid.value = false
|
||||||
|
|
||||||
const text = draft.value.trim()
|
const text = draft.value.trim()
|
||||||
// 必须在 reset 前取出:场景道具走 scene_prop,其余走 custom_text
|
|
||||||
const titleValue = productTitle.value.trim()
|
|
||||||
const isPropsTitle = titleSpec.value?.mode === 'props'
|
|
||||||
const sceneProp = isPropsTitle
|
|
||||||
? resolveScenePropFromOrder(orderStat.value, titleValue)
|
|
||||||
: ''
|
|
||||||
const customText = isPropsTitle ? '' : titleValue
|
|
||||||
|
|
||||||
if (isEditing.value) {
|
if (isEditing.value) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -973,17 +982,25 @@ function onSend() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resetComposerAfterSubmit()
|
resetComposerAfterSubmit()
|
||||||
// 编辑:不走 url/confirm,submit 只传 image_id
|
// 编辑:不传子标题相关字段
|
||||||
void startGenerate({
|
void startGenerate({
|
||||||
userText: text,
|
userText: text,
|
||||||
mode: 'edit',
|
mode: 'edit',
|
||||||
imageId,
|
imageId,
|
||||||
sceneProp,
|
sceneProp: '',
|
||||||
customText,
|
customText: '',
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 必须在 reset 前取出:场景道具走 scene_prop,其余走 custom_text
|
||||||
|
const titleValue = productTitle.value.trim()
|
||||||
|
const isPropsTitle = titleSpec.value?.mode === 'props'
|
||||||
|
const sceneProp = isPropsTitle
|
||||||
|
? resolveScenePropFromOrder(orderStat.value, titleValue)
|
||||||
|
: ''
|
||||||
|
const customText = isPropsTitle ? '' : titleValue
|
||||||
|
|
||||||
if (uploading.value) {
|
if (uploading.value) {
|
||||||
showToast(config.messages.uploadingTip)
|
showToast(config.messages.uploadingTip)
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in new issue
Block a user