Files
mxivideo/src/utils/materialCategoryMatcher.ts
2025-07-11 15:45:24 +08:00

238 lines
7.3 KiB
TypeScript

/**
* 素材分类匹配工具
* 根据文件信息和分类规则自动匹配素材分类
*/
import { VideoSegment } from '../services/mediaService'
import { ResourceCategory } from '../services/resourceCategoryService'
export interface MaterialCategoryMatch {
categoryId: string
confidence: number // 匹配置信度 0-1
reason: string // 匹配原因
}
export class MaterialCategoryMatcher {
/**
* 根据文件扩展名匹配分类
*/
static matchByFileExtension(filename: string, categories: ResourceCategory[]): MaterialCategoryMatch[] {
const extension = filename.toLowerCase().split('.').pop() || ''
const matches: MaterialCategoryMatch[] = []
for (const category of categories) {
// 检查分类的AI识别提示中是否包含文件扩展名相关的关键词
const prompt = category.ai_recognition_prompt?.toLowerCase() || ''
if (prompt.includes(extension) ||
prompt.includes('视频') && ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv'].includes(extension) ||
prompt.includes('音频') && ['mp3', 'wav', 'aac', 'flac', 'm4a'].includes(extension) ||
prompt.includes('图片') && ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(extension)) {
matches.push({
categoryId: category.id,
confidence: 0.7,
reason: `文件扩展名匹配: ${extension}`
})
}
}
return matches
}
/**
* 根据文件名关键词匹配分类
*/
static matchByFilename(filename: string, categories: ResourceCategory[]): MaterialCategoryMatch[] {
const matches: MaterialCategoryMatch[] = []
const filenameLower = filename.toLowerCase()
for (const category of categories) {
const categoryTitle = category.title.toLowerCase()
const prompt = category.ai_recognition_prompt?.toLowerCase() || ''
// 检查文件名是否包含分类标题
if (filenameLower.includes(categoryTitle)) {
matches.push({
categoryId: category.id,
confidence: 0.9,
reason: `文件名包含分类名称: ${category.title}`
})
continue
}
// 检查文件名是否包含AI识别提示中的关键词
const keywords = prompt.split(/[,,、\s]+/).filter(word => word.length > 1)
for (const keyword of keywords) {
if (filenameLower.includes(keyword)) {
matches.push({
categoryId: category.id,
confidence: 0.6,
reason: `文件名包含关键词: ${keyword}`
})
break
}
}
}
return matches
}
/**
* 根据标签匹配分类
*/
static matchByTags(tags: string[], categories: ResourceCategory[]): MaterialCategoryMatch[] {
const matches: MaterialCategoryMatch[] = []
for (const category of categories) {
const categoryTitle = category.title.toLowerCase()
const prompt = category.ai_recognition_prompt?.toLowerCase() || ''
for (const tag of tags) {
const tagLower = tag.toLowerCase()
// 检查标签是否与分类标题匹配
if (tagLower === categoryTitle || tagLower.includes(categoryTitle)) {
matches.push({
categoryId: category.id,
confidence: 0.95,
reason: `标签匹配分类: ${tag}`
})
continue
}
// 检查标签是否与AI识别提示匹配
const keywords = prompt.split(/[,,、\s]+/).filter(word => word.length > 1)
for (const keyword of keywords) {
if (tagLower.includes(keyword)) {
matches.push({
categoryId: category.id,
confidence: 0.8,
reason: `标签包含关键词: ${keyword}`
})
break
}
}
}
}
return matches
}
/**
* 根据素材路径匹配分类
*/
static matchByPath(filePath: string, categories: ResourceCategory[]): MaterialCategoryMatch[] {
const matches: MaterialCategoryMatch[] = []
const pathLower = filePath.toLowerCase()
for (const category of categories) {
const categoryTitle = category.title.toLowerCase()
// 检查路径是否包含分类名称
if (pathLower.includes(`/${categoryTitle}/`) ||
pathLower.includes(`\\${categoryTitle}\\`) ||
pathLower.includes(categoryTitle)) {
matches.push({
categoryId: category.id,
confidence: 0.8,
reason: `路径包含分类名称: ${category.title}`
})
}
}
return matches
}
/**
* 综合匹配素材分类
*/
static matchMaterialCategory(material: VideoSegment, categories: ResourceCategory[]): MaterialCategoryMatch[] {
const allMatches: MaterialCategoryMatch[] = []
// 按文件扩展名匹配
allMatches.push(...this.matchByFileExtension(material.filename, categories))
// 按文件名匹配
allMatches.push(...this.matchByFilename(material.filename, categories))
// 按标签匹配
if (material.tags && material.tags.length > 0) {
allMatches.push(...this.matchByTags(material.tags, categories))
}
// 按路径匹配
if (material.file_path) {
allMatches.push(...this.matchByPath(material.file_path, categories))
}
// 合并相同分类的匹配结果,取最高置信度
const categoryMatches = new Map<string, MaterialCategoryMatch>()
for (const match of allMatches) {
const existing = categoryMatches.get(match.categoryId)
if (!existing || match.confidence > existing.confidence) {
categoryMatches.set(match.categoryId, match)
}
}
// 按置信度排序
return Array.from(categoryMatches.values()).sort((a, b) => b.confidence - a.confidence)
}
/**
* 获取素材的最佳匹配分类
*/
static getBestMatchCategory(material: VideoSegment, categories: ResourceCategory[]): ResourceCategory | null {
const matches = this.matchMaterialCategory(material, categories)
if (matches.length === 0) {
return null
}
const bestMatch = matches[0]
// 只有置信度超过阈值才返回匹配结果
if (bestMatch.confidence >= 0.5) {
return categories.find(cat => cat.id === bestMatch.categoryId) || null
}
return null
}
/**
* 检查素材是否属于指定分类
*/
static isMaterialInCategory(material: VideoSegment, categoryId: string, categories: ResourceCategory[]): boolean {
const matches = this.matchMaterialCategory(material, categories)
const categoryMatch = matches.find(match => match.categoryId === categoryId)
// 置信度超过0.3就认为属于该分类
return categoryMatch ? categoryMatch.confidence >= 0.3 : false
}
/**
* 获取分类的素材统计
*/
static getCategoryMaterialStats(materials: VideoSegment[], categories: ResourceCategory[]): Map<string, number> {
const stats = new Map<string, number>()
// 初始化所有分类的计数
for (const category of categories) {
stats.set(category.id, 0)
}
// 统计每个分类的素材数量
for (const material of materials) {
const matches = this.matchMaterialCategory(material, categories)
for (const match of matches) {
if (match.confidence >= 0.3) {
const current = stats.get(match.categoryId) || 0
stats.set(match.categoryId, current + 1)
}
}
}
return stats
}
}