This commit is contained in:
lychang
2026-07-20 02:47:40 +08:00
parent d958655c1e
commit 8cf4bfe3d0
5 changed files with 393 additions and 282 deletions

View File

@@ -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 == '.') {