From bb9c983d367ca49986c9a150904331ce44b51c26 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 16 Mar 2026 19:27:17 +0300 Subject: [PATCH 1/3] Add AI slop detection statistics tracking Track AI slop detection results per provider (Spotify AI Blocker, Soul Over AI, SHLabs, Human Made) with database counters, metrics collection for both InfluxDB and Prometheus, and display in user and global statistics views. --- locales/actions.yml | 2 + ...8152848_add_ai_slop_detection_counters.sql | 5 ++ src/entity/user.rs | 36 +++++++++++++ src/metrics/influx_collector.rs | 21 ++++++++ src/metrics/prometheus.rs | 9 ++++ src/metrics/prometheus_collector.rs | 25 +++++++++ src/queue/track_check.rs | 7 ++- src/services/user.rs | 51 ++++++++++++++++++- src/telegram/actions/admin_users/details.rs | 16 ++++++ src/telegram/actions/global_stats.rs | 20 ++++++++ src/telegram/actions/stats.rs | 6 +++ 11 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 migrations/20260308152848_add_ai_slop_detection_counters.sql diff --git a/locales/actions.yml b/locales/actions.yml index a1e533e..2ece105 100644 --- a/locales/actions.yml +++ b/locales/actions.yml @@ -18,6 +18,7 @@ actions.stats: 🔍 Analyzed lyrics %{lyrics_analyzed} times 🙈 You ignored %{ignored} track lyrics 🤬 %{lyrics_profane} lyrics were considered profane + 💩 %{ai_slop_total} tracks were detected as AI Slop
Languages stats: %{languages}
@@ -32,6 +33,7 @@ actions.stats: 🔍 Проанализировано текстов %{lyrics_analyzed} раз 🙈 Вы проигнорировали %{ignored} текстов треков 🤬 %{lyrics_profane} текстов были признаны нецензурными + 💩 %{ai_slop_total} треков были определены как ИИ шлак
Статистика по языкам (названия на английском): %{languages}
diff --git a/migrations/20260308152848_add_ai_slop_detection_counters.sql b/migrations/20260308152848_add_ai_slop_detection_counters.sql new file mode 100644 index 0000000..f53c140 --- /dev/null +++ b/migrations/20260308152848_add_ai_slop_detection_counters.sql @@ -0,0 +1,5 @@ +alter table "user" + add column ai_slop_spotify_ai_blocker bigint not null default 0, + add column ai_slop_soul_over_ai bigint not null default 0, + add column ai_slop_shlabs bigint not null default 0, + add column ai_slop_human_made bigint not null default 0; diff --git a/src/entity/user.rs b/src/entity/user.rs index d05485d..b70ad9e 100644 --- a/src/entity/user.rs +++ b/src/entity/user.rs @@ -22,8 +22,10 @@ pub struct Model { pub name: String, pub locale: Locale, pub role: Role, + pub removed_playlists: i64, pub removed_collection: i64, + pub lyrics_checked: i64, pub lyrics_analyzed: i64, pub lyrics_genius: i64, @@ -31,15 +33,27 @@ pub struct Model { #[sea_orm(enum_name = "LyricsLrcLib")] pub lyrics_lrclib: i64, pub lyrics_profane: i64, + + #[sea_orm(enum_name = "AISlopSpotifyAIBlocker")] + pub ai_slop_spotify_ai_blocker: i64, + #[sea_orm(enum_name = "AISlopSoulOverAI")] + pub ai_slop_soul_over_ai: i64, + #[sea_orm(enum_name = "AISlopSHLabs")] + pub ai_slop_shlabs: i64, + #[sea_orm(enum_name = "AISlopHumanMade")] + pub ai_slop_human_made: i64, + pub status: Status, pub created_at: chrono::NaiveDateTime, pub updated_at: chrono::NaiveDateTime, + pub cfg_check_profanity: bool, pub cfg_skip_tracks: bool, pub cfg_skippage_secs: i64, pub cfg_skippage_enabled: bool, #[sea_orm(enum_name = "CfgAISlopDetection")] pub cfg_ai_slop_detection: AISlopDetection, + pub magic_playlist: Option, pub spotify_state: Uuid, pub ref_code: Option, @@ -70,8 +84,10 @@ pub enum Column { Name, Locale, Role, + RemovedPlaylists, RemovedCollection, + LyricsChecked, LyricsAnalyzed, LyricsGenius, @@ -79,15 +95,27 @@ pub enum Column { #[sea_orm(column_name = "lyrics_lrclib")] LyricsLrcLib, LyricsProfane, + + #[sea_orm(column_name = "ai_slop_spotify_ai_blocker")] + AISlopSpotifyAIBlocker, + #[sea_orm(column_name = "ai_slop_soul_over_ai")] + AISlopSoulOverAI, + #[sea_orm(column_name = "ai_slop_shlabs")] + AISlopSHLabs, + #[sea_orm(column_name = "ai_slop_human_made")] + AISlopHumanMade, + Status, CreatedAt, UpdatedAt, + CfgCheckProfanity, CfgSkipTracks, CfgSkippageSecs, CfgSkippageEnabled, #[sea_orm(column_name = "cfg_ai_slop_detection")] CfgAISlopDetection, + MagicPlaylist, SpotifyState, RefCode, @@ -115,14 +143,22 @@ impl ColumnTrait for Column { Self::Name => ColumnType::Text.def(), Self::Locale => Locale::db_type(), Self::Role => Role::db_type(), + Self::RemovedPlaylists => ColumnType::BigInteger.def(), Self::RemovedCollection => ColumnType::BigInteger.def(), + Self::LyricsChecked => ColumnType::BigInteger.def(), Self::LyricsAnalyzed => ColumnType::BigInteger.def(), Self::LyricsGenius => ColumnType::BigInteger.def(), Self::LyricsMusixmatch => ColumnType::BigInteger.def(), Self::LyricsLrcLib => ColumnType::BigInteger.def(), Self::LyricsProfane => ColumnType::BigInteger.def(), + + Self::AISlopSpotifyAIBlocker => ColumnType::BigInteger.def(), + Self::AISlopSoulOverAI => ColumnType::BigInteger.def(), + Self::AISlopSHLabs => ColumnType::BigInteger.def(), + Self::AISlopHumanMade => ColumnType::BigInteger.def(), + Self::Status => Status::db_type(), Self::CreatedAt => ColumnType::DateTime.def(), Self::UpdatedAt => ColumnType::DateTime.def(), diff --git a/src/metrics/influx_collector.rs b/src/metrics/influx_collector.rs index 5512d86..b177649 100644 --- a/src/metrics/influx_collector.rs +++ b/src/metrics/influx_collector.rs @@ -81,6 +81,15 @@ struct TrackLanguageStats { language: String, } +#[derive(InfluxDbWriteable, Debug)] +struct AISlopDetectionStats { + time: Timestamp, + spotify_ai_blocker: u64, + soul_over_ai: u64, + shlabs: u64, + human_made: u64, +} + #[derive(InfluxDbWriteable, Debug)] struct Uptime { time: Timestamp, @@ -113,6 +122,10 @@ pub async fn collect(client: &InfluxClient, app: &App) -> anyhow::Result<()> { lyrics_musixmatch, lyrics_lrclib, lyrics_analyzed, + ai_slop_spotify_ai_blocker, + ai_slop_soul_over_ai, + ai_slop_shlabs, + ai_slop_human_made, } = UserService::get_stats(app.db(), None).await?; let tick_health_status = utils::tick_health().await; @@ -154,6 +167,14 @@ pub async fn collect(client: &InfluxClient, app: &App) -> anyhow::Result<()> { spotify_429: MetricsService::spotify_429_get(&mut redis_conn).await?, } .into_query("errors"), + AISlopDetectionStats { + time, + spotify_ai_blocker: ai_slop_spotify_ai_blocker as u64, + soul_over_ai: ai_slop_soul_over_ai as u64, + shlabs: ai_slop_shlabs as u64, + human_made: ai_slop_human_made as u64, + } + .into_query("ai_slop_detection"), Uptime::new(time).into_query("uptime"), ]; diff --git a/src/metrics/prometheus.rs b/src/metrics/prometheus.rs index 771a248..c338b8b 100644 --- a/src/metrics/prometheus.rs +++ b/src/metrics/prometheus.rs @@ -33,6 +33,7 @@ pub struct PrometheusMetrics { pub lyrics_found: IntGauge, pub lyrics_profane: IntGauge, pub lyrics_source: IntGaugeVec, + pub ai_slop_detection: IntGaugeVec, pub process_duration: Histogram, pub process_check_interval_seconds: Gauge, pub process_users_checked: IntCounter, @@ -111,6 +112,14 @@ impl PrometheusClient { ) .context("Failed to register lyrics_source metric")?, + ai_slop_detection: register_int_gauge_vec_with_registry!( + "ai_slop_detection_total", + "AI Detection Providers", + &["provider"], + registry + ) + .context("Failed to register lyrics_source metric")?, + process_duration: register_histogram_with_registry!( "process_duration_seconds", "Processing duration in seconds", diff --git a/src/metrics/prometheus_collector.rs b/src/metrics/prometheus_collector.rs index 67638cf..f9e2b35 100644 --- a/src/metrics/prometheus_collector.rs +++ b/src/metrics/prometheus_collector.rs @@ -34,6 +34,10 @@ pub async fn collect(client: &PrometheusClient, app: &App) -> anyhow::Result<()> lyrics_musixmatch, lyrics_lrclib, lyrics_analyzed, + ai_slop_spotify_ai_blocker, + ai_slop_soul_over_ai, + ai_slop_shlabs, + ai_slop_human_made, } = UserService::get_stats(app.db(), None).await?; let tick_health_status = utils::tick_health().await; @@ -87,6 +91,27 @@ pub async fn collect(client: &PrometheusClient, app: &App) -> anyhow::Result<()> .with_label_values(&["lrclib"]) .set(lyrics_lrclib); + client + .metrics() + .ai_slop_detection + .with_label_values(&["soul_over_ai"]) + .set(ai_slop_soul_over_ai); + client + .metrics() + .ai_slop_detection + .with_label_values(&["spotify_ai_blocker"]) + .set(ai_slop_spotify_ai_blocker); + client + .metrics() + .ai_slop_detection + .with_label_values(&["shlabs"]) + .set(ai_slop_shlabs); + client + .metrics() + .ai_slop_detection + .with_label_values(&["human_made"]) + .set(ai_slop_human_made); + client .metrics() .ticks diff --git a/src/queue/track_check.rs b/src/queue/track_check.rs index 0dbb5c3..43b55a0 100644 --- a/src/queue/track_check.rs +++ b/src/queue/track_check.rs @@ -89,7 +89,7 @@ pub async fn consume(data: TrackCheckQueueTask, app: Data<&'static App>) -> anyh .context("Check lyrics failed")?; UserService::increase_stats_query(user_state.user_id()) - .checked_lyrics(res.profane, res.provider) + .checked_lyrics(res.profane, res.provider.as_ref()) .exec(app.db()) .await?; @@ -276,6 +276,11 @@ pub async fn check_ai_slop( .is_track_ai(&mut app.redis_conn().await?, track) .await?; + UserService::increase_stats_query(state.user_id()) + .ai_slop(ai_detection_result.provider.as_ref()) + .exec(app.db()) + .await?; + if !ai_detection_result.prediction.is_track_ai() { return Ok(AISlopCheckResult { is_ai_slop: false, diff --git a/src/services/user.rs b/src/services/user.rs index 4d9e095..8de9c10 100644 --- a/src/services/user.rs +++ b/src/services/user.rs @@ -15,6 +15,7 @@ use sea_orm::{ use crate::entity::prelude::*; use crate::lyrics; +use crate::services::ai_slop_detection; use crate::utils::Clock; pub struct UserStatsIncreaseQueryBuilder(UpdateMany); @@ -46,8 +47,7 @@ impl UserStatsIncreaseQueryBuilder { self } - #[allow(clippy::needless_pass_by_value)] - pub fn checked_lyrics(mut self, profane: bool, provider: Option) -> Self { + pub fn checked_lyrics(mut self, profane: bool, provider: Option<&lyrics::Provider>) -> Self { self.0 = self .0 .col_expr( @@ -71,6 +71,21 @@ impl UserStatsIncreaseQueryBuilder { self } + pub fn ai_slop(mut self, provider: Option<&ai_slop_detection::Provider>) -> Self { + let col = match provider { + Some(ai_slop_detection::Provider::SpotifyAIBlocker) => { + UserColumn::AISlopSpotifyAIBlocker + }, + Some(ai_slop_detection::Provider::SoulOverAI) => UserColumn::AISlopSoulOverAI, + Some(ai_slop_detection::Provider::SHLabs) => UserColumn::AISlopSHLabs, + None => UserColumn::AISlopHumanMade, + }; + + self.0 = self.0.col_expr(col, Expr::col(col).add(1)); + + self + } + pub fn analyzed_lyrics(mut self) -> Self { self.0 = self.0.col_expr( UserColumn::LyricsAnalyzed, @@ -99,6 +114,10 @@ pub struct UserStats { pub lyrics_musixmatch: i64, pub lyrics_lrclib: i64, pub lyrics_analyzed: i64, + pub ai_slop_spotify_ai_blocker: i64, + pub ai_slop_soul_over_ai: i64, + pub ai_slop_shlabs: i64, + pub ai_slop_human_made: i64, } pub struct UserService; @@ -479,6 +498,34 @@ impl UserService { ]), "lyrics_analyzed", ) + .expr_as( + Func::coalesce([ + UserColumn::AISlopSpotifyAIBlocker.sum().cast_as(bigint()), + Expr::val(0).into(), + ]), + "ai_slop_spotify_ai_blocker", + ) + .expr_as( + Func::coalesce([ + UserColumn::AISlopSoulOverAI.sum().cast_as(bigint()), + Expr::val(0).into(), + ]), + "ai_slop_soul_over_ai", + ) + .expr_as( + Func::coalesce([ + UserColumn::AISlopSHLabs.sum().cast_as(bigint()), + Expr::val(0).into(), + ]), + "ai_slop_shlabs", + ) + .expr_as( + Func::coalesce([ + UserColumn::AISlopHumanMade.sum().cast_as(bigint()), + Expr::val(0).into(), + ]), + "ai_slop_human_made", + ) .into_model::() .one(db) .await? diff --git a/src/telegram/actions/admin_users/details.rs b/src/telegram/actions/admin_users/details.rs index 5871fd8..b6c4d76 100644 --- a/src/telegram/actions/admin_users/details.rs +++ b/src/telegram/actions/admin_users/details.rs @@ -118,6 +118,11 @@ async fn format_user_details(app: &'static App, user_id: &str) -> anyhow::Result }) .join("\n"); + let ai_slop_total = stats.ai_slop_spotify_ai_blocker + + stats.ai_slop_soul_over_ai + + stats.ai_slop_shlabs + + stats.ai_slop_human_made; + let text = formatdoc!( r#" 👤 User Details @@ -157,6 +162,13 @@ async fn format_user_details(app: &'static App, user_id: &str) -> anyhow::Result • MusixMatch: {lyrics_musixmatch} • LrcLib: {lyrics_lrclib} + AI Slop Detection: + • Total {ai_slop_total} + • Soul Over AI {ai_slop_soul_over_ai} + • Spotify AI Blocker {ai_slop_spotify_ai_blocker} + • SHLabs {ai_slop_shlabs} + • Human Made {ai_slop_human_made} +
Languages stats: {languages}
"#, @@ -189,6 +201,10 @@ async fn format_user_details(app: &'static App, user_id: &str) -> anyhow::Result lyrics_genius = stats.lyrics_genius, lyrics_musixmatch = stats.lyrics_musixmatch, lyrics_lrclib = stats.lyrics_lrclib, + ai_slop_spotify_ai_blocker = stats.ai_slop_spotify_ai_blocker, + ai_slop_soul_over_ai = stats.ai_slop_soul_over_ai, + ai_slop_shlabs = stats.ai_slop_shlabs, + ai_slop_human_made = stats.ai_slop_human_made, ); Ok(text) diff --git a/src/telegram/actions/global_stats.rs b/src/telegram/actions/global_stats.rs index 2df3602..0a64f53 100644 --- a/src/telegram/actions/global_stats.rs +++ b/src/telegram/actions/global_stats.rs @@ -32,6 +32,10 @@ pub async fn handle( lyrics_musixmatch, lyrics_lrclib, lyrics_analyzed, + ai_slop_spotify_ai_blocker, + ai_slop_soul_over_ai, + ai_slop_shlabs, + ai_slop_human_made, } = UserService::get_stats(app.db(), None).await?; let user_locales = UserService::count_users_locales(app.db()) @@ -46,6 +50,14 @@ pub async fn handle( .collect_vec() .join("\n"); + let ai_slop_total = + ai_slop_spotify_ai_blocker + ai_slop_soul_over_ai + ai_slop_shlabs + ai_slop_human_made; + let ai_slop_spotify_ai_blocker_ratio = + 100.0 * ai_slop_spotify_ai_blocker as f32 / ai_slop_total as f32; + let ai_slop_soul_over_ai_ratio = 100.0 * ai_slop_soul_over_ai as f32 / ai_slop_total as f32; + let ai_slop_shlabs_ratio = 100.0 * ai_slop_shlabs as f32 / ai_slop_total as f32; + let ai_slop_human_made_ratio = 100.0 * ai_slop_human_made as f32 / ai_slop_total as f32; + let lyrics_found_ratio = 100.0 * lyrics_found as f32 / lyrics_checked as f32; let lyrics_genius_ratio = 100.0 * lyrics_genius as f32 / lyrics_found as f32; let lyrics_musixmatch_ratio = 100.0 * lyrics_musixmatch as f32 / lyrics_found as f32; @@ -105,6 +117,14 @@ pub async fn handle( {user_locales} + AI Slop Detection + + • Total {ai_slop_total} + • Soul Over AI {ai_slop_soul_over_ai} ({ai_slop_soul_over_ai_ratio:.2}%) + • Spotify AI Blocker {ai_slop_spotify_ai_blocker} ({ai_slop_spotify_ai_blocker_ratio:.2}%) + • SHLabs {ai_slop_shlabs} ({ai_slop_shlabs_ratio:.2}%) + • Human Made {ai_slop_human_made} ({ai_slop_human_made_ratio:.2}%) +
Languages stats: {languages}
diff --git a/src/telegram/actions/stats.rs b/src/telegram/actions/stats.rs index 7122161..cc297c1 100644 --- a/src/telegram/actions/stats.rs +++ b/src/telegram/actions/stats.rs @@ -38,9 +38,14 @@ pub async fn handle( lyrics_checked, lyrics_profane, lyrics_analyzed, + ai_slop_spotify_ai_blocker, + ai_slop_soul_over_ai, + ai_slop_shlabs, .. } = UserService::get_stats(app.db(), Some(state.user_id())).await?; + let ai_slop_total = ai_slop_spotify_ai_blocker + ai_slop_soul_over_ai + ai_slop_shlabs; + let all_langs = TrackLanguageStatsService::sum_for_user(app.db(), state.user_id()).await?; let languages = TrackLanguageStatsService::stats_for_user(app.db(), state.user_id(), Some(10)) @@ -72,6 +77,7 @@ pub async fn handle( lyrics_analyzed = lyrics_analyzed, ignored = ignored, lyrics_profane = lyrics_profane, + ai_slop_total = ai_slop_total, languages = languages, ); From 5148f81ddb5b7102d1fc600756d15ef8c9c916ee Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 16 Mar 2026 19:45:53 +0300 Subject: [PATCH 2/3] pr fix --- src/metrics/prometheus.rs | 2 +- src/queue/track_check.rs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/metrics/prometheus.rs b/src/metrics/prometheus.rs index c338b8b..dac6b73 100644 --- a/src/metrics/prometheus.rs +++ b/src/metrics/prometheus.rs @@ -118,7 +118,7 @@ impl PrometheusClient { &["provider"], registry ) - .context("Failed to register lyrics_source metric")?, + .context("Failed to register ai_slop_detection metric")?, process_duration: register_histogram_with_registry!( "process_duration_seconds", diff --git a/src/queue/track_check.rs b/src/queue/track_check.rs index 43b55a0..2b0764c 100644 --- a/src/queue/track_check.rs +++ b/src/queue/track_check.rs @@ -276,10 +276,13 @@ pub async fn check_ai_slop( .is_track_ai(&mut app.redis_conn().await?, track) .await?; - UserService::increase_stats_query(state.user_id()) + if let Err(err) = UserService::increase_stats_query(state.user_id()) .ai_slop(ai_detection_result.provider.as_ref()) .exec(app.db()) - .await?; + .await + { + tracing::error!(error = ?err, "Failed to increase ai_slop metric"); + } if !ai_detection_result.prediction.is_track_ai() { return Ok(AISlopCheckResult { From 609c5d1aaed4ef1034484a389cbf3a22f5e195b7 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 16 Mar 2026 21:03:55 +0300 Subject: [PATCH 3/3] renamed to total checks --- src/telegram/actions/admin_users/details.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/telegram/actions/admin_users/details.rs b/src/telegram/actions/admin_users/details.rs index b6c4d76..053b678 100644 --- a/src/telegram/actions/admin_users/details.rs +++ b/src/telegram/actions/admin_users/details.rs @@ -118,7 +118,7 @@ async fn format_user_details(app: &'static App, user_id: &str) -> anyhow::Result }) .join("\n"); - let ai_slop_total = stats.ai_slop_spotify_ai_blocker + let ai_slop_total_checks = stats.ai_slop_spotify_ai_blocker + stats.ai_slop_soul_over_ai + stats.ai_slop_shlabs + stats.ai_slop_human_made; @@ -163,7 +163,7 @@ async fn format_user_details(app: &'static App, user_id: &str) -> anyhow::Result • LrcLib: {lyrics_lrclib} AI Slop Detection: - • Total {ai_slop_total} + • Total {ai_slop_total_checks} • Soul Over AI {ai_slop_soul_over_ai} • Spotify AI Blocker {ai_slop_spotify_ai_blocker} • SHLabs {ai_slop_shlabs}