fix: filter noisy AI tags
Add an editable AI tag removal list and apply it to WD/JoyTag output before tags are stored. Clean existing generated AI tags during database migration while leaving manual user tags untouched.
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
/// AI-generated tags that are too broad/noisy to be useful in this gallery.
|
||||||
|
/// Edit this list to change what the tagger removes. Manual user tags are not
|
||||||
|
/// affected.
|
||||||
|
pub const AI_TAG_REMOVAL_LIST: &[&str] = &["1girl", "1boy", "no humans", "2girls", "2boys"];
|
||||||
|
|
||||||
|
fn normalize_tag_for_removal(tag: &str) -> String {
|
||||||
|
tag.trim()
|
||||||
|
.chars()
|
||||||
|
.filter(|c| !matches!(c, ' ' | '_' | '-'))
|
||||||
|
.flat_map(char::to_lowercase)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_removed_ai_tag(tag: &str) -> bool {
|
||||||
|
let normalized = normalize_tag_for_removal(tag);
|
||||||
|
AI_TAG_REMOVAL_LIST
|
||||||
|
.iter()
|
||||||
|
.any(|blocked| normalize_tag_for_removal(blocked) == normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn removed_ai_tags_match_common_spellings() {
|
||||||
|
for tag in [
|
||||||
|
"1girl",
|
||||||
|
"1 girl",
|
||||||
|
"1_girl",
|
||||||
|
"1-girl",
|
||||||
|
"NO_HUMANS",
|
||||||
|
"no humans",
|
||||||
|
] {
|
||||||
|
assert!(is_removed_ai_tag(tag), "{tag} should be removed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn removed_ai_tags_do_not_match_unrelated_tags() {
|
||||||
|
for tag in ["girl", "boy", "humans", "solo", "landscape"] {
|
||||||
|
assert!(!is_removed_ai_tag(tag), "{tag} should be kept");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
-1
@@ -1,4 +1,4 @@
|
|||||||
use crate::vector;
|
use crate::{ai_tag_filter, vector};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use r2d2::Pool;
|
use r2d2::Pool;
|
||||||
use r2d2_sqlite::SqliteConnectionManager;
|
use r2d2_sqlite::SqliteConnectionManager;
|
||||||
@@ -378,6 +378,42 @@ pub fn migrate(conn: &Connection) -> Result<()> {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
vector::migrate(conn)?;
|
vector::migrate(conn)?;
|
||||||
|
remove_filtered_ai_tags(conn)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_filtered_ai_tags(conn: &Connection) -> Result<()> {
|
||||||
|
let mut stmt = conn.prepare("SELECT id, tag FROM image_tags WHERE source = 'ai'")?;
|
||||||
|
let filtered_ids = stmt
|
||||||
|
.query_map([], |row| {
|
||||||
|
Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
|
||||||
|
})?
|
||||||
|
.filter_map(|row| match row {
|
||||||
|
Ok((id, tag)) if ai_tag_filter::is_removed_ai_tag(&tag) => Some(Ok(id)),
|
||||||
|
Ok(_) => None,
|
||||||
|
Err(error) => Some(Err(error)),
|
||||||
|
})
|
||||||
|
.collect::<rusqlite::Result<Vec<_>>>()?;
|
||||||
|
drop(stmt);
|
||||||
|
|
||||||
|
if filtered_ids.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let tx = conn.unchecked_transaction()?;
|
||||||
|
{
|
||||||
|
let mut delete_stmt = tx.prepare("DELETE FROM image_tags WHERE id = ?1")?;
|
||||||
|
for id in &filtered_ids {
|
||||||
|
delete_stmt.execute([id])?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tx.execute("DELETE FROM tag_cloud_cache", [])?;
|
||||||
|
tx.commit()?;
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Removed {} filtered AI tag(s) from existing library data",
|
||||||
|
filtered_ids.len()
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod ai_tag_filter;
|
||||||
mod captioner;
|
mod captioner;
|
||||||
mod color;
|
mod color;
|
||||||
mod commands;
|
mod commands;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::ai_tag_filter;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use hf_hub::{api::sync::Api, Repo, RepoType};
|
use hf_hub::{api::sync::Api, Repo, RepoType};
|
||||||
use image::{imageops::FilterType, DynamicImage, ImageReader};
|
use image::{imageops::FilterType, DynamicImage, ImageReader};
|
||||||
@@ -759,6 +760,7 @@ impl WdTagger {
|
|||||||
.filter(|(entry, prob)| {
|
.filter(|(entry, prob)| {
|
||||||
(entry.category == GENERAL_CATEGORY || entry.category == CHARACTER_CATEGORY)
|
(entry.category == GENERAL_CATEGORY || entry.category == CHARACTER_CATEGORY)
|
||||||
&& **prob >= threshold
|
&& **prob >= threshold
|
||||||
|
&& !ai_tag_filter::is_removed_ai_tag(&entry.name)
|
||||||
})
|
})
|
||||||
.map(|(entry, prob)| TagResult {
|
.map(|(entry, prob)| TagResult {
|
||||||
tag: entry.name.clone(),
|
tag: entry.name.clone(),
|
||||||
@@ -1137,9 +1139,11 @@ fn joytag_tags_from_logits(
|
|||||||
.zip(logits.iter())
|
.zip(logits.iter())
|
||||||
.filter_map(|(name, logit)| {
|
.filter_map(|(name, logit)| {
|
||||||
let confidence = sigmoid(*logit);
|
let confidence = sigmoid(*logit);
|
||||||
(confidence >= threshold).then(|| TagResult {
|
(confidence >= threshold && !ai_tag_filter::is_removed_ai_tag(name)).then(|| {
|
||||||
tag: name.clone(),
|
TagResult {
|
||||||
confidence,
|
tag: name.clone(),
|
||||||
|
confidence,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
Reference in New Issue
Block a user