fix
This commit is contained in:
177
docs/MATERIAL_CATEGORY_FILTER_GUIDE.md
Normal file
177
docs/MATERIAL_CATEGORY_FILTER_GUIDE.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# 项目详情页面素材分类筛选功能指南
|
||||
|
||||
## 🎯 功能概述
|
||||
|
||||
在项目详情页面的素材管理区域,新增了素材分类筛选功能,允许用户根据预设的分类快速筛选和查找项目中的素材。
|
||||
|
||||
## 🚀 功能特点
|
||||
|
||||
### 1. 智能分类匹配
|
||||
- **标签匹配**: 根据素材的标签自动匹配分类
|
||||
- **文件名匹配**: 根据文件名中的关键词匹配分类
|
||||
- **AI提示匹配**: 利用分类的AI识别提示进行智能匹配
|
||||
|
||||
### 2. 实时数量统计
|
||||
- **全部分类**: 显示项目中所有素材的总数
|
||||
- **具体分类**: 显示每个分类下匹配的素材数量
|
||||
- **动态更新**: 素材变化时自动更新统计数据
|
||||
|
||||
### 3. 视觉化标识
|
||||
- **颜色标识**: 每个分类按钮使用对应的分类颜色
|
||||
- **选中状态**: 当前选中的分类高亮显示
|
||||
- **数量显示**: 分类名称后显示匹配的素材数量
|
||||
|
||||
## 📍 使用位置
|
||||
|
||||
在项目详情页面的素材管理中心,筛选器区域包含:
|
||||
- 使用状态筛选(全部/已使用/未使用)
|
||||
- 模特标签筛选
|
||||
- 类型筛选(全部/原始/分镜)
|
||||
- **素材分类筛选**(新增功能)
|
||||
|
||||
## 🔧 使用方法
|
||||
|
||||
### 1. 查看分类列表
|
||||
1. 进入任意项目的详情页面
|
||||
2. 在素材管理区域找到筛选器
|
||||
3. 查看"分类:"标签下的分类按钮列表
|
||||
|
||||
### 2. 筛选特定分类
|
||||
1. 点击任意分类按钮(如"商业 (5)")
|
||||
2. 素材列表将只显示属于该分类的素材
|
||||
3. 分类按钮会高亮显示当前选中状态
|
||||
|
||||
### 3. 查看所有素材
|
||||
1. 点击"全部"按钮
|
||||
2. 清除分类筛选,显示所有素材
|
||||
|
||||
### 4. 组合筛选
|
||||
- 可以同时使用分类筛选和其他筛选条件
|
||||
- 例如:选择"商业"分类 + "已使用"状态
|
||||
- 筛选条件会叠加生效
|
||||
|
||||
## 🎨 分类匹配规则
|
||||
|
||||
### 1. 标签匹配(优先级最高)
|
||||
- 素材标签完全匹配分类名称
|
||||
- 素材标签包含分类名称关键词
|
||||
|
||||
**示例**:
|
||||
- 分类:"商业"
|
||||
- 匹配标签:["商业", "商业宣传", "商务"]
|
||||
|
||||
### 2. 文件名匹配
|
||||
- 文件名包含分类名称
|
||||
- 文件名包含分类相关关键词
|
||||
|
||||
**示例**:
|
||||
- 分类:"教育"
|
||||
- 匹配文件名:`教育培训视频.mp4`, `education_demo.mp4`
|
||||
|
||||
### 3. AI提示匹配
|
||||
- 根据分类的AI识别提示中的关键词匹配
|
||||
- 支持多关键词匹配
|
||||
|
||||
**示例**:
|
||||
- 分类:"科技",AI提示:"科技,技术,创新,数字化"
|
||||
- 匹配素材:包含"技术"、"创新"等关键词的素材
|
||||
|
||||
## 📊 数量统计说明
|
||||
|
||||
### 统计规则
|
||||
- **全部**: 项目中所有素材的总数
|
||||
- **具体分类**: 符合该分类匹配规则的素材数量
|
||||
- **实时更新**: 添加/删除素材时自动更新
|
||||
|
||||
### 显示格式
|
||||
```
|
||||
全部 (15) # 总共15个素材
|
||||
商业 (5) # 5个商业类素材
|
||||
教育 (3) # 3个教育类素材
|
||||
科技 (2) # 2个科技类素材
|
||||
其他 (5) # 5个其他类素材
|
||||
```
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
### 1. 素材标签规范
|
||||
- **统一命名**: 使用标准的分类名称作为标签
|
||||
- **多标签**: 一个素材可以有多个相关标签
|
||||
- **中英文**: 支持中英文标签匹配
|
||||
|
||||
**推荐标签示例**:
|
||||
```
|
||||
商业素材: ["商业", "business", "商务", "企业"]
|
||||
教育素材: ["教育", "education", "培训", "学习"]
|
||||
科技素材: ["科技", "technology", "技术", "创新"]
|
||||
```
|
||||
|
||||
### 2. 文件命名规范
|
||||
- **包含关键词**: 文件名包含分类相关关键词
|
||||
- **描述性命名**: 使用有意义的文件名
|
||||
- **避免特殊字符**: 使用标准字符命名
|
||||
|
||||
**推荐文件名示例**:
|
||||
```
|
||||
商业_产品介绍_2024.mp4
|
||||
教育_在线课程_demo.mp4
|
||||
科技_AI演示_v1.mp4
|
||||
```
|
||||
|
||||
### 3. 分类管理建议
|
||||
- **合理分类**: 创建符合项目需求的分类
|
||||
- **颜色区分**: 为不同分类设置不同颜色
|
||||
- **AI提示**: 设置准确的AI识别关键词
|
||||
|
||||
## 🔍 故障排除
|
||||
|
||||
### 问题1: 素材没有被正确分类
|
||||
|
||||
**可能原因**:
|
||||
- 素材标签不匹配分类名称
|
||||
- 文件名不包含相关关键词
|
||||
- 分类的AI提示设置不准确
|
||||
|
||||
**解决方案**:
|
||||
1. 检查素材标签,添加相关分类标签
|
||||
2. 重命名文件,包含分类关键词
|
||||
3. 更新分类的AI识别提示
|
||||
|
||||
### 问题2: 分类数量显示不正确
|
||||
|
||||
**可能原因**:
|
||||
- 匹配规则过于宽泛或严格
|
||||
- 素材数据未及时更新
|
||||
|
||||
**解决方案**:
|
||||
1. 刷新页面重新加载数据
|
||||
2. 检查分类匹配规则设置
|
||||
3. 验证素材标签和文件名
|
||||
|
||||
### 问题3: 分类加载失败
|
||||
|
||||
**可能原因**:
|
||||
- 网络连接问题
|
||||
- 分类服务异常
|
||||
|
||||
**解决方案**:
|
||||
1. 检查网络连接
|
||||
2. 刷新页面重试
|
||||
3. 查看浏览器控制台错误信息
|
||||
|
||||
## 🚀 未来扩展
|
||||
|
||||
### 计划功能
|
||||
- **自动分类**: AI自动为新上传的素材分配分类
|
||||
- **批量分类**: 批量修改素材的分类标签
|
||||
- **分类统计**: 更详细的分类使用统计报告
|
||||
- **自定义分类**: 项目级别的自定义分类
|
||||
|
||||
### 性能优化
|
||||
- **缓存机制**: 缓存分类数据减少加载时间
|
||||
- **懒加载**: 大量分类时的分页加载
|
||||
- **搜索功能**: 分类名称搜索功能
|
||||
|
||||
---
|
||||
|
||||
通过素材分类筛选功能,用户可以更高效地管理和查找项目中的素材,提升工作效率!
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Video, Play, Search, Filter, Tag, Clock, HardDrive, Eye, User } from 'lucide-react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Video, Play, Search, Filter, Tag, Clock, HardDrive, Eye, User, Folder } from 'lucide-react'
|
||||
import { Project } from '../services/projectService'
|
||||
import { VideoSegment, MediaService } from '../services/mediaService'
|
||||
import { Model } from '../services/modelService'
|
||||
import { ResourceCategory, ResourceCategoryService } from '../services/resourceCategoryService'
|
||||
import VideoPlayer from './VideoPlayer'
|
||||
|
||||
interface ProjectMaterialsCenterProps {
|
||||
@@ -25,6 +26,52 @@ const ProjectMaterialsCenter: React.FC<ProjectMaterialsCenterProps> = ({
|
||||
const [selectedMaterial, setSelectedMaterial] = useState<VideoSegment | null>(null)
|
||||
const [showVideoPlayer, setShowVideoPlayer] = useState(false)
|
||||
|
||||
// 素材分类相关状态
|
||||
const [categories, setCategories] = useState<ResourceCategory[]>([])
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string>('')
|
||||
const [loadingCategories, setLoadingCategories] = useState(false)
|
||||
|
||||
// 加载分类数据
|
||||
useEffect(() => {
|
||||
loadCategories()
|
||||
}, [])
|
||||
|
||||
const loadCategories = async () => {
|
||||
setLoadingCategories(true)
|
||||
try {
|
||||
const response = await ResourceCategoryService.getAllCategories()
|
||||
if (response.status && response.data) {
|
||||
setCategories(response.data.filter(cat => cat.is_active))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load categories:', error)
|
||||
} finally {
|
||||
setLoadingCategories(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算每个分类的素材数量
|
||||
const getCategoryMaterialCount = (categoryId: string): number => {
|
||||
if (!categoryId) return materials.length // 全部分类
|
||||
|
||||
const category = categories.find(cat => cat.id === categoryId)
|
||||
if (!category) return 0
|
||||
|
||||
return materials.filter(material => {
|
||||
const categoryTitle = category.title.toLowerCase()
|
||||
const materialTags = (material.tags || []).map(tag => tag.toLowerCase())
|
||||
const filename = material.filename.toLowerCase()
|
||||
|
||||
return materialTags.includes(categoryTitle) ||
|
||||
materialTags.some(tag => tag.includes(categoryTitle)) ||
|
||||
filename.includes(categoryTitle) ||
|
||||
Boolean(category.ai_prompt &&
|
||||
category.ai_prompt.toLowerCase().split(/[,,、\s]+/)
|
||||
.some(keyword => keyword.length > 1 &&
|
||||
(materialTags.includes(keyword) || filename.includes(keyword))))
|
||||
}).length
|
||||
}
|
||||
|
||||
// 过滤素材
|
||||
const filteredMaterials = materials.filter(material => {
|
||||
// 搜索过滤
|
||||
@@ -54,7 +101,28 @@ const ProjectMaterialsCenter: React.FC<ProjectMaterialsCenterProps> = ({
|
||||
matchesUsage = material.use_count === 0
|
||||
}
|
||||
|
||||
return matchesSearch && matchesModel && matchesType && matchesUsage
|
||||
// 分类过滤
|
||||
let matchesCategory = true
|
||||
if (selectedCategoryId) {
|
||||
const category = categories.find(cat => cat.id === selectedCategoryId)
|
||||
if (category) {
|
||||
// 检查素材标签是否包含分类标题,或者文件名是否包含分类相关关键词
|
||||
const categoryTitle = category.title.toLowerCase()
|
||||
const materialTags = (material.tags || []).map(tag => tag.toLowerCase())
|
||||
const filename = material.filename.toLowerCase()
|
||||
|
||||
matchesCategory = materialTags.includes(categoryTitle) ||
|
||||
materialTags.some(tag => tag.includes(categoryTitle)) ||
|
||||
filename.includes(categoryTitle) ||
|
||||
// 检查AI提示中的关键词
|
||||
Boolean(category.ai_prompt &&
|
||||
category.ai_prompt.toLowerCase().split(/[,,、\s]+/)
|
||||
.some(keyword => keyword.length > 1 &&
|
||||
(materialTags.includes(keyword) || filename.includes(keyword))))
|
||||
}
|
||||
}
|
||||
|
||||
return matchesSearch && matchesModel && matchesType && matchesUsage && matchesCategory
|
||||
})
|
||||
|
||||
const handleModelToggle = (modelId: string) => {
|
||||
@@ -213,6 +281,53 @@ const ProjectMaterialsCenter: React.FC<ProjectMaterialsCenterProps> = ({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 素材分类过滤 */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center">
|
||||
<Folder size={14} className="text-gray-400 mr-1" />
|
||||
<span className="text-xs font-medium text-gray-700">分类:</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<button
|
||||
onClick={() => setSelectedCategoryId('')}
|
||||
className={`px-2 py-1 text-xs rounded transition-colors ${
|
||||
selectedCategoryId === ''
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
全部 ({getCategoryMaterialCount('')})
|
||||
</button>
|
||||
{loadingCategories ? (
|
||||
<div className="px-2 py-1 text-xs text-gray-500">加载中...</div>
|
||||
) : (
|
||||
categories.map((category) => {
|
||||
const count = getCategoryMaterialCount(category.id)
|
||||
return (
|
||||
<button
|
||||
key={category.id}
|
||||
onClick={() => setSelectedCategoryId(category.id)}
|
||||
className={`px-2 py-1 text-xs rounded transition-colors ${
|
||||
selectedCategoryId === category.id
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: selectedCategoryId === category.id
|
||||
? undefined
|
||||
: `${category.color}20`,
|
||||
borderColor: category.color,
|
||||
borderWidth: '1px'
|
||||
}}
|
||||
>
|
||||
{category.title} ({count})
|
||||
</button>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -222,13 +337,13 @@ const ProjectMaterialsCenter: React.FC<ProjectMaterialsCenterProps> = ({
|
||||
<div className="text-center py-8">
|
||||
<Video className="mx-auto text-gray-400 mb-3" size={48} />
|
||||
<h3 className="text-base font-medium text-gray-900 mb-1">
|
||||
{searchTerm || selectedModelIds.length > 0 || filterType !== 'all' || usageFilter !== 'all'
|
||||
{searchTerm || selectedModelIds.length > 0 || filterType !== 'all' || usageFilter !== 'all' || selectedCategoryId
|
||||
? '没有找到匹配的素材'
|
||||
: '暂无项目素材'
|
||||
}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
{searchTerm || selectedModelIds.length > 0 || filterType !== 'all' || usageFilter !== 'all'
|
||||
{searchTerm || selectedModelIds.length > 0 || filterType !== 'all' || usageFilter !== 'all' || selectedCategoryId
|
||||
? '尝试调整搜索条件或筛选器'
|
||||
: `上传包含"${project.product_name}"标签的视频素材`
|
||||
}
|
||||
|
||||
237
src/utils/materialCategoryMatcher.ts
Normal file
237
src/utils/materialCategoryMatcher.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* 素材分类匹配工具
|
||||
* 根据文件信息和分类规则自动匹配素材分类
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user