2026-08-10 00:22:20 +01:00
//! Local, offline-first music embeddings and exact cosine search.
//!
//! SQLite owns the durable vectors. The in-memory index is deliberately
//! replaceable: it is rebuilt for the active profile and never becomes a
//! second source of truth.
use std ::collections ::{ HashMap , VecDeque };
use std ::fs ::File ;
use std ::path ::{ Path , PathBuf };
2026-08-10 20:18:51 +01:00
use std ::sync ::atomic ::{ AtomicBool , AtomicU64 , AtomicUsize , Ordering };
2026-08-10 00:22:20 +01:00
use std ::sync ::{ Arc , Mutex , RwLock };
use std ::time ::Instant ;
use anyhow ::{ Context as _ , Result };
use futures_util ::StreamExt as _ ;
use rodio ::{ Decoder , Source as _ };
use rustfft ::FftPlanner ;
use rustfft ::num_complex ::Complex ;
use sha2 ::{ Digest as _ , Sha256 };
use tokio ::io ::AsyncWriteExt as _ ;
use tract_onnx ::prelude ::* ;
use tract_onnx ::tract_core ::dims ;
use crate ::app ::event ::AppEvent ;
use crate ::config ::settings ::SimilaritySettings ;
use crate ::library ::models ::TrackItem ;
use crate ::library ::{ Library , StoredEmbedding };
const SAMPLE_RATE : usize = 16_000 ;
const FRAME_SIZE : usize = 512 ;
const HOP_SIZE : usize = 256 ;
const MEL_BANDS : usize = 96 ;
const PATCH_FRAMES : usize = 128 ;
const PATCH_HOP : usize = 62 ;
const EMBEDDING_DIMENSIONS : usize = 1280 ;
const MODEL_BATCH : usize = 8 ;
const MAX_MODEL_BYTES : usize = 64 * 1024 * 1024 ;
const RESULT_LIMIT : usize = 50 ;
const MAX_PER_ARTIST : usize = 3 ;
const NEAR_DUPLICATE_COSINE : f32 = 0.995 ;
const FULL_TRACK_MAX_SECONDS : u32 = 5 * 60 ;
const LONG_TRACK_WINDOW_SECONDS : u32 = 60 ;
const LONG_TRACK_WINDOWS : usize = 3 ;
pub const DEFAULT_MODEL_ID : & str = "discogs-effnet-bsdynamic-1" ;
pub const DEFAULT_PROFILE_ID : & str = "furumi-full-track-v1" ;
#[derive(Debug, Clone, Copy)]
pub struct ProfileSpec {
pub id : & 'static str ,
pub title : & 'static str ,
}
pub const PROFILES : & [ ProfileSpec ] = & [ ProfileSpec {
id : DEFAULT_PROFILE_ID ,
title : "Full track / balanced long track" ,
}];
#[derive(Debug, Clone, Copy)]
pub struct ModelSpec {
pub id : & 'static str ,
pub version : & 'static str ,
pub filename : & 'static str ,
pub url : & 'static str ,
pub sha256 : & 'static str ,
pub dimensions : usize ,
pub license : & 'static str ,
}
pub const MODELS : & [ ModelSpec ] = & [ ModelSpec {
id : DEFAULT_MODEL_ID ,
version : "1" ,
filename : "discogs-effnet-bsdynamic-1.onnx" ,
url : "https://essentia.upf.edu/models/feature-extractors/discogs-effnet/discogs-effnet-bsdynamic-1.onnx" ,
sha256 : "a280825b334797cf677939db8cd5762c0392aedd0ca6415dbc1cd083f045e43c" ,
dimensions : EMBEDDING_DIMENSIONS ,
license : "CC BY-NC-SA 4.0 (or proprietary from MTG)" ,
}];
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Phase {
#[default]
Disabled ,
Downloading ,
Loading ,
Processing ,
Ready ,
Error ,
}
impl Phase {
pub fn label ( self ) -> & 'static str {
match self {
Self ::Disabled => "disabled" ,
Self ::Downloading => "downloading model" ,
Self ::Loading => "loading model" ,
Self ::Processing => "processing" ,
Self ::Ready => "ready" ,
Self ::Error => "error" ,
}
}
}
2026-08-10 20:18:51 +01:00
#[derive(Debug, Clone, Default, PartialEq, Eq)]
2026-08-10 00:22:20 +01:00
pub struct SimilarityStatus {
pub phase : Phase ,
pub active_profile : Option < String > ,
pub target_profile : Option < String > ,
pub model : String ,
pub total_tracks : usize ,
pub completed_tracks : usize ,
pub failed_tracks : usize ,
pub stored_vectors : usize ,
pub stored_bytes : u64 ,
pub current_track : Option < String > ,
pub last_error : Option < String > ,
}
#[derive(Debug, Clone)]
pub struct QueryVector {
pub profile_id : String ,
pub vector : Vec < f32 > ,
pub source_content_id : Option < String > ,
}
#[derive(Debug, Clone)]
pub struct SimilarTrack {
pub track : TrackItem ,
pub score : f32 ,
pub embedding_signature : [ u8 ; music_dht ::similarity ::SIMILARITY_SIGNATURE_BYTES ],
}
#[derive(Default)]
struct Index {
profile_id : Option < String > ,
entries : Vec < StoredEmbedding > ,
}
type RunnableModel = Arc < TypedRunnableModel > ;
pub struct Manager {
library : Arc < Library > ,
event_tx : tokio ::sync ::mpsc ::UnboundedSender < AppEvent > ,
settings : Mutex < SimilaritySettings > ,
workers : AtomicUsize ,
generation : AtomicU64 ,
2026-08-10 20:18:51 +01:00
pipeline_running : AtomicBool ,
rescan_requested : AtomicBool ,
2026-08-10 00:22:20 +01:00
status : Mutex < SimilarityStatus > ,
index : RwLock < Index > ,
model : Mutex < Option < ( String , RunnableModel ) >> ,
model_dir : PathBuf ,
}
impl Manager {
pub fn new (
library : Arc < Library > ,
event_tx : tokio ::sync ::mpsc ::UnboundedSender < AppEvent > ,
settings : SimilaritySettings ,
) -> Arc < Self > {
let model_dir = crate ::config ::project_dirs ()
. map ( | dirs | dirs . cache_dir (). join ( "similarity-models" ))
. unwrap_or_else ( || PathBuf ::from ( "similarity-models" ));
let mut index = Index ::default ();
if let Some ( profile_id ) = settings . active_profile . as_deref () {
match library . load_similarity_index ( profile_id ) {
Ok ( entries ) => {
index . profile_id = Some ( profile_id . to_string ());
index . entries = entries ;
}
Err ( err ) => tracing ::warn! ( % err , "similarity index restore failed" ),
}
}
2026-08-10 20:18:51 +01:00
let target_profile = model_by_id ( & settings . model )
. filter ( | _ | profile_by_id ( & settings . profile ). is_some ())
. map ( | model | profile_fingerprint ( model , & settings . profile ));
let restored_profile_is_current = index . profile_id == target_profile ;
2026-08-10 00:22:20 +01:00
let status = SimilarityStatus {
2026-08-10 20:18:51 +01:00
phase : if ! settings . enabled {
2026-08-10 00:22:20 +01:00
Phase ::Disabled
2026-08-10 20:18:51 +01:00
} else if restored_profile_is_current {
Phase ::Ready
} else {
Phase ::Loading
2026-08-10 00:22:20 +01:00
},
active_profile : index . profile_id . clone (),
2026-08-10 20:18:51 +01:00
target_profile ,
2026-08-10 00:22:20 +01:00
model : settings . model . clone (),
.. SimilarityStatus ::default ()
};
Arc ::new ( Self {
library ,
event_tx ,
workers : AtomicUsize ::new ( settings . workers . clamp ( 1 , 16 )),
generation : AtomicU64 ::new ( 0 ),
2026-08-10 20:18:51 +01:00
pipeline_running : AtomicBool ::new ( false ),
rescan_requested : AtomicBool ::new ( false ),
2026-08-10 00:22:20 +01:00
settings : Mutex ::new ( settings ),
status : Mutex ::new ( status ),
index : RwLock ::new ( index ),
model : Mutex ::new ( None ),
model_dir ,
})
}
pub fn settings ( & self ) -> SimilaritySettings {
lock ( & self . settings ). clone ()
}
pub fn status ( & self ) -> SimilarityStatus {
lock ( & self . status ). clone ()
}
pub fn network_allowed ( & self ) -> bool {
let settings = lock ( & self . settings );
settings . enabled && settings . federation_consent
}
pub fn apply ( self : & Arc < Self > , settings : SimilaritySettings ) {
self . workers
. store ( settings . workers . clamp ( 1 , 16 ), Ordering ::Release );
let previous = std ::mem ::replace ( & mut * lock ( & self . settings ), settings . clone ());
if ! settings . enabled {
self . generation . fetch_add ( 1 , Ordering ::AcqRel );
self . update_status ( | status | {
status . phase = Phase ::Disabled ;
status . current_track = None ;
status . target_profile = None ;
status . last_error = None ;
});
return ;
}
if ! previous . enabled
|| previous . model != settings . model
|| previous . profile != settings . profile
{
2026-08-10 20:18:51 +01:00
self . generation . fetch_add ( 1 , Ordering ::AcqRel );
2026-08-10 00:22:20 +01:00
self . start ();
}
}
2026-08-10 20:18:51 +01:00
/// Requests a scan without cancelling useful work already in progress.
/// Bursts of library-change notifications collapse into one follow-up
/// pass, so metadata refreshes cannot repeatedly restart the model.
2026-08-10 00:22:20 +01:00
pub fn start ( self : & Arc < Self > ) {
2026-08-10 20:18:51 +01:00
self . rescan_requested . store ( true , Ordering ::Release );
if self . pipeline_running . swap ( true , Ordering ::AcqRel ) {
return ;
}
2026-08-10 00:22:20 +01:00
let this = Arc ::clone ( self );
tokio ::spawn ( async move {
2026-08-10 20:18:51 +01:00
loop {
// This pass covers every notification received before it
// starts. A notification during the pass requests one more.
this . rescan_requested . store ( false , Ordering ::Release );
let generation = this . generation . load ( Ordering ::Acquire );
if let Err ( err ) = this . run_pipeline ( generation ). await
&& this . generation . load ( Ordering ::Acquire ) == generation
{
tracing ::error! ( % err , "similarity pipeline failed" );
this . update_status ( | status | {
status . phase = Phase ::Error ;
status . current_track = None ;
status . last_error = Some ( format! ( " {err:#} " ));
});
}
if this . rescan_requested . load ( Ordering ::Acquire ) {
continue ;
}
this . pipeline_running . store ( false , Ordering ::Release );
// Close the small race between checking the request flag and
// releasing ownership of the worker. If another worker has
// already claimed it, that worker owns the pending pass.
if this . rescan_requested . swap ( false , Ordering ::AcqRel )
&& ! this . pipeline_running . swap ( true , Ordering ::AcqRel )
{
continue ;
}
break ;
2026-08-10 00:22:20 +01:00
}
});
}
pub fn clear ( self : & Arc < Self > ) {
self . generation . fetch_add ( 1 , Ordering ::AcqRel );
let this = Arc ::clone ( self );
tokio ::spawn ( async move {
let library = Arc ::clone ( & this . library );
let result = tokio ::task ::spawn_blocking ( move || library . clear_similarity_embeddings ())
. await
. context ( "embedding clear task failed" )
. and_then ( | result | result );
match result {
Ok (()) => {
* write ( & this . index ) = Index ::default ();
lock ( & this . settings ). active_profile = None ;
this . update_status ( | status | {
* status = SimilarityStatus {
phase : if this . settings (). enabled {
Phase ::Loading
} else {
Phase ::Disabled
},
model : this . settings (). model ,
.. SimilarityStatus ::default ()
};
});
let _ = this
. event_tx
. send ( AppEvent ::SimilarityProfileActivated ( None ));
if this . settings (). enabled {
this . start ();
}
}
Err ( err ) => this . update_status ( | status | {
status . phase = Phase ::Error ;
status . last_error = Some ( format! ( "clear failed: {err:#} " ));
}),
}
});
}
pub fn query_for_track ( & self , track_id : i64 ) -> Result < QueryVector > {
let profile_id = read ( & self . index )
. profile_id
. clone ()
. context ( "no similarity profile is ready yet" ) ? ;
let vector = self
. library
. similarity_embedding ( track_id , & profile_id ) ?
. context ( "this track has not been processed yet" ) ? ;
let source_track = self . library . tracks_by_ids ( & [ track_id ]) ? . into_iter (). next ();
let source_content_id = source_track . as_ref (). and_then ( | track | {
track
. content_id
. clone ()
. or_else ( || crate ::library ::audio_content_id ( & track . file_path ))
});
Ok ( QueryVector {
profile_id ,
vector ,
source_content_id ,
})
}
pub fn search_track (
& self ,
track_id : i64 ,
limit : usize ,
) -> Result < ( Vec < SimilarTrack > , QueryVector ) > {
let query = self . query_for_track ( track_id ) ? ;
let matches = self . search_vector (
& query . profile_id ,
& query . vector ,
Some ( track_id ),
query . source_content_id . as_deref (),
limit ,
) ? ;
Ok (( matches , query ))
}
pub fn search_vector (
& self ,
profile_id : & str ,
vector : & [ f32 ],
exclude_track_id : Option < i64 > ,
exclude_content_id : Option <& str > ,
limit : usize ,
) -> Result < Vec < SimilarTrack >> {
anyhow ::ensure! (
! vector . is_empty () && vector . len () <= 4096 ,
"wrong embedding dimensions"
);
anyhow ::ensure! (
vector . iter (). all ( | value | value . is_finite ()),
"invalid embedding"
);
let index = read ( & self . index );
anyhow ::ensure! (
index . profile_id . as_deref () == Some ( profile_id ),
"the requested similarity profile is not active"
);
let mut scores : Vec < ( i64 , f32 , & str , & [ f32 ]) > = index
. entries
. iter ()
. filter ( | entry | {
Some ( entry . track_id ) != exclude_track_id && entry . vector . len () == vector . len ()
})
. map ( | entry | {
(
entry . track_id ,
dot ( vector , & entry . vector ),
entry . artist_key . as_str (),
entry . vector . as_slice (),
)
})
. filter ( | ( _ , score , _ , _ ) | score . is_finite ())
. collect ();
scores . sort_by ( | left , right | right . 1. total_cmp ( & left . 1 ));
// Pull a wider candidate set, then cap each primary artist so a large
// discography cannot fill the whole result page.
let mut artist_counts : HashMap < String , usize > = HashMap ::new ();
let mut kept_vectors = vec! [ vector ];
let mut selected = Vec ::new ();
for ( track_id , score , artist , candidate_vector ) in scores {
if is_near_duplicate ( candidate_vector , & kept_vectors ) {
continue ;
}
let count = artist_counts . entry ( artist . to_string ()). or_default ();
if ! artist . is_empty () && * count >= MAX_PER_ARTIST {
continue ;
}
* count += 1 ;
let embedding_signature = music_dht ::similarity ::embedding_signature ( candidate_vector ) ? ;
kept_vectors . push ( candidate_vector );
selected . push (( track_id , score , embedding_signature ));
if selected . len () >= limit . min ( RESULT_LIMIT ) {
break ;
}
}
drop ( index );
let ids : Vec < i64 > = selected . iter (). map ( | ( id , _ , _ ) | * id ). collect ();
let tracks = self . library . tracks_by_ids ( & ids ) ? ;
let by_id : HashMap < i64 , TrackItem > =
tracks . into_iter (). map ( | track | ( track . id , track )). collect ();
Ok ( selected
. into_iter ()
. filter_map ( | ( id , score , signature ) | {
by_id
. get ( & id )
. cloned ()
. map ( | track | ( track , score , signature ))
})
. filter ( | ( track , _ , _ ) | {
! exclude_content_id
. is_some_and ( | source | track . content_id . as_deref () == Some ( source ))
})
. map ( | ( track , score , embedding_signature ) | SimilarTrack {
track ,
score ,
embedding_signature ,
})
. collect ())
}
async fn run_pipeline ( self : & Arc < Self > , generation : u64 ) -> Result < () > {
let settings = self . settings ();
if ! settings . enabled {
return Ok (());
}
let spec = model_by_id ( & settings . model )
. with_context ( || format! ( "unknown similarity model ' {} '" , settings . model )) ? ;
anyhow ::ensure! (
profile_by_id ( & settings . profile ). is_some (),
"unknown preprocessing profile '{}'" ,
settings . profile
);
let profile_id = profile_fingerprint ( spec , & settings . profile );
self . library . ensure_similarity_profile (
& profile_id ,
spec . id ,
spec . version ,
spec . sha256 ,
& settings . profile ,
spec . dimensions ,
) ? ;
let stats = self . library . similarity_storage_stats ( & profile_id ) ? ;
self . update_status ( | status | {
status . target_profile = Some ( profile_id . clone ());
status . model = spec . id . to_string ();
status . total_tracks = stats . total_tracks ;
status . completed_tracks = stats . embedded_tracks ;
status . stored_vectors = stats . stored_vectors ;
status . stored_bytes = stats . stored_bytes ;
status . failed_tracks = 0 ;
status . current_track = None ;
status . last_error = None ;
});
let mut pending : VecDeque < _ > = self . library . pending_similarity_tracks ( & profile_id ) ? . into ();
2026-08-10 20:18:51 +01:00
if pending . is_empty () {
self . ensure_generation ( generation ) ? ;
return self . activate_profile ( profile_id );
}
let model_path = self . ensure_model ( spec , generation ). await ? ;
self . ensure_generation ( generation ) ? ;
let model = self . load_model ( & profile_id , & model_path ). await ? ;
self . ensure_generation ( generation ) ? ;
self . update_status ( | status | status . phase = Phase ::Processing );
2026-08-10 00:22:20 +01:00
let mut jobs = tokio ::task ::JoinSet ::new ();
while ! pending . is_empty () || ! jobs . is_empty () {
self . ensure_generation ( generation ) ? ;
let workers = self . workers . load ( Ordering ::Acquire ). clamp ( 1 , 16 );
while jobs . len () < workers {
let Some ( track ) = pending . pop_front () else {
break ;
};
self . update_status ( | status | status . current_track = Some ( track . title . clone ()));
let library = Arc ::clone ( & self . library );
let model = Arc ::clone ( & model );
let profile_id = profile_id . clone ();
jobs . spawn_blocking ( move || {
let started = Instant ::now ();
let result =
embed_track ( & model , Path ::new ( & track . file_path ), track . duration_seconds )
. and_then ( | vector | {
library . store_similarity_embedding ( & track , & profile_id , & vector )
});
( track , result , started . elapsed ())
});
}
let Some ( result ) = jobs . join_next (). await else {
continue ;
};
let ( track , result , elapsed ) = result . context ( "embedding worker panicked" ) ? ;
self . ensure_generation ( generation ) ? ;
match result {
Ok (()) => {
tracing ::info! (
track_id = track . id ,
title = % track . title ,
elapsed_ms = elapsed . as_millis (),
profile = % profile_id ,
"track embedding calculated"
);
self . update_status ( | status | status . completed_tracks += 1 );
}
Err ( err ) => {
tracing ::warn! ( track_id = track . id , title = % track . title , % err , "track embedding failed" );
self . update_status ( | status | {
status . failed_tracks += 1 ;
status . last_error = Some ( format! ( " {} : {err:#} " , track . title ));
});
}
}
}
self . ensure_generation ( generation ) ? ;
2026-08-10 20:18:51 +01:00
self . activate_profile ( profile_id )
}
fn activate_profile ( & self , profile_id : String ) -> Result < () > {
2026-08-10 00:22:20 +01:00
let entries = self . library . load_similarity_index ( & profile_id ) ? ;
let total_tracks = self
. library
. similarity_storage_stats ( & profile_id ) ?
. total_tracks ;
anyhow ::ensure! (
total_tracks == 0 || ! entries . is_empty (),
"no tracks could be processed"
);
* write ( & self . index ) = Index {
profile_id : Some ( profile_id . clone ()),
entries ,
};
2026-08-10 20:18:51 +01:00
let profile_changed = {
let mut settings = lock ( & self . settings );
let changed = settings . active_profile . as_deref () != Some ( & profile_id );
settings . active_profile = Some ( profile_id . clone ());
changed
};
2026-08-10 00:22:20 +01:00
let stats = self . library . similarity_storage_stats ( & profile_id ) ? ;
self . update_status ( | status | {
status . phase = Phase ::Ready ;
status . active_profile = Some ( profile_id . clone ());
status . target_profile = Some ( profile_id . clone ());
status . total_tracks = stats . total_tracks ;
status . completed_tracks = stats . embedded_tracks ;
status . stored_vectors = stats . stored_vectors ;
status . stored_bytes = stats . stored_bytes ;
status . current_track = None ;
});
2026-08-10 20:18:51 +01:00
if profile_changed {
let _ = self
. event_tx
. send ( AppEvent ::SimilarityProfileActivated ( Some ( profile_id )));
}
2026-08-10 00:22:20 +01:00
Ok (())
}
fn ensure_generation ( & self , generation : u64 ) -> Result < () > {
anyhow ::ensure! (
self . generation . load ( Ordering ::Acquire ) == generation ,
"similarity processing superseded by newer settings"
);
Ok (())
}
async fn ensure_model ( & self , spec : & ModelSpec , generation : u64 ) -> Result < PathBuf > {
tokio ::fs ::create_dir_all ( & self . model_dir ). await ? ;
let path = self . model_dir . join ( spec . filename );
if path . exists () {
let verify_path = path . clone ();
let expected = spec . sha256 . to_string ();
let valid = tokio ::task ::spawn_blocking ( move || sha256_file ( & verify_path ))
. await
. context ( "model hash task failed" ) ??
== expected ;
if valid {
return Ok ( path );
}
tokio ::fs ::remove_file ( & path ). await ? ;
}
2026-08-10 20:18:51 +01:00
self . update_status ( | status | status . phase = Phase ::Downloading );
2026-08-10 00:22:20 +01:00
let response = reqwest ::get ( spec . url ). await ? . error_for_status () ? ;
let tmp = path . with_extension ( format! ( "part- {} - {generation} " , std ::process ::id ()));
let mut file = tokio ::fs ::File ::create ( & tmp ). await ? ;
let mut hasher = Sha256 ::new ();
let mut received = 0 usize ;
let mut stream = response . bytes_stream ();
while let Some ( chunk ) = stream . next (). await {
let chunk = chunk ? ;
received = received . saturating_add ( chunk . len ());
anyhow ::ensure! (
received <= MAX_MODEL_BYTES ,
"model download exceeds size limit"
);
hasher . update ( & chunk );
file . write_all ( & chunk ). await ? ;
}
file . flush (). await ? ;
drop ( file );
let actual = format! ( " {:x} " , hasher . finalize ());
if actual != spec . sha256 {
let _ = tokio ::fs ::remove_file ( & tmp ). await ;
anyhow ::bail! ( "downloaded model hash mismatch" );
}
if let Err ( err ) = tokio ::fs ::rename ( & tmp , & path ). await {
// A superseding pipeline may have installed the same verified
// artifact first. This is expected on platforms where rename
// does not replace an existing destination.
if path . exists () {
let _ = tokio ::fs ::remove_file ( & tmp ). await ;
} else {
return Err ( err . into ());
}
}
Ok ( path )
}
async fn load_model ( & self , profile_id : & str , path : & Path ) -> Result < RunnableModel > {
if let Some (( cached_profile , model )) = lock ( & self . model ). as_ref ()
&& cached_profile == profile_id
{
return Ok ( Arc ::clone ( model ));
}
2026-08-10 20:18:51 +01:00
self . update_status ( | status | status . phase = Phase ::Loading );
2026-08-10 00:22:20 +01:00
let path = path . to_path_buf ();
let model = tokio ::task ::spawn_blocking ( move || load_onnx ( & path ))
. await
. context ( "model loading task failed" ) ?? ;
* lock ( & self . model ) = Some (( profile_id . to_string (), Arc ::clone ( & model )));
Ok ( model )
}
fn update_status ( & self , update : impl FnOnce ( & mut SimilarityStatus )) {
let snapshot = {
let mut status = lock ( & self . status );
2026-08-10 20:18:51 +01:00
let previous = status . clone ();
2026-08-10 00:22:20 +01:00
update ( & mut status );
2026-08-10 20:18:51 +01:00
( * status != previous ). then ( || status . clone ())
2026-08-10 00:22:20 +01:00
};
2026-08-10 20:18:51 +01:00
if let Some ( snapshot ) = snapshot {
let _ = self . event_tx . send ( AppEvent ::SimilarityStatus ( snapshot ));
}
2026-08-10 00:22:20 +01:00
}
}
pub fn model_by_id ( id : & str ) -> Option <& 'static ModelSpec > {
MODELS . iter (). find ( | model | model . id == id )
}
pub fn profile_by_id ( id : & str ) -> Option <& 'static ProfileSpec > {
PROFILES . iter (). find ( | profile | profile . id == id )
}
pub fn profile_details ( profile_id : & str , model_id : & str ) -> Option < String > {
let profile = profile_by_id ( profile_id ) ? ;
let dimensions = model_by_id ( model_id )
. map ( | model | model . dimensions . to_string ())
. unwrap_or_else ( || "model-defined" . to_string ());
Some ( format! (
" {} \n\n Track selection: \n • Up to {} seconds: entire track. \n • Longer: {} × {} -second windows (start, middle, end). \n\n Audio: mono, {} Hz; 16-tap windowed-sinc resampling. \n Spectrogram: Hann window; FFT {} , hop {} samples (16 ms). \n Mel: {} Slaney bands, 0– 8 kHz, unit-triangle normalization. \n Compression: log10(1 + 10000 × energy). \n Patches: {} frames (~2.05 s), hop {} frames (~0.99 s). \n Aggregation: mean of patch embeddings, then L2 normalization. \n Output dimensions for selected model: {} . \n\n Compatibility includes the exact model version and SHA-256; peers compare only matching profiles. \n\n Enter / Esc: close" ,
profile . title ,
FULL_TRACK_MAX_SECONDS ,
LONG_TRACK_WINDOWS ,
LONG_TRACK_WINDOW_SECONDS ,
SAMPLE_RATE ,
FRAME_SIZE ,
HOP_SIZE ,
MEL_BANDS ,
PATCH_FRAMES ,
PATCH_HOP ,
dimensions ,
))
}
pub fn profile_fingerprint ( model : & ModelSpec , profile : & str ) -> String {
let contract = format! (
"furumi-similarity-v1 \n model= {} \n version= {} \n sha256= {} \n profile= {} \n input= {} -mono-windowed-sinc16 \n selection=full-to- {} s-else-first-middle-last- {} s \n frame= {} \n hop= {} \n mel=slaney- {} -unit-tri \n patch= {} \n patch-hop= {} \n aggregate=mean-l2 \n dimensions= {} " ,
model . id ,
model . version ,
model . sha256 ,
profile ,
SAMPLE_RATE ,
FULL_TRACK_MAX_SECONDS ,
LONG_TRACK_WINDOW_SECONDS ,
FRAME_SIZE ,
HOP_SIZE ,
MEL_BANDS ,
PATCH_FRAMES ,
PATCH_HOP ,
model . dimensions
);
format! ( "sim1: {} " , blake3 ::hash ( contract . as_bytes ()). to_hex ())
}
fn load_onnx ( path : & Path ) -> Result < RunnableModel > {
let model = tract_onnx ::onnx (). model_for_path ( path ) ? ;
let batch = model . sym ( "batch_size" );
let model = model
. with_input_fact ( 0 , f32 ::fact ( dims! ( batch , PATCH_FRAMES , MEL_BANDS )). into ()) ?
. into_optimized () ?
. into_runnable () ? ;
Ok ( model )
}
fn embed_track ( model : & RunnableModel , path : & Path , duration_seconds : f64 ) -> Result < Vec < f32 >> {
let signal = decode_mono_16k ( path , duration_seconds ) ? ;
let mel = mel_spectrogram ( & signal ) ? ;
anyhow ::ensure! (
mel . len () >= PATCH_FRAMES ,
"track is too short for the model"
);
let starts : Vec < usize > = ( 0 ..= mel . len () - PATCH_FRAMES ). step_by ( PATCH_HOP ). collect ();
let mut sum = vec! [ 0.0 f32 ; EMBEDDING_DIMENSIONS ];
let mut count = 0 usize ;
for batch in starts . chunks ( MODEL_BATCH ) {
let mut input = vec! [ 0.0 f32 ; MODEL_BATCH * PATCH_FRAMES * MEL_BANDS ];
for ( batch_index , & start ) in batch . iter (). enumerate () {
let offset = batch_index * PATCH_FRAMES * MEL_BANDS ;
for frame in 0 .. PATCH_FRAMES {
let dst = offset + frame * MEL_BANDS ;
input [ dst .. dst + MEL_BANDS ]. copy_from_slice ( & mel [ start + frame ]);
}
}
let tensor = Tensor ::from_shape ( & [ MODEL_BATCH , PATCH_FRAMES , MEL_BANDS ], & input ) ? ;
let outputs = model . run ( tvec! ( tensor . into_tvalue ())) ? ;
let embedding = outputs
. iter ()
. find ( | output | output . len () == MODEL_BATCH * EMBEDDING_DIMENSIONS )
. context ( "model did not return its 1280-dimensional embedding output" ) ?
. to_plain_array_view ::< f32 > () ? ;
let values = embedding
. as_slice ()
. context ( "model embedding output is not contiguous" ) ? ;
for batch_index in 0 .. batch . len () {
let row = & values
[ batch_index * EMBEDDING_DIMENSIONS .. ( batch_index + 1 ) * EMBEDDING_DIMENSIONS ];
for ( total , value ) in sum . iter_mut (). zip ( row ) {
* total += * value ;
}
count += 1 ;
}
}
anyhow ::ensure! ( count > 0 , "model produced no patches" );
for value in & mut sum {
* value /= count as f32 ;
}
normalize ( & mut sum ) ? ;
Ok ( sum )
}
fn decode_mono_16k ( path : & Path , duration_seconds : f64 ) -> Result < Vec < f32 >> {
if duration_seconds . is_finite () && duration_seconds > f64 ::from ( FULL_TRACK_MAX_SECONDS ) {
let window = f64 ::from ( LONG_TRACK_WINDOW_SECONDS );
let starts = [
0.0 ,
( duration_seconds / 2.0 - window / 2.0 ). max ( 0.0 ),
( duration_seconds - window ). max ( 0.0 ),
];
let mut selected = Vec ::new ();
for start in starts {
selected . extend ( decode_mono_window ( path , start , Some ( window )) ? );
}
anyhow ::ensure! ( ! selected . is_empty (), "decoded track is empty" );
return Ok ( selected );
}
decode_mono_window ( path , 0.0 , None )
}
fn decode_mono_window (
path : & Path ,
start_seconds : f64 ,
length_seconds : Option < f64 > ,
) -> Result < Vec < f32 >> {
let file = File ::open ( path ). with_context ( || format! ( "opening {} " , path . display ())) ? ;
let mut decoder =
Decoder ::try_from ( file ). with_context ( || format! ( "decoding {} " , path . display ())) ? ;
let channels = decoder . channels (). get () as usize ;
let source_rate = decoder . sample_rate (). get () as usize ;
if start_seconds > 0.0 {
decoder
. try_seek ( std ::time ::Duration ::from_secs_f64 ( start_seconds ))
. with_context ( || format! ( "seeking {} " , path . display ())) ? ;
}
let max_samples =
length_seconds . map ( | seconds | ( seconds * source_rate as f64 ). ceil () as usize * channels );
let mut mono = Vec ::new ();
let mut channel_sum = 0.0 f32 ;
let mut channel_index = 0 usize ;
for ( sample_index , sample ) in decoder . enumerate () {
if max_samples . is_some_and ( | limit | sample_index >= limit ) {
break ;
}
channel_sum += sample ;
channel_index += 1 ;
if channel_index == channels {
mono . push ( channel_sum / channels as f32 );
channel_sum = 0.0 ;
channel_index = 0 ;
}
}
anyhow ::ensure! ( ! mono . is_empty (), "decoded track is empty" );
if source_rate == SAMPLE_RATE {
return Ok ( mono );
}
Ok ( resample_sinc ( & mono , source_rate , SAMPLE_RATE ))
}
fn resample_sinc ( input : & [ f32 ], source_rate : usize , target_rate : usize ) -> Vec < f32 > {
if input . len () < 2 || source_rate == 0 {
return input . to_vec ();
}
let output_len = input
. len ()
. saturating_mul ( target_rate )
. checked_div ( source_rate )
. unwrap_or ( 0 )
. max ( 1 );
let ratio = source_rate as f64 / target_rate as f64 ;
let cutoff = ( target_rate as f64 / source_rate as f64 ). min ( 1.0 ) * 0.95 ;
const HALF_TAPS : isize = 8 ;
( 0 .. output_len )
. map ( | index | {
let position = index as f64 * ratio ;
let center = position . floor () as isize ;
let mut value = 0.0 f64 ;
let mut weight_sum = 0.0 f64 ;
for sample_index in center - HALF_TAPS + 1 ..= center + HALF_TAPS {
if sample_index < 0 || sample_index >= input . len () as isize {
continue ;
}
let distance = position - sample_index as f64 ;
let phase = std ::f64 ::consts ::PI * distance * cutoff ;
let sinc = if phase . abs () < 1e-12 {
1.0
} else {
phase . sin () / phase
};
let window_position = distance / HALF_TAPS as f64 ;
let window = if window_position . abs () <= 1.0 {
0.5 + 0.5 * ( std ::f64 ::consts ::PI * window_position ). cos ()
} else {
0.0
};
let weight = cutoff * sinc * window ;
value += input [ sample_index as usize ] as f64 * weight ;
weight_sum += weight ;
}
if weight_sum . abs () < 1e-12 {
input [ center . clamp ( 0 , input . len () as isize - 1 ) as usize ]
} else {
( value / weight_sum ) as f32
}
})
. collect ()
}
fn mel_spectrogram ( signal : & [ f32 ]) -> Result < Vec < [ f32 ; MEL_BANDS ] >> {
let frame_count = 1 + signal
. len ()
. saturating_sub ( FRAME_SIZE / 2 )
. div_ceil ( HOP_SIZE );
let filters = mel_filters ();
let mut planner = FftPlanner ::< f32 > ::new ();
let fft = planner . plan_fft_forward ( FRAME_SIZE );
let mut mel = Vec ::with_capacity ( frame_count );
let mut spectrum = vec! [ Complex ::new ( 0.0 f32 , 0.0 ); FRAME_SIZE ];
for frame_index in 0 .. frame_count {
let start = frame_index as isize * HOP_SIZE as isize - ( FRAME_SIZE / 2 ) as isize ;
for ( index , value ) in spectrum . iter_mut (). enumerate () {
let source = start + index as isize ;
let sample = if source >= 0 {
signal . get ( source as usize ). copied (). unwrap_or ( 0.0 )
} else {
0.0
};
let window = 0.5
- 0.5 * ( 2.0 * std ::f32 ::consts ::PI * index as f32 / ( FRAME_SIZE - 1 ) as f32 ). cos ();
* value = Complex ::new ( sample * window , 0.0 );
}
fft . process ( & mut spectrum );
let powers : Vec < f32 > = spectrum [ ..= FRAME_SIZE / 2 ]
. iter ()
. map ( | value | value . norm_sqr ())
. collect ();
let mut bands = [ 0.0 f32 ; MEL_BANDS ];
for ( band , weights ) in filters . iter (). enumerate () {
let energy : f32 = powers
. iter ()
. zip ( weights )
. map ( | ( power , weight ) | power * weight )
. sum ();
bands [ band ] = ( 1.0 + 10_000.0 * energy . max ( 0.0 )). log10 ();
}
mel . push ( bands );
}
Ok ( mel )
}
fn mel_filters () -> Vec < Vec < f32 >> {
let low = hz_to_mel_slaney ( 0.0 );
let high = hz_to_mel_slaney (( SAMPLE_RATE / 2 ) as f32 );
let points : Vec < f32 > = ( 0 .. MEL_BANDS + 2 )
. map ( | index | mel_to_hz_slaney ( low + ( high - low ) * index as f32 / ( MEL_BANDS + 1 ) as f32 ))
. collect ();
let frequency_scale = ( SAMPLE_RATE as f32 / 2.0 ) / ( FRAME_SIZE / 2 ) as f32 ;
( 0 .. MEL_BANDS )
. map ( | band | {
let left = points [ band ];
let center = points [ band + 1 ];
let right = points [ band + 2 ];
let area = (( center - left ) + ( right - center )) / 2.0 ;
( 0 ..= FRAME_SIZE / 2 )
. map ( | bin | {
let frequency = bin as f32 * frequency_scale ;
let triangle = if frequency < left || frequency > right {
0.0
} else if frequency < center {
( frequency - left ) / ( center - left )
} else {
( right - frequency ) / ( right - center )
};
triangle . max ( 0.0 ) / area
})
. collect ()
})
. collect ()
}
fn hz_to_mel_slaney ( hz : f32 ) -> f32 {
if hz < 1000.0 {
hz / ( 200.0 / 3.0 )
} else {
15.0 + 27.0 * ( hz / 1000.0 ). ln () / 6.4 f32 . ln ()
}
}
fn mel_to_hz_slaney ( mel : f32 ) -> f32 {
if mel < 15.0 {
mel * ( 200.0 / 3.0 )
} else {
1000.0 * ( 6.4 f32 . ln () * ( mel - 15.0 ) / 27.0 ). exp ()
}
}
fn normalize ( vector : & mut [ f32 ]) -> Result < () > {
let norm = vector . iter (). map ( | value | value * value ). sum ::< f32 > (). sqrt ();
anyhow ::ensure! (
norm . is_finite () && norm > f32 ::EPSILON ,
"zero or invalid embedding"
);
for value in vector {
* value /= norm ;
}
Ok (())
}
fn dot ( left : & [ f32 ], right : & [ f32 ]) -> f32 {
left . iter (). zip ( right ). map ( | ( a , b ) | a * b ). sum ()
}
fn is_near_duplicate ( candidate : & [ f32 ], kept : & [ & [ f32 ]]) -> bool {
kept . iter ()
. any ( | existing | dot ( candidate , existing ) >= NEAR_DUPLICATE_COSINE )
}
fn sha256_file ( path : & Path ) -> Result < String > {
use std ::io ::Read as _ ;
let mut file = File ::open ( path ) ? ;
let mut hasher = Sha256 ::new ();
let mut buffer = [ 0 u8 ; 64 * 1024 ];
loop {
let read = file . read ( & mut buffer ) ? ;
if read == 0 {
break ;
}
hasher . update ( & buffer [ .. read ]);
}
Ok ( format! ( " {:x} " , hasher . finalize ()))
}
fn lock < T > ( mutex : & Mutex < T > ) -> std ::sync ::MutexGuard < '_ , T > {
mutex
. lock ()
. unwrap_or_else ( std ::sync ::PoisonError ::into_inner )
}
fn read < T > ( lock : & RwLock < T > ) -> std ::sync ::RwLockReadGuard < '_ , T > {
lock . read ()
. unwrap_or_else ( std ::sync ::PoisonError ::into_inner )
}
fn write < T > ( lock : & RwLock < T > ) -> std ::sync ::RwLockWriteGuard < '_ , T > {
lock . write ()
. unwrap_or_else ( std ::sync ::PoisonError ::into_inner )
}
#[cfg(test)]
mod tests {
use super ::* ;
2026-08-10 20:18:51 +01:00
fn unique_test_dir ( label : & str ) -> PathBuf {
let unique = std ::time ::SystemTime ::now ()
. duration_since ( std ::time ::UNIX_EPOCH )
. unwrap ()
. as_nanos ();
std ::env ::temp_dir (). join ( format! ( "furumi- {label} - {} - {unique} " , std ::process ::id ()))
}
2026-08-10 00:22:20 +01:00
#[test]
fn profile_fingerprint_changes_with_contract () {
let model = & MODELS [ 0 ];
let first = profile_fingerprint ( model , DEFAULT_PROFILE_ID );
let second = profile_fingerprint ( model , "another-profile" );
assert_ne! ( first , second );
assert_eq! ( first , profile_fingerprint ( model , DEFAULT_PROFILE_ID ));
}
#[test]
fn profile_details_describe_the_processing_contract () {
let details = profile_details ( DEFAULT_PROFILE_ID , DEFAULT_MODEL_ID ). unwrap ();
assert! ( details . contains ( "Up to 300 seconds" ));
assert! ( details . contains ( "16000 Hz" ));
assert! ( details . contains ( "1280" ));
}
#[test]
fn vectors_are_normalized () {
let mut vector = vec! [ 3.0 , 4.0 ];
normalize ( & mut vector ). unwrap ();
assert! (( dot ( & vector , & vector ) - 1.0 ). abs () < 1e-6 );
}
#[test]
fn near_duplicate_embeddings_are_filtered_but_distinct_tracks_remain () {
let query = [ 1.0 , 0.0 , 0.0 ];
let near_duplicate = [ 0.99995 , 0.01 , 0.0 ];
let distinct = [ 0.0 , 1.0 , 0.0 ];
assert! ( is_near_duplicate ( & near_duplicate , & [ & query ]));
assert! ( ! is_near_duplicate ( & distinct , & [ & query ]));
}
2026-08-10 20:18:51 +01:00
#[tokio::test]
async fn repeated_rescans_keep_an_up_to_date_profile_ready () {
let directory = unique_test_dir ( "similarity-stable-status" );
let library = Arc ::new ( Library ::open ( & directory . join ( "library.db" )). unwrap ());
let profile_id = profile_fingerprint ( & MODELS [ 0 ], DEFAULT_PROFILE_ID );
let settings = SimilaritySettings {
enabled : true ,
active_profile : Some ( profile_id ),
.. SimilaritySettings ::default ()
};
let ( event_tx , mut event_rx ) = tokio ::sync ::mpsc ::unbounded_channel ();
let manager = Manager ::new ( Arc ::clone ( & library ), event_tx , settings );
assert_eq! ( manager . status (). phase , Phase ::Ready );
for _ in 0 .. 32 {
manager . start ();
}
tokio ::time ::timeout ( std ::time ::Duration ::from_secs ( 2 ), async {
while manager . pipeline_running . load ( Ordering ::Acquire ) {
tokio ::task ::yield_now (). await ;
}
})
. await
. unwrap ();
assert_eq! ( manager . status (). phase , Phase ::Ready );
while let Ok ( event ) = event_rx . try_recv () {
if let AppEvent ::SimilarityStatus ( status ) = event {
assert_eq! ( status . phase , Phase ::Ready );
}
}
drop ( manager );
drop ( library );
std ::fs ::remove_dir_all ( directory ). unwrap ();
}
2026-08-10 00:22:20 +01:00
#[test]
fn resampling_keeps_a_constant_signal () {
let output = resample_sinc ( & vec! [ 0.25 ; 441 ], 44_100 , 16_000 );
assert_eq! ( output . len (), 160 );
assert! ( output . iter (). all ( | value | ( * value - 0.25 ). abs () < 1e-6 ));
}
/// Manual compatibility check for the separately downloaded model:
/// `FURUMI_TEST_MODEL=/path/model.onnx cargo test onnx_model_smoke -- --ignored`.
#[test]
#[ignore]
fn onnx_model_smoke () {
let path = std ::env ::var_os ( "FURUMI_TEST_MODEL" )
. map ( PathBuf ::from )
. expect ( "set FURUMI_TEST_MODEL" );
let model = load_onnx ( & path ). unwrap ();
let mut input = vec! [ 0.0 f32 ; MODEL_BATCH * PATCH_FRAMES * MEL_BANDS ];
input [ 0 ] = 1.0 ;
let tensor = Tensor ::from_shape ( & [ MODEL_BATCH , PATCH_FRAMES , MEL_BANDS ], & input ). unwrap ();
let outputs = model . run ( tvec! ( tensor . clone (). into_tvalue ())). unwrap ();
assert! (
outputs
. iter ()
. any ( | output | output . len () == MODEL_BATCH * EMBEDDING_DIMENSIONS )
);
let started = Instant ::now ();
for _ in 0 .. 5 {
model . run ( tvec! ( tensor . clone (). into_tvalue ())). unwrap ();
}
eprintln! (
"five warm batch- {MODEL_BATCH} runs: {:.3} s" ,
started . elapsed (). as_secs_f64 ()
);
}
}