238 lines
7.3 KiB
TypeScript
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
|
|
}
|
|
}
|