qualia_core_db/hypermedia/processors/mod.rs
1//! **Content processors** — ingest *derives* searchability. Each processor
2//! turns an asset's bytes into the derived, searchable representations +
3//! descriptor facets (+ flags) that fold into its container, so the *original*
4//! becomes findable by meaning.
5//!
6//! The framework itself — the [`Processor`](super::Processor) trait,
7//! [`ProcessorOutput`](super::ProcessorOutput), and the model-free
8//! [`TextProcessor`](super::TextProcessor) — lives in the parent module. This
9//! submodule holds the heavier, self-contained processors and the dispatcher
10//! that picks one by media type:
11//!
12//! - [`ImageProcessor`] — EXIF/PNG metadata → timeline + map facets (model-free).
13//! - [`WavProcessor`] — WAV → duration + dominant-frequency, via the project's STFT.
14//!
15//! **Plug-in points (honest gaps, not stubs):** *what an image depicts* /
16//! OCR needs a vision model, and a *transcript* needs an ASR model. Both are
17//! new `Processor` implementations to register here when the `qualia-vision` /
18//! `qualia-audio` model engines exist — the dispatcher already routes by media
19//! type, so they slot in without touching callers.
20
21pub mod audio;
22pub mod image;
23
24pub use audio::{AudioSpectralSummary, WavProcessor};
25pub use image::{ImageMetadata, ImageProcessor};
26
27use super::{Processor, TextProcessor};
28
29/// Pick the processor that best handles `media_type`, or `None` if no
30/// registered processor claims it (the caller can fall back to storing the
31/// asset with no derived facets). Order matters only where `handles` overlaps;
32/// here the media-type families are disjoint.
33///
34/// Returned boxed so heterogeneous processors share one call site; ingest is
35/// not a hot path (it runs once per asset, off the render/query loops), so the
36/// single allocation is acceptable.
37pub fn processor_for(media_type: &str) -> Option<Box<dyn Processor>> {
38 let candidates: [Box<dyn Processor>; 3] = [
39 Box::new(ImageProcessor),
40 Box::new(WavProcessor),
41 Box::new(TextProcessor::default()),
42 ];
43 candidates.into_iter().find(|p| p.handles(media_type))
44}
45
46/// The media types a registered processor can derive searchability from — for
47/// callers that want to advertise what ingest understands.
48pub fn supported_media_types() -> &'static [&'static str] {
49 &[
50 "image/jpeg",
51 "image/jpg",
52 "image/png",
53 "audio/wav",
54 "audio/x-wav",
55 "audio/wave",
56 "text/*",
57 ]
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn dispatch_routes_by_media_type() {
66 assert!(processor_for("image/jpeg").unwrap().handles("image/jpeg"));
67 assert!(processor_for("audio/wav").unwrap().handles("audio/wav"));
68 assert!(processor_for("text/markdown")
69 .unwrap()
70 .handles("text/markdown"));
71 // An unknown binary type has no registered processor.
72 assert!(processor_for("application/octet-stream").is_none());
73 }
74}