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,
);