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..dac6b73 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 ai_slop_detection 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..2b0764c 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,14 @@ pub async fn check_ai_slop( .is_track_ai(&mut app.redis_conn().await?, track) .await?; + if let Err(err) = UserService::increase_stats_query(state.user_id()) + .ai_slop(ai_detection_result.provider.as_ref()) + .exec(app.db()) + .await + { + tracing::error!(error = ?err, "Failed to increase ai_slop metric"); + } + 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..053b678 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_checks = 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_checks} + • 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, );