update
This commit is contained in:
227
dict/tags.json
227
dict/tags.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"description": "标签词典 — 支持 style/outfit/scene/body/mood/topic 六大分类及人物配置",
|
||||
"version": 2,
|
||||
"description": "标签词典 v2 — 9 大分类 + 人物规则 + 停用词",
|
||||
"tags": {
|
||||
"style": {
|
||||
"label": "风格",
|
||||
@@ -64,6 +64,7 @@
|
||||
"连衣裙",
|
||||
"半身裙",
|
||||
"短裙",
|
||||
"丁字裤",
|
||||
"长裙",
|
||||
"牛仔裤",
|
||||
"热裤",
|
||||
@@ -76,6 +77,7 @@
|
||||
"针织衫",
|
||||
"外套",
|
||||
"风衣",
|
||||
"白衬衫",
|
||||
"大衣",
|
||||
"西装",
|
||||
"吊带",
|
||||
@@ -241,6 +243,144 @@
|
||||
"travel",
|
||||
"wedding"
|
||||
]
|
||||
},
|
||||
"position": {
|
||||
"label": "姿势",
|
||||
"color_bg": "#fefce8",
|
||||
"color_fg": "#a16207",
|
||||
"words": [
|
||||
"站立",
|
||||
"坐姿",
|
||||
"躺姿",
|
||||
"跪姿",
|
||||
"侧卧",
|
||||
"趴",
|
||||
"蹲姿",
|
||||
"回眸",
|
||||
"背影",
|
||||
"正面",
|
||||
"侧面",
|
||||
"仰视",
|
||||
"俯视",
|
||||
"站",
|
||||
"坐",
|
||||
"躺",
|
||||
"跪",
|
||||
"趴",
|
||||
"蹲",
|
||||
"standing",
|
||||
"sitting",
|
||||
"lying",
|
||||
"kneeling",
|
||||
"side lying",
|
||||
"back view",
|
||||
"front view",
|
||||
"profile"
|
||||
]
|
||||
},
|
||||
"location": {
|
||||
"label": "地点",
|
||||
"color_bg": "#f0fdfa",
|
||||
"color_fg": "#0f766e",
|
||||
"words": [
|
||||
"日本",
|
||||
"韩国",
|
||||
"中国",
|
||||
"台湾",
|
||||
"香港",
|
||||
"泰国",
|
||||
"越南",
|
||||
"新加坡",
|
||||
"马来西亚",
|
||||
"菲律宾",
|
||||
"印尼",
|
||||
"美国",
|
||||
"俄罗斯",
|
||||
"东京",
|
||||
"大阪",
|
||||
"首尔",
|
||||
"北京",
|
||||
"上海",
|
||||
"广州",
|
||||
"深圳",
|
||||
"成都",
|
||||
"重庆",
|
||||
"杭州",
|
||||
"三亚",
|
||||
"厦门",
|
||||
"昆明",
|
||||
"大理",
|
||||
"曼谷",
|
||||
"普吉岛",
|
||||
"巴厘岛",
|
||||
"马尔代夫",
|
||||
"工作室",
|
||||
"酒店",
|
||||
"民宿",
|
||||
"摄影棚",
|
||||
"japan",
|
||||
"korea",
|
||||
"china",
|
||||
"thailand",
|
||||
"bali",
|
||||
"tokyo",
|
||||
"seoul",
|
||||
"shanghai",
|
||||
"beijing",
|
||||
"bangkok"
|
||||
]
|
||||
},
|
||||
"brand": {
|
||||
"label": "品牌",
|
||||
"color_bg": "#fff7ed",
|
||||
"color_fg": "#c2410c",
|
||||
"words": [
|
||||
"Victoria's Secret",
|
||||
"Victorias Secret",
|
||||
"VS",
|
||||
"维密",
|
||||
"维多利亚的秘密",
|
||||
"La Perla",
|
||||
"Agent Provocateur",
|
||||
"CK",
|
||||
"Calvin Klein",
|
||||
"Nike",
|
||||
"Adidas",
|
||||
"Puma",
|
||||
"Gucci",
|
||||
"Prada",
|
||||
"Chanel",
|
||||
"Louis Vuitton",
|
||||
"LV",
|
||||
"Dior",
|
||||
"Hermes",
|
||||
"Shiseido",
|
||||
"资生堂",
|
||||
"Apple",
|
||||
"Sony",
|
||||
"Canon",
|
||||
"Fantasy Factory",
|
||||
"XiuRen",
|
||||
"秀人网",
|
||||
"XIAOYU",
|
||||
"语画界",
|
||||
"尤蜜荟",
|
||||
"美媛馆",
|
||||
"TGOD",
|
||||
"推女神",
|
||||
"IMISS",
|
||||
"MiStar",
|
||||
"YouMi",
|
||||
"爱蜜社",
|
||||
"魅妍社",
|
||||
"DKGirl",
|
||||
"御女郎",
|
||||
"HuaYang",
|
||||
"花漾",
|
||||
"BoLoli",
|
||||
"波萝社",
|
||||
"MyGirl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"stop_words": [
|
||||
@@ -289,19 +429,13 @@
|
||||
"actor",
|
||||
"actress",
|
||||
"singer",
|
||||
"秀人",
|
||||
"no",
|
||||
"网",
|
||||
"图",
|
||||
"套",
|
||||
"合集",
|
||||
"系列",
|
||||
"作品",
|
||||
"美女",
|
||||
"模特",
|
||||
"女神",
|
||||
"小姐姐",
|
||||
"性感",
|
||||
"高清",
|
||||
"原版",
|
||||
"无水印",
|
||||
"番号",
|
||||
@@ -317,35 +451,48 @@
|
||||
"shoot",
|
||||
"shot"
|
||||
],
|
||||
"person_prefixes": [
|
||||
"秀人网",
|
||||
"丝袜美腿",
|
||||
"诱惑写真",
|
||||
"白衬衫",
|
||||
"情趣内衣",
|
||||
"尤蜜荟",
|
||||
"美媛馆",
|
||||
"性感美"
|
||||
],
|
||||
"excluded_person_names": [
|
||||
"XiuRen",
|
||||
"xiuren",
|
||||
"秀人",
|
||||
"秀人网",
|
||||
"MFStar",
|
||||
"MFStar模特",
|
||||
"MiiTao",
|
||||
"蜜桃社",
|
||||
"YouMi",
|
||||
"尤蜜",
|
||||
"HuaYang",
|
||||
"花洋",
|
||||
"FeiLin",
|
||||
"Feiling",
|
||||
"飞灵",
|
||||
"MFModel",
|
||||
"LeiLei",
|
||||
"Tristana",
|
||||
"Av"
|
||||
]
|
||||
"person_rules": {
|
||||
"prefixes": [
|
||||
"美女模特",
|
||||
"性感女神",
|
||||
"清纯美女",
|
||||
"气质美女",
|
||||
"可爱女孩",
|
||||
"甜美少女",
|
||||
"时尚达人",
|
||||
"女神",
|
||||
"美女",
|
||||
"模特",
|
||||
"小姐姐",
|
||||
"小仙女",
|
||||
"御姐",
|
||||
"萝莉",
|
||||
"辣妹",
|
||||
"潮人"
|
||||
],
|
||||
"excluded": [
|
||||
"XiuRen",
|
||||
"xiuren",
|
||||
"秀人",
|
||||
"秀人网",
|
||||
"MFStar",
|
||||
"MFStar模特",
|
||||
"MiiTao",
|
||||
"蜜桃社",
|
||||
"YouMi",
|
||||
"尤蜜",
|
||||
"HuaYang",
|
||||
"花洋",
|
||||
"FeiLin",
|
||||
"Feiling",
|
||||
"飞灵",
|
||||
"MFModel",
|
||||
"LeiLei",
|
||||
"Tristana",
|
||||
"Av"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -265,30 +265,23 @@ pub async fn phase_clean_persons(db: &Database) -> (DirtySet, usize) {
|
||||
/// 这样可以防止停用词(如 "photo"、"model" 等通用词)被错误识别为人物。
|
||||
pub async fn phase_clean_stop_word_persons(db: &Database) -> (DirtySet, usize) {
|
||||
let persons = db.get_all_persons_with_counts().await;
|
||||
|
||||
// 预先复制停用词集合到本地 HashSet,避免持 RwLockReadGuard 穿越 await
|
||||
let stop_words: HashSet<String> = {
|
||||
let dict = tag_dict::get_tag_dict();
|
||||
dict.get_stop_words().clone()
|
||||
};
|
||||
|
||||
let mut dirty = DirtySet::new();
|
||||
let mut deleted = 0usize;
|
||||
|
||||
for (pid, name, _albums, _images) in &persons {
|
||||
// 检查人名本身是否在停用词中
|
||||
let in_stop_words = stop_words.contains(name)
|
||||
|| stop_words.contains(&name.to_lowercase());
|
||||
// 检查中文核心是否在停用词中
|
||||
let cn_in_stop_words = title_parser::extract_cn(name)
|
||||
.map(|cn| stop_words.contains(&cn))
|
||||
// 使用 tag_dict 的 is_excluded_person 进行判断(含包含匹配)
|
||||
let is_excluded = tag_dict::get_tag_dict().is_excluded_person(name);
|
||||
|
||||
// 也检查中文核心(如 "秀人" 是排除词,"秀人网" 也会包含)
|
||||
let cn_check = title_parser::extract_cn(name)
|
||||
.map(|cn| tag_dict::get_tag_dict().is_excluded_person(&cn))
|
||||
.unwrap_or(false);
|
||||
|
||||
if in_stop_words || cn_in_stop_words {
|
||||
if is_excluded || cn_check {
|
||||
let album_list = db.get_person_albums(*pid).await;
|
||||
let album_ids: Vec<String> = album_list.into_iter().map(|a| a.id.to_string()).collect();
|
||||
for aid in &album_ids { dirty.add(aid); }
|
||||
println!("[管线·PhaseA2B] 停用词命中人物:{} (id={}),释放 {} 个相册", name, pid, album_ids.len());
|
||||
println!("[管线·PhaseA2B] 排除人名命中:{} (id={}),释放 {} 个相册", name, pid, album_ids.len());
|
||||
// 清空相册的 person 字段
|
||||
db.update_album_person_by_person_id(*pid).await;
|
||||
// 彻底删除该人物
|
||||
@@ -298,9 +291,9 @@ pub async fn phase_clean_stop_word_persons(db: &Database) -> (DirtySet, usize) {
|
||||
}
|
||||
|
||||
if deleted > 0 {
|
||||
println!("[管线·PhaseA2B] 清理完成:移除 {} 个与停用词重叠的人物,标记 {} 个脏相册", deleted, dirty.len());
|
||||
println!("[管线·PhaseA2B] 清理完成:移除 {} 个被排除的人物,标记 {} 个脏相册", deleted, dirty.len());
|
||||
} else {
|
||||
println!("[管线·PhaseA2B] 清理完成:无与停用词重叠的人物");
|
||||
println!("[管线·PhaseA2B] 清理完成:无被排除的人物");
|
||||
}
|
||||
(dirty, deleted)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
//! 标签词典 — 从外部 JSON 文件加载分类标签、停用词、颜色配置
|
||||
//! 标签词典 — 从外部 JSON 文件加载分类标签、停用词、人物规则
|
||||
//!
|
||||
//! 结构:
|
||||
//! - `TagDict` 是唯一的运行时词典实例,通过 `lazy_static!` 全局可用
|
||||
//! - 词典文件路径默认 `dict/tags.json`,可通过 `load()` 或 `reload()` 加载
|
||||
//! - 提供 `get_category()` / `get_all_categories()` / `is_stop_word()` / `classify()` 等查询接口
|
||||
//! - `reload()` 可热重载,词典变更后无需重启服务即可生效
|
||||
//! 词典文件格式(dict/tags.json v2):
|
||||
//! ```json
|
||||
//! {
|
||||
//! "version": 2,
|
||||
//! "tags": { "style": {"words": [...]}, "outfit": {...}, ... },
|
||||
//! "stop_words": [...],
|
||||
//! "person_rules": {
|
||||
//! "prefixes": ["美女模特", ...],
|
||||
//! "excluded": ["XiuRen", ...]
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
@@ -23,9 +30,7 @@ pub struct TagDictFile {
|
||||
pub tags: HashMap<String, TagCategoryDef>,
|
||||
pub stop_words: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub person_prefixes: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub excluded_person_names: Vec<String>,
|
||||
pub person_rules: Option<PersonRulesDef>,
|
||||
}
|
||||
|
||||
/// 分类定义
|
||||
@@ -40,6 +45,15 @@ pub struct TagCategoryDef {
|
||||
pub words: Vec<String>,
|
||||
}
|
||||
|
||||
/// 人物规则定义
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct PersonRulesDef {
|
||||
#[serde(default)]
|
||||
pub prefixes: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub excluded: Vec<String>,
|
||||
}
|
||||
|
||||
// ── 运行时词典结构 ──
|
||||
|
||||
/// 分类信息
|
||||
@@ -61,9 +75,9 @@ pub struct TagDict {
|
||||
pub tag_to_category: HashMap<String, String>,
|
||||
/// 停用词集合
|
||||
pub stop_words: HashSet<String>,
|
||||
/// 模特称呼前缀(提取人名时先移除的称谓词)
|
||||
/// 人物称呼前缀(提取人名时直接 replace 移除)
|
||||
pub person_prefixes: Vec<String>,
|
||||
/// 排除的人名(工作室/平台名,不应作为人物)
|
||||
/// 排除的人物名(工作室/平台名,匹配上的人和包含这些词的人直接删除重解析)
|
||||
pub excluded_person_names: HashSet<String>,
|
||||
}
|
||||
|
||||
@@ -76,7 +90,6 @@ impl Default for TagDict {
|
||||
person_prefixes: Vec::new(),
|
||||
excluded_person_names: HashSet::new(),
|
||||
};
|
||||
// 内建回退:即使 JSON 文件不存在也能工作
|
||||
dict.fill_builtin();
|
||||
dict
|
||||
}
|
||||
@@ -91,32 +104,37 @@ impl TagDict {
|
||||
/// 从指定路径加载词典
|
||||
pub fn load_from(path: impl AsRef<Path>) -> Self {
|
||||
let path = path.as_ref();
|
||||
if !path.exists() {
|
||||
eprintln!("[词典] 词典文件不存在 {:?},使用内建回退", path);
|
||||
return TagDict::default();
|
||||
}
|
||||
let content = match std::fs::read_to_string(path) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("[词典] 警告: 无法读取词典文件 {:?}: {},使用内建回退", path, e);
|
||||
let mut d = TagDict::default();
|
||||
d.fill_builtin();
|
||||
return d;
|
||||
eprintln!("[词典] 无法读取 {:?}: {},使用内建回退", path, e);
|
||||
return TagDict::default();
|
||||
}
|
||||
};
|
||||
|
||||
let file: TagDictFile = match serde_json::from_str(&content) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("[词典] 警告: 解析词典文件 {:?} 失败: {},使用内建回退", path, e);
|
||||
let mut d = TagDict::default();
|
||||
d.fill_builtin();
|
||||
return d;
|
||||
eprintln!("[词典] 解析 {:?} 失败: {},使用内建回退", path, e);
|
||||
return TagDict::default();
|
||||
}
|
||||
};
|
||||
|
||||
let (person_prefixes, excluded_person_names) = match file.person_rules {
|
||||
Some(rules) => (rules.prefixes, rules.excluded.into_iter().collect()),
|
||||
None => (Vec::new(), HashSet::new()),
|
||||
};
|
||||
|
||||
let mut dict = TagDict {
|
||||
categories: HashMap::new(),
|
||||
tag_to_category: HashMap::new(),
|
||||
stop_words: file.stop_words.into_iter().collect(),
|
||||
person_prefixes: file.person_prefixes,
|
||||
excluded_person_names: file.excluded_person_names.into_iter().collect(),
|
||||
person_prefixes,
|
||||
excluded_person_names,
|
||||
};
|
||||
|
||||
for (cat_name, def) in file.tags {
|
||||
@@ -137,25 +155,27 @@ impl TagDict {
|
||||
});
|
||||
}
|
||||
|
||||
println!("[词典] 已加载 {} 个分类, {} 个标签, {} 个停用词",
|
||||
dict.categories.len(), dict.tag_to_category.len(), dict.stop_words.len());
|
||||
if !path.as_os_str().is_empty() {
|
||||
println!("[词典] v{} 已加载: {} 分类, {} 标签, {} 停用词, {} 前缀, {} 排除人名",
|
||||
file.version, dict.categories.len(), dict.tag_to_category.len(),
|
||||
dict.stop_words.len(), dict.person_prefixes.len(), dict.excluded_person_names.len());
|
||||
}
|
||||
|
||||
dict
|
||||
}
|
||||
|
||||
/// 热重载词典(从默认路径重新读取)
|
||||
/// 热重载词典
|
||||
pub fn reload(&mut self) {
|
||||
*self = Self::load();
|
||||
}
|
||||
|
||||
/// 获取所有分类名称(按固定顺序返回,保证前端显示一致性)
|
||||
/// 获取所有分类名称(固定顺序)
|
||||
pub fn get_category_order(&self) -> Vec<&str> {
|
||||
let preferred = ["style", "outfit", "scene", "body", "mood", "topic"];
|
||||
let preferred = ["style", "outfit", "scene", "body", "mood", "topic", "position", "location", "brand"];
|
||||
let mut result: Vec<&str> = preferred.iter()
|
||||
.filter(|c| self.categories.contains_key(**c))
|
||||
.map(|c| *c)
|
||||
.collect();
|
||||
// 补上自定义分类
|
||||
for name in self.categories.keys() {
|
||||
if !result.contains(&name.as_str()) {
|
||||
result.push(name);
|
||||
@@ -164,7 +184,7 @@ impl TagDict {
|
||||
result
|
||||
}
|
||||
|
||||
/// 获取分类信息(如果存在)
|
||||
/// 获取分类信息
|
||||
pub fn get_category(&self, name: &str) -> Option<&CategoryInfo> {
|
||||
self.categories.get(name)
|
||||
}
|
||||
@@ -184,17 +204,17 @@ impl TagDict {
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断一个词是否属于某个已知分类,如果是则返回分类名
|
||||
/// 判断一个词属于哪个已知分类
|
||||
pub fn classify(&self, word: &str) -> Option<&str> {
|
||||
self.tag_to_category.get(&word.to_lowercase()).map(|s| s.as_str())
|
||||
}
|
||||
|
||||
/// 判断一个词是否是停用词
|
||||
/// 判断是否是停用词
|
||||
pub fn is_stop_word(&self, word: &str) -> bool {
|
||||
self.stop_words.contains(&word.to_lowercase())
|
||||
}
|
||||
|
||||
/// 获取所有标签到分类的映射(用于匹配加速)
|
||||
/// 获取所有标签→分类映射
|
||||
pub fn get_tag_category_map(&self) -> &HashMap<String, String> {
|
||||
&self.tag_to_category
|
||||
}
|
||||
@@ -204,75 +224,94 @@ impl TagDict {
|
||||
&self.stop_words
|
||||
}
|
||||
|
||||
/// 判断一个词是否是排除人名
|
||||
pub fn is_excluded_person_name(&self, name: &str) -> bool {
|
||||
let name_lower = name.to_lowercase();
|
||||
self.excluded_person_names.contains(name) || self.excluded_person_names.contains(&name_lower)
|
||||
/// ── 人物规则 ──
|
||||
|
||||
/// 判断人名是否应被排除(精确匹配 或 被排除词包含)
|
||||
/// 例如:排除词 "秀人" 会匹配 DB 中的 "秀人网"
|
||||
pub fn is_excluded_person(&self, person_name: &str) -> bool {
|
||||
let name_lower = person_name.to_lowercase();
|
||||
// 精确匹配
|
||||
if self.excluded_person_names.contains(person_name) {
|
||||
return true;
|
||||
}
|
||||
if self.excluded_person_names.contains(&name_lower) {
|
||||
return true;
|
||||
}
|
||||
// 包含匹配:排除词是 person_name 的子串
|
||||
for excluded in &self.excluded_person_names {
|
||||
if name_lower.contains(&excluded.to_lowercase()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 反向包含:person_name 包含在排除名中(如排除 "秀人",人名 "秀人网")
|
||||
// 实际上上面的 contains 已经覆盖了
|
||||
false
|
||||
}
|
||||
|
||||
/// 获取模特称呼前缀列表
|
||||
/// 获取人物称呼前缀列表
|
||||
pub fn get_person_prefixes(&self) -> &[String] {
|
||||
&self.person_prefixes
|
||||
}
|
||||
|
||||
/// 内置回退:当 JSON 文件不可用时使用硬编码数据
|
||||
/// 获取排除人名集合
|
||||
pub fn get_excluded_persons(&self) -> &HashSet<String> {
|
||||
&self.excluded_person_names
|
||||
}
|
||||
|
||||
/// 内置回退
|
||||
fn fill_builtin(&mut self) {
|
||||
let builtin_categories: Vec<(&str, &str, &str, &str, Vec<&str>)> = vec![
|
||||
let builtin = vec![
|
||||
("style", "风格", "#f0f9ff", "#0284c7", vec![
|
||||
"日系", "韩系", "森系", "复古", "甜美", "性感", "清新", "冷艳",
|
||||
"优雅", "知性", "可爱", "俏皮", "温柔", "妩媚", "野性", "清纯",
|
||||
"时尚", "潮流", "街头", "学院", "文艺", "古典", "民族", "异域",
|
||||
"japanese", "korean", "retro", "vintage", "cute", "sexy",
|
||||
"elegant", "fresh", "sweet", "wild", "pure", "fashion",
|
||||
"日系", "韩系", "复古", "甜美", "性感", "清新", "冷艳",
|
||||
"优雅", "知性", "可爱", "俏皮", "温柔",
|
||||
"时尚", "文艺", "古典",
|
||||
]),
|
||||
("outfit", "服装", "#fdf2f8", "#db2777", vec![
|
||||
"比基尼", "泳装", "泳衣", "旗袍", "制服", "丝袜", "内衣", "文胸",
|
||||
"包臀裙", "紧身裙", "超短裙", "连衣裙", "半身裙", "短裙", "长裙",
|
||||
"牛仔裤", "热裤", "短裤", "休闲裤", "西装裤",
|
||||
"T恤", "衬衫", "毛衣", "针织衫", "外套", "风衣", "大衣", "西装",
|
||||
"吊带", "抹胸", "露背", "透视", "蕾丝", "镂空", "开叉",
|
||||
"高跟鞋", "运动鞋", "凉鞋", "拖鞋", "靴子",
|
||||
"bikini", "swimsuit", "stockings", "lingerie", "dress",
|
||||
"skirt", "jeans", "shirt", "sweater", "coat", "heels",
|
||||
"比基尼", "泳装", "旗袍", "制服", "丝袜", "内衣",
|
||||
"连衣裙", "短裙", "长裙", "牛仔裤", "热裤",
|
||||
"吊带", "抹胸", "蕾丝", "镂空",
|
||||
"高跟鞋",
|
||||
]),
|
||||
("scene", "场景", "#f0fdf4", "#15803d", vec![
|
||||
"室内", "户外", "街拍", "浴室", "浴缸", "床上", "床照", "沙发",
|
||||
"厨房", "阳台", "窗边", "楼梯", "走廊", "电梯", "车内", "驾驶座",
|
||||
"海边", "沙滩", "泳池", "温泉", "森林", "草地", "花园", "公园",
|
||||
"街道", "巷弄", "建筑", "天台", "屋顶", "咖啡馆", "餐厅", "酒店",
|
||||
"indoor", "outdoor", "street", "beach", "pool", "bathroom",
|
||||
"bedroom", "kitchen", "balcony", "car", "cafe", "hotel",
|
||||
"室内", "户外", "浴室", "床上",
|
||||
"海边", "沙滩", "泳池", "花园",
|
||||
]),
|
||||
("body", "身体", "#fef3c7", "#b45309", vec![
|
||||
"美腿", "美胸", "翘臀", "玉足", "美背", "香肩", "蛮腰", "细腰",
|
||||
"长腿", "酥胸", "丰臀", "白皙", "肌肤", "纹身",
|
||||
"legs", "breasts", "ass", "feet", "back", "shoulders", "waist",
|
||||
"skin", "tattoo",
|
||||
"美腿", "美胸", "翘臀", "玉足", "美背", "香肩",
|
||||
]),
|
||||
("mood", "情绪", "#faf5ff", "#7c3aed", vec![
|
||||
"清纯", "诱惑", "唯美", "清新", "冷艳", "妩媚", "娇羞", "俏皮",
|
||||
"温柔", "野性", "冷傲", "热情", "慵懒", "迷离", "甜美", "知性",
|
||||
"pure", "seductive", "beautiful", "fresh", "cool", "charming",
|
||||
"shy", "cute", "gentle", "wild", "lazy", "sweet",
|
||||
"清纯", "诱惑", "唯美", "清新", "冷艳", "妩媚",
|
||||
]),
|
||||
("topic", "主题", "#f0f9ff", "#0369a1", vec![
|
||||
"写真", "私房", "街拍", "旅拍", "婚纱照", "艺术照", "生活照",
|
||||
"自拍", "跟拍", "约拍", "商拍",
|
||||
"photoshoot", "portrait", "selfie", "travel", "wedding",
|
||||
"写真", "私房", "街拍", "旅拍", "婚纱照",
|
||||
]),
|
||||
("position", "姿势", "#fefce8", "#a16207", vec![
|
||||
"站立", "坐姿", "躺姿", "跪姿", "侧卧",
|
||||
]),
|
||||
("location", "地点", "#f0fdfa", "#0f766e", vec![
|
||||
"日本", "韩国", "中国", "泰国",
|
||||
"东京", "首尔", "北京", "上海",
|
||||
"japan", "korea", "china",
|
||||
]),
|
||||
("brand", "品牌", "#fff7ed", "#c2410c", vec![
|
||||
"Victoria's Secret", "维密",
|
||||
"Calvin Klein", "CK",
|
||||
"Gucci", "Chanel",
|
||||
"Louis Vuitton",
|
||||
]),
|
||||
];
|
||||
|
||||
let stop_words: Vec<&str> = vec![
|
||||
"photos", "photo", "pictures", "picture", "images", "image", "video", "videos",
|
||||
"cosplay", "cos", "and", "the", "of", "in", "a", "an", "is", "to", "for",
|
||||
"photos", "photo", "pictures", "picture", "images", "image",
|
||||
"video", "videos", "cosplay", "cos",
|
||||
"and", "the", "of", "in", "a", "an", "is", "to", "for",
|
||||
"part", "vol", "collection", "special", "extra", "bonus",
|
||||
"new", "old", "selfie", "official", "original", "preview", "patreon",
|
||||
"with", "without", "set", "gallery", "album", "page", "issue",
|
||||
"new", "old", "selfie", "official", "original",
|
||||
"set", "gallery", "album", "page", "issue",
|
||||
"model", "idol", "star", "actor", "actress", "singer",
|
||||
"秀人", "网", "图", "套", "合集", "系列", "作品", "美女", "模特",
|
||||
"女神", "小姐姐", "性感", "高清", "原版", "无水印", "番号", "编号",
|
||||
"site", "web", "online", "digital", "exclusive", "premium",
|
||||
"classic", "photography", "shoot", "shot",
|
||||
"秀人", "网", "图", "套", "合集", "系列", "作品",
|
||||
"美女", "模特",
|
||||
"高清", "原版", "无水印", "番号", "编号",
|
||||
];
|
||||
|
||||
let person_prefixes: Vec<&str> = vec![
|
||||
@@ -281,71 +320,57 @@ impl TagDict {
|
||||
"小仙女", "御姐", "萝莉", "辣妹", "潮人",
|
||||
];
|
||||
|
||||
let excluded_names: Vec<&str> = vec![
|
||||
"XiuRen", "xiuren", "秀人", "秀人网", "MFStar", "MFStar模特",
|
||||
"MiiTao", "蜜桃社", "YouMi", "尤蜜", "HuaYang", "花洋",
|
||||
"FeiLin", "Feiling", "飞灵", "MFModel", "LeiLei",
|
||||
"Tristana", "Av",
|
||||
let excluded: Vec<&str> = vec![
|
||||
"XiuRen", "xiuren", "秀人", "秀人网",
|
||||
"MFStar", "MiiTao", "蜜桃社",
|
||||
"YouMi", "尤蜜", "HuaYang", "花洋",
|
||||
"FeiLin", "Feiling", "飞灵", "MFModel",
|
||||
"LeiLei", "Tristana", "Av",
|
||||
];
|
||||
|
||||
for word in &stop_words {
|
||||
self.stop_words.insert(word.to_string());
|
||||
}
|
||||
for w in &stop_words { self.stop_words.insert(w.to_string()); }
|
||||
self.person_prefixes = person_prefixes.iter().map(|s| s.to_string()).collect();
|
||||
for n in &excluded_names {
|
||||
self.excluded_person_names.insert(n.to_string());
|
||||
}
|
||||
for n in &excluded { self.excluded_person_names.insert(n.to_string()); }
|
||||
|
||||
for (name, label, bg, fg, words) in builtin_categories {
|
||||
let word_strings: Vec<String> = words.iter().map(|w| w.to_string()).collect();
|
||||
for word in &word_strings {
|
||||
self.tag_to_category.insert(word.to_lowercase(), name.to_string());
|
||||
}
|
||||
for (name, label, bg, fg, words) in builtin {
|
||||
let ws: Vec<String> = words.iter().map(|w| w.to_string()).collect();
|
||||
for w in &ws { self.tag_to_category.insert(w.to_lowercase(), name.to_string()); }
|
||||
self.categories.insert(name.to_string(), CategoryInfo {
|
||||
name: name.to_string(),
|
||||
label: label.to_string(),
|
||||
color_bg: bg.to_string(),
|
||||
color_fg: fg.to_string(),
|
||||
words: word_strings,
|
||||
name: name.to_string(), label: label.to_string(),
|
||||
color_bg: bg.to_string(), color_fg: fg.to_string(), words: ws,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── lazy_static 全局词典实例 ──
|
||||
// ── lazy_static 全局词典 ──
|
||||
|
||||
lazy_static! {
|
||||
/// 全局词典实例。首次访问时自动从默认路径加载。
|
||||
/// 可在运行时通过 `reload_tag_dict()` 热重载。
|
||||
static ref TAG_DICT: RwLock<TagDict> = RwLock::new(TagDict::load());
|
||||
}
|
||||
|
||||
/// 获取全局词典的只读引用
|
||||
/// 获取全局词典只读引用
|
||||
pub fn get_tag_dict() -> std::sync::RwLockReadGuard<'static, TagDict> {
|
||||
TAG_DICT.read().expect("tag_dict 锁被污染")
|
||||
}
|
||||
|
||||
/// 热重载标签词典(从默认路径重新读取)
|
||||
/// 热重载词典
|
||||
pub fn reload_tag_dict() -> String {
|
||||
let mut dict = TAG_DICT.write().expect("tag_dict 锁被污染");
|
||||
dict.reload();
|
||||
format!(
|
||||
"词典已重载:{} 个分类, {} 个标签, {} 个停用词",
|
||||
dict.categories.len(),
|
||||
dict.tag_to_category.len(),
|
||||
dict.stop_words.len(),
|
||||
"词典已重载: {} 分类, {} 标签, {} 停用词, {} 前缀, {} 排除人名",
|
||||
dict.categories.len(), dict.tag_to_category.len(),
|
||||
dict.stop_words.len(), dict.person_prefixes.len(), dict.excluded_person_names.len(),
|
||||
)
|
||||
}
|
||||
|
||||
/// 获取词典的统计信息字符串
|
||||
/// 获取词典统计信息
|
||||
pub fn get_tag_dict_stats() -> String {
|
||||
let dict = get_tag_dict();
|
||||
format!(
|
||||
"标签词典: {} 个分类, {} 个标签, {} 个停用词, {} 个排除人名, {} 个称呼前缀 | 文件: dict/tags.json",
|
||||
dict.categories.len(),
|
||||
dict.tag_to_category.len(),
|
||||
dict.stop_words.len(),
|
||||
dict.excluded_person_names.len(),
|
||||
dict.person_prefixes.len(),
|
||||
"标签词典: {} 分类, {} 标签, {} 停用词, {} 前缀, {} 排除人名 | 文件: dict/tags.json",
|
||||
dict.categories.len(), dict.tag_to_category.len(),
|
||||
dict.stop_words.len(), dict.person_prefixes.len(), dict.excluded_person_names.len(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -65,28 +65,21 @@ fn classify_tag(word: &str) -> String {
|
||||
}
|
||||
|
||||
/// 获取分类的优先级索引(用于排序)
|
||||
/// 从词典动态获取,支持任意新分类
|
||||
fn category_order_index(category: &str) -> usize {
|
||||
// 使用词典中的顺序,预定义分类在前,自定义在后
|
||||
match category {
|
||||
"style" => 0,
|
||||
"outfit" => 1,
|
||||
"scene" => 2,
|
||||
"body" => 3,
|
||||
"mood" => 4,
|
||||
"topic" => 5,
|
||||
"noun" => 6,
|
||||
"proper" => 7,
|
||||
"location" => 8,
|
||||
_ => 999,
|
||||
}
|
||||
let dict = tag_dict::get_tag_dict();
|
||||
let order = dict.get_category_order();
|
||||
order.iter().position(|c| c == &category).unwrap_or(999)
|
||||
}
|
||||
|
||||
/// 获取所有分类(按词典定义的顺序)
|
||||
fn get_category_order_list() -> Vec<&'static str> {
|
||||
vec![
|
||||
"style", "outfit", "scene", "body", "mood", "topic",
|
||||
"noun", "proper", "location", "other"
|
||||
]
|
||||
/// 获取所有分类(按词典定义的顺序,动态生成)
|
||||
fn get_category_order_list() -> Vec<String> {
|
||||
let dict = tag_dict::get_tag_dict();
|
||||
let mut list: Vec<String> = dict.get_category_order().iter()
|
||||
.map(|c| c.to_string())
|
||||
.collect();
|
||||
list.extend(vec!["noun".to_string(), "proper".to_string(), "other".to_string()]);
|
||||
list
|
||||
}
|
||||
|
||||
/// 用 jieba 分词提取名词标签(排除人名)
|
||||
|
||||
@@ -5,81 +5,18 @@ use regex::Regex;
|
||||
use std::collections::HashSet;
|
||||
use lazy_static::lazy_static;
|
||||
use jieba_rs::Jieba;
|
||||
use crate::engine::tag_dict;
|
||||
|
||||
// 全局 jieba 分词器
|
||||
lazy_static! {
|
||||
static ref JIEBA: Jieba = Jieba::new();
|
||||
}
|
||||
|
||||
// ── 常量 ──
|
||||
|
||||
/// 模特称呼前缀(移除用)
|
||||
const MODEL_TITLE_PREFIX: &[&str] = &[
|
||||
"美女模特", "性感女神", "清纯美女", "气质美女", "可爱女孩",
|
||||
"甜美少女", "时尚达人", "女神", "美女", "模特", "小姐姐",
|
||||
"小仙女", "御姐", "萝莉", "辣妹", "潮人",
|
||||
];
|
||||
|
||||
/// 停用词(标签过滤用)
|
||||
const STOP_WORDS: &[&str] = &[
|
||||
"写真", "套图", "图片", "照片", "摄影", "拍摄", "风格",
|
||||
"系列", "作品", "专辑", "主题", "人像", "艺术", "唯美",
|
||||
"诱惑", "性感", "迷人", "魅力", "时尚", "潮流", "高清",
|
||||
"原版", "无水印", "私房", "内衣", "比基尼", "泳装",
|
||||
"旗袍", "制服", "丝袜", "美腿", "美胸", "翘臀",
|
||||
"cosplay", "cos", "角色扮演", "动漫", "游戏",
|
||||
"classic", "exclusive", "premium", "collection",
|
||||
"photo", "photos", "video", "videos", "set", "vol",
|
||||
"no", "番号", "编号", "期号",
|
||||
];
|
||||
// ── 常量(仅保留正则相关常量,人物规则从 tag_dict 动态读取)──
|
||||
|
||||
/// 保留词性(仅用于 jieba 分词场景,这里做占位符)
|
||||
const _KEEP_POS: &[&str] = &["n", "nr", "ns", "nz", "v", "vn", "a", "an", "eng", "x"];
|
||||
|
||||
/// 工作室/平台排除名(大幅扩充)
|
||||
const EXCLUDED_NAMES: &[&str] = &[
|
||||
// 工作室/平台
|
||||
"Pure Media", "XiuRen秀人网", "尤蜜", "魅妍社", "花漾",
|
||||
"瑞丝馆", "撸图", "情欲萝莉", "蜜桃社", "tgod",
|
||||
"MiStar", "BLUECAKE", "HuaYang", "YouMi", "秀人",
|
||||
"XIUREN", "YouMi尤蜜", "IMiss爱蜜社", "MiiTao蜜桃社",
|
||||
"MFStar模范学院", "TGOD推女神", "DKGirl御女郎",
|
||||
"XINGYAN星颜社", "XIAOYU语画界", "YOUMI尤蜜荟",
|
||||
"FAIRY CLUB", "FAIRY-CLUB", "FEILIN嗲囡囡",
|
||||
"CANDY糖果画报", "YouWu尤物馆", "UXING优星馆",
|
||||
"108TV", "108酱", "RuiSG瑞丝馆", "SIW斯文传媒",
|
||||
"SLADY猎女神", "SSA丝社", "SJA佳爷", "TASHA系列",
|
||||
"Ugirls爱尤物", "V Girls", "VGirl", "WingS影私荟",
|
||||
"YALAYI雅拉伊", "ISHOW爱秀", "ISHOW",
|
||||
"写真", "私房", "套图", "系列", "合集", "作品",
|
||||
// 国家/地区
|
||||
"日本", "韩国", "中国", "台湾", "香港", "泰国", "越南",
|
||||
"新加坡", "马来西亚", "菲律宾", "印度尼西亚", "美国", "俄罗斯",
|
||||
"东京", "首尔", "北京", "上海", "广州", "深圳", "成都", "重庆",
|
||||
"曼谷", "胡志明", "新加坡市", "马尼拉", "雅加达",
|
||||
// 非人名常见词
|
||||
"模特", "美女", "女神", "小姐姐", "小仙女", "御姐", "萝莉",
|
||||
"性感", "诱惑", "清纯", "甜美", "可爱", "迷人", "魅力",
|
||||
"时尚", "潮流", "高清", "原版", "无水印", "完整版",
|
||||
"比基尼", "泳装", "内衣", "丝袜", "制服", "旗袍", "睡衣",
|
||||
"浴室", "浴缸", "床上", "沙发", "厨房", "阳台",
|
||||
"室内", "户外", "街拍", "旅拍", "海边", "沙滩", "泳池",
|
||||
"cosplay", "角色扮演", "动漫", "游戏",
|
||||
"超短裙", "包臀裙", "紧身裙", "连衣裙", "牛仔裤", "热裤",
|
||||
"高跟鞋", "运动鞋", "凉鞋",
|
||||
"美腿", "美胸", "翘臀", "玉足", "美背", "香肩", "蛮腰",
|
||||
"白皙", "肌肤", "纹身",
|
||||
"日系", "韩系", "森系", "复古", "冷艳", "优雅", "知性",
|
||||
"俏皮", "温柔", "妩媚", "野性", "热情", "慵懒", "迷离",
|
||||
"唯美", "清新", "冷傲",
|
||||
// 工作室前缀
|
||||
"PureMedia", "BlueCake", "HuaYang", "XiuRen", "MiStar",
|
||||
"YouMi", "MFStar", "TGOD", "DKGirl", "XINGYAN", "XIAOYU",
|
||||
"YOUMI", "FAIRYCLUB", "FEILIN", "CANDY", "YouWu", "UXING",
|
||||
"RuiSG", "SIW", "SLADY", "SSA", "SJA", "TASHA", "Ugirls",
|
||||
"VGIRLS", "VGIRL", "WING", "YALAYI", "ISHOW",
|
||||
];
|
||||
|
||||
lazy_static! {
|
||||
/// 编译后的正则
|
||||
static ref RE_COSPLAY: Regex = Regex::new(
|
||||
@@ -110,19 +47,15 @@ lazy_static! {
|
||||
r"(?i)(私房|写真|内衣|比基尼|泳装|旗袍|制服|街拍|室内|户外|浴室|浴室|浴缸|床上|床照|内衣|透视|包臀裙|紧身裙|超短裙|丝袜|美腿|美臀|美胸|翘臀|性感|清纯|甜美|气质|可爱|女神|嫩模|模特)"
|
||||
).unwrap();
|
||||
|
||||
/// 停用词集合
|
||||
static ref STOP_WORDS_SET: HashSet<&'static str> = {
|
||||
let mut s = HashSet::new();
|
||||
for w in STOP_WORDS { s.insert(*w); }
|
||||
s
|
||||
/// 停用词集合(从 tag_dict 动态获取)
|
||||
static ref STOP_WORDS_SET: HashSet<String> = {
|
||||
tag_dict::get_tag_dict().get_stop_words().clone()
|
||||
};
|
||||
|
||||
/// 排除名集合(小写)
|
||||
/// 排除名集合(从 tag_dict 动态获取)
|
||||
static ref EXCLUDED_LOWER: HashSet<String> = {
|
||||
let mut s = HashSet::new();
|
||||
for n in EXCLUDED_NAMES { s.insert(n.to_lowercase()); }
|
||||
s.extend(["youmi", "huayang", "bluecake", "mistar", "xiuren", "秀人"].map(|s| s.to_string()));
|
||||
s
|
||||
let dict = tag_dict::get_tag_dict();
|
||||
dict.get_excluded_persons().clone()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -162,8 +95,8 @@ pub fn extract_person_name_from_title(title: &str) -> Option<String> {
|
||||
// 步骤3: 移除 [工作室] 前缀
|
||||
t = RE_BRACKET_PREFIX.replace(&t, "").trim().to_string();
|
||||
|
||||
// 步骤4: 移除模特称呼前缀
|
||||
for prefix in MODEL_TITLE_PREFIX {
|
||||
// 步骤4: 移除模特称呼前缀(从词典动态获取)
|
||||
for prefix in tag_dict::get_tag_dict().get_person_prefixes() {
|
||||
if t.starts_with(prefix) {
|
||||
t = t[prefix.len()..].trim().to_string();
|
||||
break;
|
||||
@@ -218,7 +151,7 @@ pub fn extract_person_name_from_title(title: &str) -> Option<String> {
|
||||
for cap in RE_EN_NAME.captures_iter(&t) {
|
||||
let name = clean_name(&cap[1]);
|
||||
// 排除停用词
|
||||
if STOP_WORDS_SET.contains(&name.to_lowercase().as_str()) { continue; }
|
||||
if STOP_WORDS_SET.contains(&name.to_lowercase()) { continue; }
|
||||
if is_likely_person_name(&name) {
|
||||
return Some(name);
|
||||
}
|
||||
@@ -276,30 +209,46 @@ fn extract_name_from_segment(segment: &str) -> Option<String> {
|
||||
|
||||
// 策略1: 纯中文片段(2-6字)
|
||||
if is_pure_cn && seg.len() >= 2 && seg.len() <= 6 {
|
||||
return Some(clean_name(seg));
|
||||
// 标签词典中的词不应作为人名
|
||||
if !tag_dict::get_tag_dict().classify(seg).is_some()
|
||||
&& !tag_dict::get_tag_dict().classify(&seg.to_lowercase()).is_some() {
|
||||
return Some(clean_name(seg));
|
||||
}
|
||||
}
|
||||
|
||||
// 策略2: 尝试匹配纯中文人名模式
|
||||
if let Some(cap) = RE_CN_NAME.captures(seg) {
|
||||
let name = clean_name(&cap[1]);
|
||||
if name.len() >= 2 {
|
||||
return Some(name);
|
||||
// 标签词典中的词不应作为人名
|
||||
if !tag_dict::get_tag_dict().classify(&name).is_some()
|
||||
&& !tag_dict::get_tag_dict().classify(&name.to_lowercase()).is_some() {
|
||||
return Some(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 策略3: 纯英文片段(2-15字)
|
||||
if is_pure_en && seg.len() >= 2 && seg.len() <= 15 {
|
||||
// 过滤停用词
|
||||
if !STOP_WORDS_SET.contains(&seg.to_lowercase().as_str()) {
|
||||
return Some(clean_name(seg));
|
||||
if !STOP_WORDS_SET.contains(&seg.to_lowercase()) {
|
||||
// 标签词典中的词不应作为人名
|
||||
if !tag_dict::get_tag_dict().classify(seg).is_some()
|
||||
&& !tag_dict::get_tag_dict().classify(&seg.to_lowercase()).is_some() {
|
||||
return Some(clean_name(seg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 策略4: 尝试匹配纯英文人名模式
|
||||
if let Some(cap) = RE_EN_NAME.captures(seg) {
|
||||
let name = clean_name(&cap[1]);
|
||||
if name.len() >= 2 && !STOP_WORDS_SET.contains(&name.to_lowercase().as_str()) {
|
||||
return Some(name);
|
||||
if name.len() >= 2 && !STOP_WORDS_SET.contains(&name.to_lowercase()) {
|
||||
// 标签词典中的词不应作为人名
|
||||
if !tag_dict::get_tag_dict().classify(&name).is_some()
|
||||
&& !tag_dict::get_tag_dict().classify(&name.to_lowercase()).is_some() {
|
||||
return Some(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,6 +326,11 @@ pub fn is_likely_person_name(name: &str) -> bool {
|
||||
// 全部是大写字母且长度 > 6 的排除(通常是缩写/工作室名)
|
||||
if name.chars().all(|c| c.is_ascii_uppercase()) && name.len() > 6 { return false; }
|
||||
|
||||
// 标签词典中的词不应作为人名(tags 永远不出现在 person 中)
|
||||
if tag_dict::get_tag_dict().classify(name).is_some() { return false; }
|
||||
// 也检查小写版本
|
||||
if tag_dict::get_tag_dict().classify(&name.to_lowercase()).is_some() { return false; }
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -389,8 +343,8 @@ pub fn clean_person_name(raw_name: &str) -> (String, Vec<(String, String)>) {
|
||||
let mut aliases = Vec::new();
|
||||
let mut name = raw_name.to_string();
|
||||
|
||||
// 1. 移除前缀
|
||||
for prefix in MODEL_TITLE_PREFIX {
|
||||
// 1. 移除前缀(从词典动态获取)
|
||||
for prefix in tag_dict::get_tag_dict().get_person_prefixes() {
|
||||
if name.starts_with(prefix) {
|
||||
aliases.push(("prefix".to_string(), prefix.to_string()));
|
||||
name = name[prefix.len()..].trim().to_string();
|
||||
@@ -557,9 +511,8 @@ fn extract_person_from_tags(tags: &[jieba_rs::Tag]) -> Option<String> {
|
||||
if next_tag == "eng" ||
|
||||
(next_word.chars().all(|c| c.is_ascii_alphabetic()) && next_word.len() >= 2 && next_word.len() <= 15) {
|
||||
let lower = next_word.to_lowercase();
|
||||
// 跳过停用词(如 photos、cosplay 等停在停用词之前)
|
||||
let stop_words: std::collections::HashSet<&str> = STOP_WORDS.iter().copied().collect();
|
||||
if !stop_words.contains(lower.as_str()) && !EXCLUDED_LOWER.contains(&lower) {
|
||||
// 跳过停用词(从词典动态获取)
|
||||
if !tag_dict::get_tag_dict().is_stop_word(&lower) && !EXCLUDED_LOWER.contains(&lower) {
|
||||
full_name.push_str(next_word);
|
||||
j += 1;
|
||||
continue;
|
||||
@@ -599,8 +552,8 @@ fn extract_tags_from_jieba_result(
|
||||
let mut result = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
// 停用词
|
||||
let stop_words: HashSet<&str> = STOP_WORDS.iter().copied().collect();
|
||||
// 停用词(从词典动态获取)
|
||||
let stop_words = tag_dict::get_tag_dict().get_stop_words().clone();
|
||||
|
||||
// 人名排除
|
||||
let person_exclude: Vec<String> = person_name
|
||||
@@ -713,7 +666,7 @@ pub fn extract_tags_from_title_legacy(title: &str, person_name: Option<&str>) ->
|
||||
if word.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
if STOP_WORDS_SET.contains(word.as_str()) {
|
||||
if STOP_WORDS_SET.contains(word) {
|
||||
continue;
|
||||
}
|
||||
if word.chars().all(|c| c.is_ascii_digit() || c == '.') {
|
||||
|
||||
Reference in New Issue
Block a user