Skip to main content

qualia_client_core/wellfair/
perception_catalog.rs

1//! Perception assets catalogue — seed **models** and **ontologies** into the
2//! hypermedia Library (Software / Tools shelves) with honest labels.
3//!
4//! Pattern mirrors `qapp_catalog.rs`. Does **not** claim foundation-model quality
5//! or A4 FEA; seed weights and reference paths are explicitly labelled.
6
7use super::hypermedia_store::{CommonsVisibility, HypermediaStore, LibraryEntry, LibrarySection};
8use serde::{Deserialize, Serialize};
9use std::path::Path;
10
11/// Media type for on-device model weight manifests.
12pub const MODEL_MEDIA_TYPE: &str = "application/x-webizen-model-manifest";
13/// Media type for ontology catalogue rows.
14pub const ONTOLOGY_MEDIA_TYPE: &str = "application/x-webizen-ontology";
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17pub struct ModelCatalogEntry {
18    pub id: &'static str,
19    pub title: &'static str,
20    /// Relative path under `{storage}/models/` when materialised.
21    pub rel_path: &'static str,
22    pub role: &'static str,
23    /// If true, UI must not present as foundation / certified production model.
24    pub is_seed_reference: bool,
25    pub category: &'static str,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub struct OntologyCatalogEntry {
30    pub id: &'static str,
31    pub title: &'static str,
32    pub rel_path: &'static str,
33    pub domain: &'static str,
34    pub licence_note: &'static str,
35}
36
37/// Seed / reference perception models (paths materialised by ensure_*_weights).
38pub const PERCEPTION_MODEL_CATALOG: &[ModelCatalogEntry] = &[
39    ModelCatalogEntry {
40        id: "vision-seed-qvwt",
41        title: "Vision seed QVWT (detector head)",
42        rel_path: "models/vision_seed.qvwt",
43        role: "vision-encoder-detector",
44        is_seed_reference: true,
45        category: "vision",
46    },
47    // MIG-V4 — specialized_libs::computer_vision (algorithm library, not a weight file)
48    ModelCatalogEntry {
49        id: "lib-computer-vision-cv",
50        title: "computer_vision::cv (classical filters/edges/features)",
51        rel_path: "crates/qualia-core-db/src/specialized_libs/computer_vision/cv/",
52        role: "specialized-lib-cv",
53        is_seed_reference: false,
54        category: "vision-library",
55    },
56    ModelCatalogEntry {
57        id: "lib-computer-vision-sr",
58        title: "computer_vision::sr (classical + tiled SR + device policy)",
59        rel_path: "crates/qualia-core-db/src/specialized_libs/computer_vision/sr/",
60        role: "specialized-lib-sr",
61        is_seed_reference: false,
62        category: "vision-library",
63    },
64    ModelCatalogEntry {
65        id: "lib-computer-vision-bio",
66        title: "computer_vision::bio (histo/radiomics/DICOM-lite)",
67        rel_path: "crates/qualia-core-db/src/specialized_libs/computer_vision/bio/",
68        role: "specialized-lib-bio",
69        is_seed_reference: false,
70        category: "vision-library",
71    },
72    ModelCatalogEntry {
73        id: "lib-computer-vision-spatial",
74        title: "computer_vision::spatial (MeshIR export/quality/σ)",
75        rel_path: "crates/qualia-core-db/src/specialized_libs/computer_vision/spatial/",
76        role: "specialized-lib-spatial",
77        is_seed_reference: false,
78        category: "vision-library",
79    },
80    ModelCatalogEntry {
81        id: "audio-seed-aed",
82        title: "Audio seed AED (.qaed)",
83        rel_path: "models/aed_seed.qaed",
84        role: "auditory-event-detection",
85        is_seed_reference: true,
86        category: "audio",
87    },
88    ModelCatalogEntry {
89        id: "audio-seed-speech",
90        title: "Speech seed phones (.qspk)",
91        rel_path: "models/speech_seed.qspk",
92        role: "speech-encoder-phones",
93        is_seed_reference: true,
94        category: "audio",
95    },
96    ModelCatalogEntry {
97        id: "llm-gguf-slot",
98        title: "Local GGUF slot (user-supplied)",
99        rel_path: "models/",
100        role: "llm-gguf",
101        is_seed_reference: false,
102        category: "language",
103    },
104    ModelCatalogEntry {
105        id: "vision-yunet-face",
106        title: "YuNet 2D Face Detector (MIT)",
107        rel_path: "models/vision/face/yunet/face_detection_yunet_2023mar.onnx",
108        role: "vision-face-detector",
109        is_seed_reference: false,
110        category: "vision",
111    },
112    ModelCatalogEntry {
113        id: "vision-sface-embed",
114        title: "SFace Biometric Embeddings (Apache-2.0)",
115        rel_path: "models/vision/face/sface/face_recognition_sface_2021dec.onnx",
116        role: "vision-face-embedding",
117        is_seed_reference: false,
118        category: "vision",
119    },
120    ModelCatalogEntry {
121        id: "vision-mediapipe-facemesh",
122        title: "MediaPipe 468-pt Face Mesh (Apache-2.0)",
123        rel_path: "models/vision/face/mediapipe_landmarker/face_landmarker.task",
124        role: "vision-face-landmarks",
125        is_seed_reference: false,
126        category: "vision",
127    },
128    ModelCatalogEntry {
129        id: "vision-midas-depth",
130        title: "MiDaS 3D Monocular Depth (MIT)",
131        rel_path: "models/vision/depth/midas_v21_small.onnx",
132        role: "vision-monocular-depth",
133        is_seed_reference: false,
134        category: "vision",
135    },
136    ModelCatalogEntry {
137        id: "vision-espcn-sr",
138        title: "ESPCN Image Super-Resolution 2x/4x (Apache-2.0)",
139        rel_path: "models/vision/sr/espcn/espcn_x4.onnx",
140        role: "vision-super-resolution",
141        is_seed_reference: false,
142        category: "vision",
143    },
144    ModelCatalogEntry {
145        id: "vision-yolonas-object",
146        title: "YOLO-NAS Object Detector (Apache-2.0)",
147        rel_path: "models/vision/detect/yolo_nas/yolo_nas_s.onnx",
148        role: "vision-object-detector",
149        is_seed_reference: false,
150        category: "vision",
151    },
152    ModelCatalogEntry {
153        id: "audio-chatterbox-voice",
154        title: "Resemble AI Chatterbox Voice & TTS (Apache-2.0 / MIT)",
155        rel_path: "models/audio/tts/chatterbox/chatterbox.onnx",
156        role: "audio-voice-cloning-tts",
157        is_seed_reference: false,
158        category: "audio",
159    },
160    ModelCatalogEntry {
161        id: "audio-kokoro-82m-tts",
162        title: "Kokoro-82M Ultra-Lightweight TTS (Apache-2.0)",
163        rel_path: "models/audio/tts/kokoro/kokoro_v0_88.onnx",
164        role: "audio-fast-tts",
165        is_seed_reference: false,
166        category: "audio",
167    },
168    ModelCatalogEntry {
169        id: "audio-f5tts-clone",
170        title: "F5-TTS Flow-Matching Voice Cloner (MIT)",
171        rel_path: "models/audio/tts/f5tts/f5tts_small.onnx",
172        role: "audio-voice-cloning",
173        is_seed_reference: false,
174        category: "audio",
175    },
176];
177
178/// Ontologies exposed in Library (subset of `bundled/ontologies` + perception-relevant).
179pub const PERCEPTION_ONTOLOGY_CATALOG: &[OntologyCatalogEntry] = &[
180    OntologyCatalogEntry {
181        id: "shacl",
182        title: "SHACL",
183        rel_path: "bundled/ontologies/shacl.ttl",
184        domain: "validation",
185        licence_note: "W3C SHACL",
186    },
187    OntologyCatalogEntry {
188        id: "ldp",
189        title: "Linked Data Platform (LDP)",
190        rel_path: "bundled/ontologies/w3c-archives/ldp.ttl",
191        domain: "solid",
192        licence_note: "W3C",
193    },
194    OntologyCatalogEntry {
195        id: "acl",
196        title: "Web Access Control (ACL)",
197        rel_path: "bundled/ontologies/w3c-archives/auth-acl.ttl",
198        domain: "solid",
199        licence_note: "W3C / Solid",
200    },
201    OntologyCatalogEntry {
202        id: "solid-terms",
203        title: "Solid Terms",
204        rel_path: "bundled/ontologies/w3c-archives/solid-terms.ttl",
205        domain: "solid",
206        licence_note: "Solid",
207    },
208    OntologyCatalogEntry {
209        id: "solid-oidc",
210        title: "Solid-OIDC",
211        rel_path: "bundled/ontologies/w3c-archives/solid-oidc.ttl",
212        domain: "solid",
213        licence_note: "Solid",
214    },
215    OntologyCatalogEntry {
216        id: "pim-space",
217        title: "PIM Space",
218        rel_path: "bundled/ontologies/w3c-archives/pim-space.ttl",
219        domain: "solid",
220        licence_note: "Solid",
221    },
222    OntologyCatalogEntry {
223        id: "foaf",
224        title: "FOAF",
225        rel_path: "bundled/ontologies/w3c-archives/foaf.ttl",
226        domain: "social",
227        licence_note: "FOAF",
228    },
229    OntologyCatalogEntry {
230        id: "prov",
231        title: "PROV-O",
232        rel_path: "bundled/ontologies/w3c/prov.ttl",
233        domain: "provenance",
234        licence_note: "W3C",
235    },
236    OntologyCatalogEntry {
237        id: "skos",
238        title: "SKOS",
239        rel_path: "bundled/ontologies/w3c/skos.ttl",
240        domain: "vocab",
241        licence_note: "W3C",
242    },
243    OntologyCatalogEntry {
244        id: "dcterms",
245        title: "Dublin Core Terms",
246        rel_path: "bundled/ontologies/purl/dcterms.ttl",
247        domain: "metadata",
248        licence_note: "Dublin Core",
249    },
250    OntologyCatalogEntry {
251        id: "owl",
252        title: "OWL",
253        rel_path: "bundled/ontologies/w3c/owl.ttl",
254        domain: "logic",
255        licence_note: "W3C",
256    },
257    OntologyCatalogEntry {
258        id: "rdfs",
259        title: "RDFS",
260        rel_path: "bundled/ontologies/w3c/rdfs.ttl",
261        domain: "logic",
262        licence_note: "W3C",
263    },
264    OntologyCatalogEntry {
265        id: "time",
266        title: "OWL-Time",
267        rel_path: "bundled/ontologies/w3c/time.ttl",
268        domain: "temporal",
269        licence_note: "W3C",
270    },
271    OntologyCatalogEntry {
272        id: "sosa",
273        title: "SOSA (sensors)",
274        rel_path: "bundled/ontologies/w3c/sosa.ttl",
275        domain: "sensing",
276        licence_note: "W3C",
277    },
278    OntologyCatalogEntry {
279        id: "music",
280        title: "Music Ontology (purl)",
281        rel_path: "bundled/ontologies/purl/music.ttl",
282        domain: "audio",
283        licence_note: "Music Ontology",
284    },
285    OntologyCatalogEntry {
286        id: "consent",
287        title: "Consent ontology (purl)",
288        rel_path: "bundled/ontologies/purl/consent.ttl",
289        domain: "rights",
290        licence_note: "bundled purl",
291    },
292];
293
294fn primary_subject_for_uri(uri: &str) -> u64 {
295    const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
296    const FNV_PRIME: u64 = 0x100_0000_01b3;
297    let mut h = FNV_OFFSET;
298    for b in uri.as_bytes() {
299        h ^= u64::from(*b);
300        h = h.wrapping_mul(FNV_PRIME);
301    }
302    h & 0x0FFF_FFFF_FFFF_FFFF
303}
304
305fn now_unix() -> u64 {
306    std::time::SystemTime::now()
307        .duration_since(std::time::UNIX_EPOCH)
308        .map(|d| d.as_secs())
309        .unwrap_or(0)
310}
311
312fn model_to_library(m: ModelCatalogEntry, storage_root: &Path, now: u64) -> LibraryEntry {
313    let uri = format!("model://webizen/{}", m.id);
314    let abs = storage_root.join(m.rel_path);
315    let on_disk = abs.is_file() || (m.rel_path.ends_with('/') && abs.is_dir());
316    let honesty = if m.is_seed_reference {
317        "SEED/REFERENCE — not a certified foundation model. Do not use for production accuracy claims."
318    } else {
319        "User-supplied path slot — activate only licence-compatible weights you control."
320    };
321    let mut le = LibraryEntry {
322        asset_uri: uri.clone(),
323        primary_subject: primary_subject_for_uri(&uri),
324        media_type: MODEL_MEDIA_TYPE.into(),
325        quins: Vec::new(),
326        topics: vec![
327            "model".into(),
328            m.category.into(),
329            m.role.into(),
330            if m.is_seed_reference {
331                "seed-reference".into()
332            } else {
333                "user-slot".into()
334            },
335        ],
336        projects: vec![format!("perception:{}", m.category)],
337        purposes: vec!["model".into(), "software".into(), "perception".into()],
338        place: Some(abs.display().to_string()),
339        occurred_at: None,
340        lat: None,
341        lon: None,
342        flags: if m.is_seed_reference {
343            vec![super::hypermedia_store::LibraryFlag {
344                kind: "honesty:seed_reference".into(),
345                severity_level: 1,
346                detail: honesty.into(),
347            }]
348        } else {
349            Vec::new()
350        },
351        ingested_unix: now,
352        excerpt: format!(
353            "{} — role={} path={} on_disk={} — {}",
354            m.title, m.role, m.rel_path, on_disk, honesty
355        ),
356        sensitivity: "public".into(),
357        section: LibrarySection::Software.as_str().into(),
358        commons_visibility: CommonsVisibility::None,
359        cml_signals: Vec::new(),
360        cml_concept_count: 0,
361        cml_n3: String::new(),
362        cof_html: String::new(),
363        cof_segment_count: 0,
364        cof_segment_index: 0,
365        cof_profile: String::new(),
366    };
367    le.recompute_section();
368    le
369}
370
371fn ontology_to_library(o: OntologyCatalogEntry, now: u64) -> LibraryEntry {
372    let uri = format!("ontology://webizen/{}", o.id);
373    let mut le = LibraryEntry {
374        asset_uri: uri.clone(),
375        primary_subject: primary_subject_for_uri(&uri),
376        media_type: ONTOLOGY_MEDIA_TYPE.into(),
377        quins: Vec::new(),
378        topics: vec!["ontology".into(), o.domain.into(), o.id.into()],
379        projects: vec![format!("ontology:{}", o.domain)],
380        purposes: vec!["ontology".into(), "software".into(), "knowledge".into()],
381        place: Some(o.rel_path.into()),
382        occurred_at: None,
383        lat: None,
384        lon: None,
385        flags: Vec::new(),
386        ingested_unix: now,
387        excerpt: format!(
388            "{} — domain={} · {} · source {}",
389            o.title, o.domain, o.licence_note, o.rel_path
390        ),
391        sensitivity: "public".into(),
392        section: LibrarySection::Software.as_str().into(),
393        commons_visibility: CommonsVisibility::None,
394        cml_signals: Vec::new(),
395        cml_concept_count: 0,
396        cml_n3: String::new(),
397        cof_html: String::new(),
398        cof_segment_count: 0,
399        cof_segment_index: 0,
400        cof_profile: String::new(),
401    };
402    le.recompute_section();
403    le
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct PerceptionSeedReport {
408    pub models_added: usize,
409    pub models_updated: usize,
410    pub ontologies_added: usize,
411    pub ontologies_updated: usize,
412    pub weights_ensured: Vec<String>,
413    pub note: String,
414}
415
416/// Ensure seed weight files exist and catalogue models + ontologies into Library.
417pub fn seed_perception_into_library(
418    store: &HypermediaStore,
419    storage_root: &Path,
420) -> Result<PerceptionSeedReport, String> {
421    let mut weights_ensured = Vec::new();
422    if let Ok(s) = crate::audio_pipeline::ensure_aed_weights(storage_root) {
423        weights_ensured.push(s);
424    }
425    if let Ok(s) = crate::audio_pipeline::ensure_speech_weights(storage_root) {
426        weights_ensured.push(s);
427    }
428    if let Ok(s) = crate::vision_pipeline::ensure_vision_weights(storage_root) {
429        weights_ensured.push(s);
430    }
431
432    let now = now_unix();
433    let mut entries = store.load().map_err(|e| e.to_string())?;
434    let mut by_uri: std::collections::HashMap<String, usize> = entries
435        .iter()
436        .enumerate()
437        .map(|(i, e)| (e.asset_uri.clone(), i))
438        .collect();
439
440    let mut models_added = 0usize;
441    let mut models_updated = 0usize;
442    for row in PERCEPTION_MODEL_CATALOG {
443        let mut entry = model_to_library(*row, storage_root, now);
444        let uri = entry.asset_uri.clone();
445        if let Some(&idx) = by_uri.get(&uri) {
446            entry.ingested_unix = entries[idx].ingested_unix;
447            entries[idx] = entry;
448            models_updated += 1;
449        } else {
450            by_uri.insert(uri, entries.len());
451            entries.push(entry);
452            models_added += 1;
453        }
454    }
455
456    let mut ontologies_added = 0usize;
457    let mut ontologies_updated = 0usize;
458    for row in PERCEPTION_ONTOLOGY_CATALOG {
459        let mut entry = ontology_to_library(*row, now);
460        let uri = entry.asset_uri.clone();
461        if let Some(&idx) = by_uri.get(&uri) {
462            entry.ingested_unix = entries[idx].ingested_unix;
463            entries[idx] = entry;
464            ontologies_updated += 1;
465        } else {
466            by_uri.insert(uri, entries.len());
467            entries.push(entry);
468            ontologies_added += 1;
469        }
470    }
471
472    store.replace_all(&entries).map_err(|e| e.to_string())?;
473
474    Ok(PerceptionSeedReport {
475        models_added,
476        models_updated,
477        ontologies_added,
478        ontologies_updated,
479        weights_ensured,
480        note: "Models and ontologies catalogued in Library → Software with honesty flags on seed weights. Not foundation models.".into(),
481    })
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487
488    #[test]
489    fn catalogs_nonempty() {
490        assert!(!PERCEPTION_MODEL_CATALOG.is_empty());
491        assert!(PERCEPTION_ONTOLOGY_CATALOG.len() >= 10);
492        for m in PERCEPTION_MODEL_CATALOG {
493            assert!(!m.id.is_empty());
494        }
495    }
496
497    #[test]
498    fn seed_idempotent() {
499        let dir = tempfile::tempdir().unwrap();
500        let store = HypermediaStore::open(dir.path()).unwrap();
501        let r1 = seed_perception_into_library(&store, dir.path()).unwrap();
502        assert!(r1.models_added + r1.ontologies_added > 0);
503        let r2 = seed_perception_into_library(&store, dir.path()).unwrap();
504        assert_eq!(r2.models_added, 0);
505        assert_eq!(r2.ontologies_added, 0);
506        assert!(r2.models_updated >= PERCEPTION_MODEL_CATALOG.len());
507        let software = store.by_section(LibrarySection::Software).unwrap();
508        assert!(software.iter().any(|e| e.asset_uri.starts_with("model://")));
509        assert!(software
510            .iter()
511            .any(|e| e.asset_uri.starts_with("ontology://")));
512    }
513
514    #[test]
515    fn test_vision_catalog_contains_permissive_models() {
516        let model_ids: Vec<&str> = PERCEPTION_MODEL_CATALOG.iter().map(|m| m.id).collect();
517
518        assert!(model_ids.contains(&"vision-yunet-face"));
519        assert!(model_ids.contains(&"vision-sface-embed"));
520        assert!(model_ids.contains(&"vision-mediapipe-facemesh"));
521        assert!(model_ids.contains(&"vision-midas-depth"));
522        assert!(model_ids.contains(&"vision-espcn-sr"));
523        assert!(model_ids.contains(&"vision-yolonas-object"));
524    }
525}