92 lines
3.8 KiB
TypeScript
92 lines
3.8 KiB
TypeScript
import { reactive } from 'vue'
|
|
import { validOptionalSize } from './model'
|
|
import type { KeyframeReadiness } from './types'
|
|
import type { QualityInput, QualityReceipt, QualityTarget, VisualValidation, VideoValidation } from './quality.types'
|
|
|
|
/** 未返回的操作不显示上次成功结论;网络失败不会自动重发。 */
|
|
const sessions = reactive<
|
|
Record<string, { receipt: QualityReceipt | null; error: string; videoValidation?: VideoValidation }>
|
|
>({})
|
|
|
|
/** 正式 ID 与目标类型共同隔离会话结果,批量回执按剧集隔离。 */
|
|
export function qualityKey(projectId: string, target: QualityTarget | null) {
|
|
return JSON.stringify([
|
|
projectId,
|
|
target?.kind,
|
|
target && ('assetId' in target ? [target.shotId, target.assetId] : target.episodeNo)
|
|
])
|
|
}
|
|
|
|
/** 同一目标关闭后重新打开仍能查看本会话的回执。 */
|
|
export function qualitySession(key: string) {
|
|
return (sessions[key] ??= { receipt: null, error: '' })
|
|
}
|
|
|
|
/** 一行一个允许文字,仅清理空白与重复,不自行添加授权内容。 */
|
|
export function allowedTextLines(value: string) {
|
|
return [
|
|
...new Set(
|
|
value
|
|
.split('\n')
|
|
.map(item => item.trim())
|
|
.filter(Boolean)
|
|
)
|
|
]
|
|
}
|
|
|
|
/** 付费前独立校验整数、尺寸与剧集范围,0 次修复代表只验不修。 */
|
|
export function validQualityInput(input: QualityInput) {
|
|
return (
|
|
[input.concurrency, input.limit, input.episodeNo].every(value => Number.isSafeInteger(value) && value > 0) &&
|
|
Number.isSafeInteger(input.maxRepairAttempts) &&
|
|
input.maxRepairAttempts >= 0 &&
|
|
validOptionalSize(input.width ?? '', input.height ?? '')
|
|
)
|
|
}
|
|
|
|
/** 与后端保持同一优先级:全项目有过期项时先筛过期,再按当前集筛选。 */
|
|
export function qualityTargets(readiness: KeyframeReadiness, input: QualityInput) {
|
|
return readiness.items
|
|
.filter(
|
|
item =>
|
|
item.status === 'ready' &&
|
|
(input.force ||
|
|
(readiness.stalePrimaryKeyframe > 0
|
|
? item.primaryKeyframeStale === true
|
|
: !item.primaryKeyframeId)) &&
|
|
item.episodeNo === input.episodeNo
|
|
)
|
|
.slice(0, input.limit)
|
|
}
|
|
|
|
/** 最小响应校验,避免畸形响应或仅 HTTP 200 被显示成视觉通过。 */
|
|
export function isVisualValidation(value: unknown): value is VisualValidation {
|
|
if (!value || typeof value !== 'object') return false
|
|
const item = value as VisualValidation
|
|
return (
|
|
typeof item.passed === 'boolean' &&
|
|
typeof item.summary === 'string' &&
|
|
typeof item.subjectCountConsistent === 'boolean' &&
|
|
Array.isArray(item.subjects) &&
|
|
item.subjects.every(
|
|
subject =>
|
|
typeof subject.subjectRef === 'string' &&
|
|
typeof subject.identityConsistent === 'boolean' &&
|
|
typeof subject.formConsistent === 'boolean' &&
|
|
Array.isArray(subject.issues) &&
|
|
subject.issues.every(issue => typeof issue === 'string')
|
|
) &&
|
|
Array.isArray(item.issues) &&
|
|
item.issues.every(
|
|
issue => typeof issue.description === 'string' && ['warning', 'error'].includes(issue.severity)
|
|
) &&
|
|
typeof item.unauthorizedText?.detected === 'boolean' &&
|
|
Array.isArray(item.unauthorizedText.texts)
|
|
)
|
|
}
|
|
|
|
/** 公开视频 DTO 不含质量历史,只使用本会话真实收到的校验回执。 */
|
|
export function sessionVideoValidation(projectId: string, shotId: string, assetId: string): VideoValidation | null {
|
|
return qualitySession(qualityKey(projectId, { kind: 'video', shotId, assetId, title: '' })).videoValidation ?? null
|
|
}
|