Skip to main content

qualia_client_core/
vision_pipeline.rs

1//! End-to-end native vision pipeline (MVP + GSW W/G/S).
2//!
3//! Synthetic or raw RGB → detect (reference or production weights) → track →
4//! epistemic quins → overlay BMP. Plus generate + image-to-3D. No Python.
5
6use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
7use qualia_core_db::container_10d::provenance_section::ProvenanceSidecar;
8use qualia_core_db::render::assets::{mesh_to_nquins_with_digests, Mesh};
9use qualia_core_db::render::compile_10d::{
10    compile_mesh_to_10d_vision, compile_mesh_to_10d_vision_with_provenance,
11};
12use qualia_core_db::sparql_library::vision_shacl::{
13    validate_vision_observation_graph, VisionShaclReport,
14};
15use qualia_core_db::specialized_libs::computational_geometry::{
16    decimate_qem, DecimateOptions, Point3,
17};
18use qualia_core_db::tensor::Tensor10D;
19use qualia_core_db::NQuin;
20use qualia_vision::detector::{
21    GridMultiObjectDetector, CLASS_MOSTLY_BLUE, CLASS_MOSTLY_GREEN, CLASS_MOSTLY_RED,
22};
23use qualia_vision::generator::{compile_generation_receipt_quins, NativeImageGenerator};
24use qualia_vision::media_store::{MediaStore, RetentionClass};
25use qualia_vision::metrics::evaluate_synthetic;
26use qualia_vision::overlay::{box_css_percent, compose_rgb_overlay_rgba8, encode_bmp_rgba8};
27use qualia_vision::query_instances_in_region;
28use qualia_vision::semantic::{compile_observation_quins_full, media_digest, VisionQuin};
29use qualia_vision::spatial::{
30    cleanup_mesh_ir, detections_to_node_hints, image_to_heightfield_mesh, mesh_ir_to_export,
31    mesh_ir_to_obj, pack_geometry_export_for_10d, MeshCleanupOptions, MeshIR, NodeHint,
32};
33use qualia_vision::synthetic::{generate_scene_rgb8, sample_id, DatasetSplit, SyntheticSampleId};
34use qualia_vision::tracker::BoundedTracker;
35use qualia_vision::types::{Detection, ImageView, PixelFormat, VisualModel, MAX_DETECTIONS};
36use qualia_vision::weights::{ProductionVision, VisionBackendKind, VisionWeightBundle};
37use serde::Serialize;
38
39use crate::vision_ingest::{
40    append_human_attestation, append_native_observation_quins, human_correct_quin,
41    human_reject_quin, NativeDetection,
42};
43
44#[derive(Debug, Clone, Serialize)]
45pub struct OverlayBoxDto {
46    pub class_hash: String,
47    pub instance_hash: String,
48    pub score: f32,
49    pub track_id: u32,
50    pub frame_index: u32,
51    /// CSS percent: left, top, width, height
52    pub left: f32,
53    pub top: f32,
54    pub width: f32,
55    pub height: f32,
56    pub rejected: bool,
57}
58
59#[derive(Debug, Clone, Serialize)]
60pub struct VisionDemoResult {
61    pub width: u32,
62    pub height: u32,
63    pub seed: u64,
64    pub split: String,
65    pub model_hash: String,
66    pub media_hash: String,
67    pub detections: Vec<OverlayBoxDto>,
68    pub n_gt: usize,
69    pub n_pred: usize,
70    pub quins_written: usize,
71    pub shacl_ok: bool,
72    pub shacl_observations: u32,
73    pub shacl_human: u32,
74    /// data:image/bmp;base64,... with boxes drawn
75    pub overlay_data_url: String,
76    pub note: String,
77    /// `reference` | `production_weights`
78    pub backend: String,
79    pub is_reference_backend: bool,
80    pub synthetic_match_acc: Option<f32>,
81}
82
83#[derive(Debug, Clone, Serialize)]
84pub struct GenerateResult {
85    pub width: u32,
86    pub height: u32,
87    pub seed: u64,
88    pub steps: u32,
89    pub model_hash: String,
90    pub prompt_hash: String,
91    pub output_hash: String,
92    pub is_reference_generator: bool,
93    pub image_data_url: String,
94    pub note: String,
95}
96
97#[derive(Debug, Clone, Serialize)]
98pub struct ImageTo3dResult {
99    pub vertex_count: u32,
100    pub triangle_count: u32,
101    pub mesh_hash: String,
102    pub model_hash: String,
103    pub validation_ok: bool,
104    pub validation_status: String,
105    pub is_reference_recon: bool,
106    pub note: String,
107}
108
109/// Full G→S continuum: generate → media store → recon → validate → OBJ + .10d + quins.
110#[derive(Debug, Clone, Serialize)]
111pub struct GsContinuumResult {
112    pub generate: GenerateResult,
113    pub mesh: ImageTo3dResult,
114    pub media_digest_hex: String,
115    pub media_stored: bool,
116    pub obj_bytes: usize,
117    pub container_10d_bytes: usize,
118    pub geometry_quins: usize,
119    pub generation_quins: usize,
120    pub obj_path: Option<String>,
121    pub container_10d_path: Option<String>,
122    pub note: String,
123}
124
125/// Convert vision MeshIR → core `Mesh` via public export + 10d pack (C1 handoff).
126fn mesh_ir_to_core_mesh(ir: &MeshIR) -> Result<Mesh, String> {
127    let export = mesh_ir_to_export(ir).map_err(|e| format!("mesh_ir_to_export: {e:?}"))?;
128    let g = pack_geometry_export_for_10d(&export);
129    Ok(Mesh {
130        positions: g.positions,
131        triangles: g.triangles,
132        min: g.min,
133        max: g.max,
134    })
135}
136
137/// Map vision [`NodeHint`] → [`Tensor10D`] for spectral paint (D1/D2).
138///
139/// Epistemic parallel context (q=1): vision detections are proposals, not ground truth.
140pub fn node_hint_to_tensor10d(h: &NodeHint) -> Tensor10D {
141    Tensor10D::parallel_context(
142        1.0, // q: proposal
143        0.0, // v: Euclidean
144        0.0, // w: medical-class slot (vision still uses 0 until domain wiring)
145        h.x,
146        h.y,
147        h.z,
148        h.t,
149        1.0, // alpha full amplitude; score is in σ path already
150        0.0,
151        h.sigma.clamp(0.0, 1.0),
152    )
153}
154
155/// Host C2: optional QEM decimate when triangle count exceeds `max_faces`.
156fn maybe_decimate_mesh(mesh: &mut Mesh, max_faces: usize) -> Result<Option<String>, String> {
157    if mesh.triangle_count() <= max_faces || max_faces == 0 {
158        return Ok(None);
159    }
160    let verts: Vec<Point3> = mesh
161        .positions
162        .iter()
163        .map(|p| Point3::new(p[0] as f64, p[1] as f64, p[2] as f64))
164        .collect();
165    let tris = mesh.triangles.clone();
166    let mut out_v = vec![Point3::new(0.0, 0.0, 0.0); verts.len()];
167    let mut out_t = vec![[0u32; 3]; tris.len()];
168    let report = decimate_qem(
169        &verts,
170        &tris,
171        DecimateOptions::to_faces(max_faces),
172        &mut out_v,
173        &mut out_t,
174    )
175    .map_err(|e| format!("decimate_qem: {e:?}"))?;
176    mesh.positions = out_v[..report.vertices]
177        .iter()
178        .map(|p| [p.x as f32, p.y as f32, p.z as f32])
179        .collect();
180    mesh.triangles = out_t[..report.faces].to_vec();
181    // Recompute AABB.
182    let mut min = [f32::INFINITY; 3];
183    let mut max = [f32::NEG_INFINITY; 3];
184    for p in &mesh.positions {
185        for k in 0..3 {
186            min[k] = min[k].min(p[k]);
187            max[k] = max[k].max(p[k]);
188        }
189    }
190    mesh.min = min;
191    mesh.max = max;
192    Ok(Some(format!(
193        "decimated faces {}→{} ({} collapses)",
194        tris.len(),
195        report.faces,
196        report.collapses
197    )))
198}
199
200fn det_to_dto(d: &Detection, rejected: bool) -> OverlayBoxDto {
201    let (left, top, width, height) = box_css_percent(d);
202    OverlayBoxDto {
203        class_hash: format!("0x{:016x}", d.class_hash),
204        instance_hash: format!("0x{:016x}", d.instance_hash),
205        score: d.score_f32(),
206        track_id: d.track_id,
207        frame_index: d.frame_index,
208        left,
209        top,
210        width,
211        height,
212        rejected,
213    }
214}
215
216fn vision_quin_to_nquin(v: &VisionQuin) -> NQuin {
217    NQuin {
218        subject: v.subject,
219        predicate: v.predicate,
220        object: v.object,
221        context: v.context,
222        metadata: v.metadata,
223        parity: v.parity,
224    }
225}
226
227/// Run synthetic sample through detect → track → compile → SHACL → overlay BMP.
228pub fn run_synthetic_demo(
229    split: DatasetSplit,
230    index: u32,
231    width: u32,
232    height: u32,
233) -> Result<VisionDemoResult, String> {
234    run_synthetic_demo_with_backend(split, index, width, height, "reference")
235}
236
237pub fn run_synthetic_demo_with_backend(
238    split: DatasetSplit,
239    index: u32,
240    width: u32,
241    height: u32,
242    backend: &str,
243) -> Result<VisionDemoResult, String> {
244    let sample = sample_id(split, index, width, height);
245    run_sample_demo_with_backend(&sample, backend)
246}
247
248pub fn run_sample_demo(sample: &SyntheticSampleId) -> Result<VisionDemoResult, String> {
249    run_sample_demo_with_backend(sample, "reference")
250}
251
252pub fn run_sample_demo_with_backend(
253    sample: &SyntheticSampleId,
254    backend: &str,
255) -> Result<VisionDemoResult, String> {
256    let w = sample.width;
257    let h = sample.height;
258    let px = (w as usize) * (h as usize);
259    let mut rgb = vec![0u8; px * 3];
260    let mut gt = [Detection::empty(); MAX_DETECTIONS];
261    let n_gt = generate_scene_rgb8(sample, &mut rgb, &mut gt).map_err(|e| format!("{e:?}"))?;
262
263    let img = ImageView {
264        bytes: &rgb,
265        width: w,
266        height: h,
267        row_stride: w * 3,
268        format: PixelFormat::Rgb8,
269    };
270    let mut preds = [Detection::empty(); MAX_DETECTIONS];
271    let mut emb = [0.0f32; 32];
272    let mut ws = [0u8; MAX_DETECTIONS];
273    let use_prod = backend.eq_ignore_ascii_case("production")
274        || backend.eq_ignore_ascii_case("production_weights")
275        || backend.eq_ignore_ascii_case("weights");
276
277    let (n_pred, model_hash, backend_kind, is_ref, synth_acc) = if use_prod {
278        let classes = [CLASS_MOSTLY_RED, CLASS_MOSTLY_GREEN, CLASS_MOSTLY_BLUE];
279        let bundle = VisionWeightBundle::from_seed(0x01D1_FACE_u64, 16, &classes);
280        let mh = bundle.model_hash();
281        let mut prod = ProductionVision::new(bundle);
282        let counts = prod
283            .infer(img, &mut preds, &mut emb, &mut ws)
284            .map_err(|e| format!("{e:?}"))?;
285        let mut m2 =
286            ProductionVision::new(VisionWeightBundle::from_seed(0x01D1_FACE_u64, 16, &classes));
287        let metrics =
288            evaluate_synthetic(&mut m2, VisionBackendKind::ProductionWeights, mh, 4, 32, 24);
289        (
290            counts.detections,
291            mh,
292            "production_weights",
293            false,
294            Some(metrics.mean_match_acc),
295        )
296    } else {
297        let det = GridMultiObjectDetector::new(4, 3);
298        let n_pred = det
299            .detect(img, 0, &mut preds, &mut ws)
300            .map_err(|e| format!("{e:?}"))?;
301        let mh = det.model_hash();
302        let mut det2 = GridMultiObjectDetector::new(4, 3);
303        let metrics = evaluate_synthetic(&mut det2, VisionBackendKind::Reference, mh, 4, 32, 24);
304        (n_pred, mh, "reference", true, Some(metrics.mean_match_acc))
305    };
306
307    let mut tracker = BoundedTracker::new();
308    tracker.update(0, &mut preds, n_pred);
309
310    let digest = media_digest(&rgb);
311    let mut vquins = [VisionQuin::with_parity(0, 0, 0, 0, 0); 256];
312    let n_q = compile_observation_quins_full(digest, &preds[..n_pred], model_hash, &mut vquins);
313
314    let mut nquins = Vec::with_capacity(n_q);
315    for q in vquins.iter().take(n_q) {
316        nquins.push(vision_quin_to_nquin(q));
317    }
318    let report = validate_vision_observation_graph(&nquins);
319
320    let mut rgba = vec![0u8; px * 4];
321    compose_rgb_overlay_rgba8(w, h, &rgb, &preds, n_pred, [0, 255, 180, 255], 2, &mut rgba)
322        .map_err(|e| format!("{e:?}"))?;
323    let mut bmp = vec![0u8; 54 + px * 4];
324    let bmp_n = encode_bmp_rgba8(w, h, &rgba, &mut bmp).map_err(|e| format!("{e:?}"))?;
325    let b64 = B64.encode(&bmp[..bmp_n]);
326
327    let split_s = match sample.split {
328        DatasetSplit::Train => "train",
329        DatasetSplit::Test => "test",
330    };
331
332    let note = if is_ref {
333        "Backend=reference (grid). Epistemic only — not ground truth. H1 real eval not run."
334    } else {
335        "Backend=production_weights (QVWT seed fixture). Synthetic metrics only until H1 corpus."
336    };
337
338    Ok(VisionDemoResult {
339        width: w,
340        height: h,
341        seed: sample.seed,
342        split: split_s.to_string(),
343        model_hash: format!("0x{:016x}", model_hash),
344        media_hash: format!("0x{:016x}", digest.hash),
345        detections: preds[..n_pred]
346            .iter()
347            .map(|d| det_to_dto(d, false))
348            .collect(),
349        n_gt,
350        n_pred,
351        quins_written: n_q,
352        shacl_ok: report.ok,
353        shacl_observations: report.observation_count,
354        shacl_human: report.human_attestation_count,
355        overlay_data_url: format!("data:image/bmp;base64,{b64}"),
356        note: note.into(),
357        backend: backend_kind.into(),
358        is_reference_backend: is_ref,
359        synthetic_match_acc: synth_acc,
360    })
361}
362
363/// Swarm G — native seeded image generation.
364pub fn generate_image(
365    prompt: &str,
366    seed: u64,
367    steps: u32,
368    width: u32,
369    height: u32,
370) -> Result<GenerateResult, String> {
371    let w = width.clamp(8, 256);
372    let h = height.clamp(8, 256);
373    let mut rgb = vec![0u8; (w * h * 3) as usize];
374    let g = NativeImageGenerator::new();
375    let rec = g
376        .generate_rgb8(prompt, seed, steps, w, h, &mut rgb)
377        .map_err(|e| format!("{e:?}"))?;
378    let mut rgba = vec![0u8; (w * h * 4) as usize];
379    for i in 0..(w * h) as usize {
380        rgba[i * 4] = rgb[i * 3];
381        rgba[i * 4 + 1] = rgb[i * 3 + 1];
382        rgba[i * 4 + 2] = rgb[i * 3 + 2];
383        rgba[i * 4 + 3] = 255;
384    }
385    let mut bmp = vec![0u8; 54 + rgba.len()];
386    let n = encode_bmp_rgba8(w, h, &rgba, &mut bmp).map_err(|e| format!("{e:?}"))?;
387    Ok(GenerateResult {
388        width: w,
389        height: h,
390        seed,
391        steps: rec.steps,
392        model_hash: format!("0x{:016x}", rec.model_hash),
393        prompt_hash: format!("0x{:016x}", rec.prompt_hash),
394        output_hash: format!("0x{:016x}", rec.output_digest.hash),
395        is_reference_generator: rec.is_reference_generator,
396        image_data_url: format!("data:image/bmp;base64,{}", B64.encode(&bmp[..n])),
397        note: "Native reference generator (seeded). Not a foundation DiT; swap weights under G0 licence.".into(),
398    })
399}
400
401/// Swarm S-V10 — heightfield recon + validation.
402pub fn image_to_3d_from_rgb(
403    width: u32,
404    height: u32,
405    rgb: &[u8],
406    grid: u32,
407) -> Result<ImageTo3dResult, String> {
408    let img = ImageView {
409        bytes: rgb,
410        width,
411        height,
412        row_stride: width * 3,
413        format: PixelFormat::Rgb8,
414    };
415    let (mesh, rec, rep) = image_to_heightfield_mesh(img, grid).map_err(|e| format!("{e:?}"))?;
416    let status = format!("{:?}", rep.status);
417    Ok(ImageTo3dResult {
418        vertex_count: mesh.vertex_count() as u32,
419        triangle_count: mesh.triangle_count() as u32,
420        mesh_hash: format!("0x{:016x}", mesh.content_hash),
421        model_hash: format!("0x{:016x}", rec.model_hash),
422        validation_ok: rep.ok(),
423        validation_status: status,
424        is_reference_recon: rec.is_reference_recon,
425        note: "Heightfield recon is epistemic proposal; validated before any Q42 geometry commit."
426            .into(),
427    })
428}
429
430/// Generate then reconstruct (G→S10 smoke path).
431pub fn generate_and_reconstruct(
432    prompt: &str,
433    seed: u64,
434) -> Result<(GenerateResult, ImageTo3dResult), String> {
435    let gen = generate_image(prompt, seed, 4, 32, 32)?;
436    let mut rgb = vec![0u8; 32 * 32 * 3];
437    let g = NativeImageGenerator::new();
438    g.generate_rgb8(prompt, seed, 4, 32, 32, &mut rgb)
439        .map_err(|e| format!("{e:?}"))?;
440    let mesh = image_to_3d_from_rgb(32, 32, &rgb, 8)?;
441    Ok((gen, mesh))
442}
443
444/// Full continuum used before handing off to auditory work:
445/// generate → content-addressed store → heightfield recon → validate →
446/// OBJ + sealed `.10d` + generation receipt quins + geometry quins.
447pub fn run_gs_continuum(
448    storage_root: &std::path::Path,
449    prompt: &str,
450    seed: u64,
451    steps: u32,
452    width: u32,
453    height: u32,
454    recon_grid: u32,
455    media_time_ms: u64,
456) -> Result<GsContinuumResult, String> {
457    let w = width.clamp(8, 128);
458    let h = height.clamp(8, 128);
459    let mut rgb = vec![0u8; (w * h * 3) as usize];
460    let gen = NativeImageGenerator::new();
461    let rec = gen
462        .generate_rgb8_cancellable(prompt, seed, steps, w, h, &mut rgb, None, media_time_ms)
463        .map_err(|e| format!("{e:?}"))?;
464
465    // Media store (no partial commit if later steps fail after store — store is deduped).
466    let media_dir = storage_root.join("vision_media");
467    let store = MediaStore::open(&media_dir).map_err(|e| e)?;
468    let now = std::time::SystemTime::now()
469        .duration_since(std::time::UNIX_EPOCH)
470        .map(|d| d.as_secs())
471        .unwrap_or(0);
472    let record = store
473        .import_bytes(
474            &rgb,
475            "application/octet-stream",
476            w,
477            h,
478            RetentionClass::Restricted,
479            now,
480        )
481        .map_err(|e| e)?;
482
483    let mut gen_quins = [VisionQuin::with_parity(0, 0, 0, 0, 0); 8];
484    let n_gen_q = compile_generation_receipt_quins(&rec, &mut gen_quins);
485
486    // BMP preview for API
487    let mut rgba = vec![0u8; (w * h * 4) as usize];
488    for i in 0..(w * h) as usize {
489        rgba[i * 4] = rgb[i * 3];
490        rgba[i * 4 + 1] = rgb[i * 3 + 1];
491        rgba[i * 4 + 2] = rgb[i * 3 + 2];
492        rgba[i * 4 + 3] = 255;
493    }
494    let mut bmp = vec![0u8; 54 + rgba.len()];
495    let bmp_n = encode_bmp_rgba8(w, h, &rgba, &mut bmp).map_err(|e| format!("{e:?}"))?;
496    let generate = GenerateResult {
497        width: w,
498        height: h,
499        seed,
500        steps: rec.steps,
501        model_hash: format!("0x{:016x}", rec.model_hash),
502        prompt_hash: format!("0x{:016x}", rec.prompt_hash),
503        output_hash: format!("0x{:016x}", rec.output_digest.hash),
504        is_reference_generator: rec.is_reference_generator,
505        image_data_url: format!("data:image/bmp;base64,{}", B64.encode(&bmp[..bmp_n])),
506        note: format!(
507            "Stored media digest {}; media_time_ms={media_time_ms} for cross-modal timeline.",
508            record.digest_hex
509        ),
510    };
511
512    let img = ImageView {
513        bytes: &rgb,
514        width: w,
515        height: h,
516        row_stride: w * 3,
517        format: PixelFormat::Rgb8,
518    };
519    let (mut mesh_ir, recon_rec, rep) =
520        image_to_heightfield_mesh(img, recon_grid).map_err(|e| format!("{e:?}"))?;
521    if !rep.ok() {
522        return Err(format!("mesh validation failed: {:?}", rep.status));
523    }
524
525    // C2 vision-side: drop degenerate faces before export.
526    let quality = cleanup_mesh_ir(
527        &mut mesh_ir,
528        MeshCleanupOptions {
529            weld_epsilon: 1e-6,
530            min_area: 0.0,
531        },
532    )
533    .map_err(|e| format!("mesh quality cleanup: {e:?}"))?;
534
535    let mut obj_buf = vec![0u8; mesh_ir.positions.len() * 64 + mesh_ir.indices.len() * 24 + 256];
536    let obj_n = mesh_ir_to_obj(&mesh_ir, &mut obj_buf).map_err(|e| format!("{e:?}"))?;
537    obj_buf.truncate(obj_n);
538
539    let mut core_mesh = mesh_ir_to_core_mesh(&mesh_ir)?;
540    // C2 host: QEM when heightfield is dense (cap 2048 faces for edge browsers).
541    let decimate_note = maybe_decimate_mesh(&mut core_mesh, 2048)?;
542
543    // D1: seal with Tensor10DNodes from a single plane marker (heightfield centre).
544    // Full detection→node path uses seal_vision_mesh_with_detections when callers have dets.
545    let centre = Tensor10D::parallel_context(
546        1.0, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 1.0, 0.0, 0.35, // mid-band σ for recon marker
547    );
548    // D4: provenance sidecar — media digest bytes + recon model hash in-envelope.
549    let mut version_hash = [0u8; 32];
550    let mh = recon_rec.model_hash.to_le_bytes();
551    version_hash[..8].copy_from_slice(&mh);
552    let dig = record.digest_u64.to_le_bytes();
553    version_hash[8..16].copy_from_slice(&dig);
554    // Source bytes: short media digest prefix (self-authenticating CRC of this payload).
555    let source_tag = format!(
556        "qualia-vision-recon;media={};model=0x{:016x}",
557        record.digest_hex, recon_rec.model_hash
558    );
559    let provenance = ProvenanceSidecar::new(
560        source_tag.into_bytes(),
561        "application/x-qualia-vision-recon",
562        "PermissiveReady-local", // synthetic continuum — not a third-party weight licence
563    )
564    .with_metadata(
565        format!(
566            r#"{{"media_digest":"{}","model_hash":"0x{:016x}"}}"#,
567            record.digest_hex, recon_rec.model_hash
568        )
569        .into_bytes(),
570        0,
571        version_hash,
572    );
573    // C3+D4: topology/spatial when CG linked + provenance always.
574    let container = compile_mesh_to_10d_vision_with_provenance(&core_mesh, &[centre], &provenance)
575        .map_err(|e| e.to_string())?;
576    // CRC of container for compiled digest (first 4 bytes of crc is enough for quin object)
577    let compiled_digest = {
578        let mut h: u32 = 0;
579        for chunk in container.chunks(4) {
580            let mut b = [0u8; 4];
581            b[..chunk.len()].copy_from_slice(chunk);
582            h ^= u32::from_le_bytes(b);
583        }
584        h
585    };
586    let source_digest = (record.digest_u64 & 0xFFFF_FFFF) as u32;
587    let asset_uri = format!("urn:qualia:vision:recon:{}", record.digest_hex);
588    let (geo_quins, _lex) = mesh_to_nquins_with_digests(
589        &core_mesh,
590        &asset_uri,
591        "obj",
592        source_digest,
593        compiled_digest,
594    );
595
596    let out_dir = storage_root
597        .join("vision_geometry")
598        .join(&record.digest_hex);
599    std::fs::create_dir_all(&out_dir).map_err(|e| e.to_string())?;
600    let obj_path = out_dir.join("recon.obj");
601    let c10_path = out_dir.join("recon.10d");
602    std::fs::write(&obj_path, &obj_buf).map_err(|e| e.to_string())?;
603    std::fs::write(&c10_path, &container).map_err(|e| e.to_string())?;
604
605    // Append generation + geometry quins to vision_native.wal when possible
606    let mut nquin_buf = Vec::with_capacity(n_gen_q + geo_quins.len());
607    for q in gen_quins.iter().take(n_gen_q) {
608        nquin_buf.push(NQuin {
609            subject: q.subject,
610            predicate: q.predicate,
611            object: q.object,
612            context: q.context,
613            metadata: q.metadata,
614            parity: q.parity,
615        });
616    }
617    nquin_buf.extend(geo_quins.iter().cloned());
618    let wal_path = storage_root.join("models").join("vision_native.wal");
619    if let Some(parent) = wal_path.parent() {
620        let _ = std::fs::create_dir_all(parent);
621    }
622    if let Ok(mut wal) = qualia_core_db::wal::WriteAheadLog::open(&wal_path) {
623        for q in &nquin_buf {
624            let _ = wal.append_mutation(q);
625        }
626    }
627
628    let mesh = ImageTo3dResult {
629        vertex_count: mesh_ir.vertex_count() as u32,
630        triangle_count: mesh_ir.triangle_count() as u32,
631        mesh_hash: format!("0x{:016x}", mesh_ir.content_hash),
632        model_hash: format!("0x{:016x}", recon_rec.model_hash),
633        validation_ok: true,
634        validation_status: format!("{:?}", rep.status),
635        is_reference_recon: recon_rec.is_reference_recon,
636        note: format!(
637            "Validated MeshIR → cleanup(deg={},weld={}) → OBJ + sealed .10d with Tensor10DNodes; digests on quins.",
638            quality.degenerates_removed, quality.vertices_welded
639        ),
640    };
641
642    let mut note = String::from(
643        "G→S continuum closed: store + validate + cleanup + compile(mesh+nodes). Ready for 10d browse.",
644    );
645    if let Some(d) = decimate_note {
646        note.push(' ');
647        note.push_str(&d);
648    }
649
650    Ok(GsContinuumResult {
651        generate,
652        mesh,
653        media_digest_hex: record.digest_hex,
654        media_stored: true,
655        obj_bytes: obj_n,
656        container_10d_bytes: container.len(),
657        geometry_quins: nquin_buf.len().saturating_sub(n_gen_q),
658        generation_quins: n_gen_q,
659        obj_path: Some(obj_path.display().to_string()),
660        container_10d_path: Some(c10_path.display().to_string()),
661        note,
662    })
663}
664
665/// Seal a MeshIR with detection-derived Tensor10D nodes (D1 product path).
666pub fn seal_vision_mesh_with_detections(
667    mesh_ir: &MeshIR,
668    dets: &[Detection],
669) -> Result<Vec<u8>, String> {
670    let mut cleaned = mesh_ir.clone();
671    let _ = cleanup_mesh_ir(
672        &mut cleaned,
673        MeshCleanupOptions {
674            weld_epsilon: 1e-6,
675            min_area: 0.0,
676        },
677    );
678    let mut core = mesh_ir_to_core_mesh(&cleaned)?;
679    let _ = maybe_decimate_mesh(&mut core, 4096)?;
680    let mut hints = vec![
681        NodeHint {
682            x: 0.0,
683            y: 0.0,
684            z: 0.0,
685            t: 0.0,
686            sigma: 0.0,
687        };
688        dets.len().min(256)
689    ];
690    let n = detections_to_node_hints(dets, &mut hints);
691    let nodes: Vec<Tensor10D> = hints[..n].iter().map(node_hint_to_tensor10d).collect();
692    compile_mesh_to_10d_vision(&core, &nodes).map_err(|e| e.to_string())
693}
694
695/// Persist native observations to WAL and return SHACL report.
696pub fn ingest_demo_to_wal(
697    storage_root: &std::path::Path,
698    demo: &VisionDemoResult,
699) -> Result<VisionShaclReport, String> {
700    let media_hash = u64::from_str_radix(demo.media_hash.trim_start_matches("0x"), 16)
701        .map_err(|e| e.to_string())?;
702    let model_hash = u64::from_str_radix(demo.model_hash.trim_start_matches("0x"), 16)
703        .map_err(|e| e.to_string())?;
704    let mut natives = Vec::new();
705    for d in &demo.detections {
706        let instance_hash = u64::from_str_radix(d.instance_hash.trim_start_matches("0x"), 16)
707            .map_err(|e| e.to_string())?;
708        let class_hash = u64::from_str_radix(d.class_hash.trim_start_matches("0x"), 16)
709            .map_err(|e| e.to_string())?;
710        let (x0, y0, x1, y1) = css_to_u16(d.left, d.top, d.width, d.height);
711        natives.push(NativeDetection {
712            class_hash,
713            instance_hash,
714            score_u16: (d.score.clamp(0.0, 1.0) * 65535.0) as u16,
715            x_min_u16: x0,
716            y_min_u16: y0,
717            x_max_u16: x1,
718            y_max_u16: y1,
719            frame_index: d.frame_index,
720            track_id: d.track_id,
721            flags: 0,
722        });
723    }
724    let n = append_native_observation_quins(
725        storage_root,
726        media_hash,
727        (demo.width as u64) * (demo.height as u64) * 3,
728        model_hash,
729        &natives,
730    )
731    .map_err(|e| e.to_string())?;
732    // Rebuild minimal graph for SHACL
733    let mut buf = [NQuin {
734        subject: 0,
735        predicate: 0,
736        object: 0,
737        context: 0,
738        metadata: 0,
739        parity: 0,
740    }; 256];
741    let written = crate::vision_ingest::compile_native_observation_quins(
742        media_hash,
743        (demo.width as u64) * (demo.height as u64) * 3,
744        model_hash,
745        &natives,
746        &mut buf,
747    )
748    .map_err(|e| e.to_string())?;
749    let _ = n;
750    Ok(validate_vision_observation_graph(&buf[..written]))
751}
752
753fn css_to_u16(left: f32, top: f32, width: f32, height: f32) -> (u16, u16, u16, u16) {
754    let x0 = ((left / 100.0) * 65535.0).clamp(0.0, 65535.0) as u16;
755    let y0 = ((top / 100.0) * 65535.0).clamp(0.0, 65535.0) as u16;
756    let x1 = (((left + width) / 100.0) * 65535.0).clamp(0.0, 65535.0) as u16;
757    let y1 = (((top + height) / 100.0) * 65535.0).clamp(0.0, 65535.0) as u16;
758    (x0, y0, x1, y1)
759}
760
761/// Human rejects instance; machine claims remain. Returns updated attestation note.
762pub fn reject_instance(
763    storage_root: &std::path::Path,
764    human_did_hash: u64,
765    instance_hash: u64,
766    reason_hash: u64,
767) -> Result<(), String> {
768    let q = human_reject_quin(human_did_hash, instance_hash, reason_hash);
769    append_human_attestation(storage_root, &q).map_err(|e| e.to_string())
770}
771
772pub fn correct_instance(
773    storage_root: &std::path::Path,
774    human_did_hash: u64,
775    instance_hash: u64,
776    new_class_hash: u64,
777) -> Result<(), String> {
778    let q = human_correct_quin(human_did_hash, instance_hash, new_class_hash);
779    append_human_attestation(storage_root, &q).map_err(|e| e.to_string())
780}
781
782/// Public helpers for UI without storage.
783pub fn demo_train(index: u32) -> Result<VisionDemoResult, String> {
784    run_synthetic_demo(DatasetSplit::Train, index, 96, 64)
785}
786
787pub fn demo_test(index: u32) -> Result<VisionDemoResult, String> {
788    run_synthetic_demo(DatasetSplit::Test, index, 96, 64)
789}
790
791#[cfg(test)]
792mod tests {
793    use super::*;
794
795    #[test]
796    fn synthetic_demo_produces_overlay_and_shacl() {
797        let r = demo_test(0).expect("demo");
798        assert!(r.n_pred >= 1 || r.n_gt >= 1);
799        assert!(r.overlay_data_url.starts_with("data:image/bmp;base64,"));
800        assert!(r.shacl_ok, "shacl should pass on full compile");
801        assert!(r.quins_written >= 1);
802        assert_eq!(r.backend, "reference");
803    }
804
805    #[test]
806    fn production_backend_labelled() {
807        let r = run_synthetic_demo_with_backend(DatasetSplit::Test, 0, 48, 32, "production")
808            .expect("prod");
809        assert_eq!(r.backend, "production_weights");
810        assert!(!r.is_reference_backend);
811        assert!(r.synthetic_match_acc.is_some());
812    }
813
814    #[test]
815    fn generate_and_recon_smoke() {
816        let (g, m) = generate_and_reconstruct("test field", 7).expect("g+s");
817        assert!(g.is_reference_generator);
818        assert!(m.validation_ok);
819        assert!(m.triangle_count > 0);
820    }
821
822    #[test]
823    fn gs_continuum_writes_obj_and_10d() {
824        let dir = std::env::temp_dir().join(format!(
825            "qv-gs-{}",
826            std::time::SystemTime::now()
827                .duration_since(std::time::UNIX_EPOCH)
828                .unwrap()
829                .as_nanos()
830        ));
831        let _ = std::fs::remove_dir_all(&dir);
832        std::fs::create_dir_all(&dir).unwrap();
833        let r = run_gs_continuum(&dir, "hills", 11, 3, 24, 24, 6, 1000).expect("continuum");
834        assert!(r.media_stored);
835        assert!(r.obj_bytes > 0);
836        assert!(r.container_10d_bytes > 64);
837        assert!(r.geometry_quins >= 1);
838        assert!(r.generation_quins == 3);
839        let obj = r.obj_path.as_ref().unwrap();
840        assert!(std::path::Path::new(obj).is_file());
841        let c10 = r.container_10d_path.as_ref().unwrap();
842        assert!(std::path::Path::new(c10).is_file());
843        let _ = std::fs::remove_dir_all(&dir);
844    }
845
846    #[test]
847    fn section15_smoke_passes() {
848        let s = section15_smoke().expect("§15");
849        assert!(s.contains("OK"));
850    }
851
852    #[test]
853    fn rgb_import_detects() {
854        let dir = std::env::temp_dir().join(format!(
855            "qv-imp-{}",
856            std::time::SystemTime::now()
857                .duration_since(std::time::UNIX_EPOCH)
858                .unwrap()
859                .as_nanos()
860        ));
861        let _ = std::fs::create_dir_all(&dir);
862        let mut rgb = vec![0u8; 16 * 16 * 3];
863        for p in rgb.chunks_mut(3) {
864            p[0] = 220;
865            p[1] = 20;
866            p[2] = 20;
867        }
868        let r = detect_from_rgb8(&dir, &rgb, 16, 16, "reference", true).unwrap();
869        assert!(r.n_pred >= 1);
870        assert!(r.shacl_ok);
871        let _ = std::fs::remove_dir_all(&dir);
872    }
873}
874
875/// Detect from raw RGB8 buffer (file import path).
876pub fn detect_from_rgb8(
877    storage_root: &std::path::Path,
878    rgb: &[u8],
879    width: u32,
880    height: u32,
881    backend: &str,
882    persist: bool,
883) -> Result<VisionDemoResult, String> {
884    if rgb.len() < (width as usize) * (height as usize) * 3 || width == 0 || height == 0 {
885        return Err("bad rgb geometry".into());
886    }
887    let img = ImageView {
888        bytes: rgb,
889        width,
890        height,
891        row_stride: width * 3,
892        format: PixelFormat::Rgb8,
893    };
894    let mut preds = [Detection::empty(); MAX_DETECTIONS];
895    let mut emb = [0.0f32; 32];
896    let mut ws = [0u8; MAX_DETECTIONS];
897    let use_prod = backend.eq_ignore_ascii_case("production")
898        || backend.eq_ignore_ascii_case("production_weights");
899    let (n_pred, model_hash, backend_kind, is_ref) = if use_prod {
900        let classes = [CLASS_MOSTLY_RED, CLASS_MOSTLY_GREEN, CLASS_MOSTLY_BLUE];
901        let bundle = VisionWeightBundle::from_seed(0x01D1_FACE_u64, 16, &classes);
902        let mh = bundle.model_hash();
903        let mut prod = ProductionVision::new(bundle);
904        let counts = prod
905            .infer(img, &mut preds, &mut emb, &mut ws)
906            .map_err(|e| format!("{e:?}"))?;
907        (counts.detections, mh, "production_weights", false)
908    } else {
909        let det = GridMultiObjectDetector::new(4, 3);
910        let n = det
911            .detect(img, 0, &mut preds, &mut ws)
912            .map_err(|e| format!("{e:?}"))?;
913        (n, det.model_hash(), "reference", true)
914    };
915    let mut tracker = BoundedTracker::new();
916    tracker.update(0, &mut preds, n_pred);
917    let digest = media_digest(rgb);
918    if persist {
919        let store = MediaStore::open(storage_root.join("vision_media")).map_err(|e| e)?;
920        let now = std::time::SystemTime::now()
921            .duration_since(std::time::UNIX_EPOCH)
922            .map(|d| d.as_secs())
923            .unwrap_or(0);
924        let _ = store.import_bytes(
925            rgb,
926            "image/x-rgb8",
927            width,
928            height,
929            RetentionClass::Restricted,
930            now,
931        );
932    }
933    let mut vquins = [VisionQuin::with_parity(0, 0, 0, 0, 0); 256];
934    let n_q = compile_observation_quins_full(digest, &preds[..n_pred], model_hash, &mut vquins);
935    let mut nq = Vec::new();
936    for q in vquins.iter().take(n_q) {
937        nq.push(vision_quin_to_nquin(q));
938    }
939    let report = validate_vision_observation_graph(&nq);
940    let mut rgba = vec![0u8; (width * height * 4) as usize];
941    compose_rgb_overlay_rgba8(
942        width,
943        height,
944        rgb,
945        &preds,
946        n_pred,
947        [0, 255, 180, 255],
948        2,
949        &mut rgba,
950    )
951    .map_err(|e| format!("{e:?}"))?;
952    let mut bmp = vec![0u8; 54 + rgba.len()];
953    let bmp_n = encode_bmp_rgba8(width, height, &rgba, &mut bmp).map_err(|e| format!("{e:?}"))?;
954    Ok(VisionDemoResult {
955        width,
956        height,
957        seed: 0,
958        split: "import".into(),
959        model_hash: format!("0x{:016x}", model_hash),
960        media_hash: format!("0x{:016x}", digest.hash),
961        detections: preds[..n_pred]
962            .iter()
963            .map(|d| det_to_dto(d, false))
964            .collect(),
965        n_gt: 0,
966        n_pred,
967        quins_written: n_q,
968        shacl_ok: report.ok,
969        shacl_observations: report.observation_count,
970        shacl_human: report.human_attestation_count,
971        overlay_data_url: format!("data:image/bmp;base64,{}", B64.encode(&bmp[..bmp_n])),
972        note: format!("RGB import path. Backend={backend_kind}. Epistemic only."),
973        backend: backend_kind.into(),
974        is_reference_backend: is_ref,
975        synthetic_match_acc: None,
976    })
977}
978
979/// Decode PNG/JPEG and detect.
980pub fn detect_from_image_file(
981    storage_root: &std::path::Path,
982    path: &std::path::Path,
983    backend: &str,
984) -> Result<VisionDemoResult, String> {
985    let img = image::open(path).map_err(|e| e.to_string())?;
986    let rgb = img.to_rgb8();
987    let (w, h) = rgb.dimensions();
988    detect_from_rgb8(storage_root, rgb.as_raw(), w, h, backend, true)
989}
990
991/// Query observation quins by normalized region (u16).
992pub fn query_vision_region(quins: &[VisionQuin], x0: u16, y0: u16, x1: u16, y1: u16) -> Vec<u64> {
993    let mut out = [0u64; 64];
994    let n = query_instances_in_region(quins, x0, y0, x1, y1, &mut out);
995    out[..n].to_vec()
996}
997
998/// §15 automated smoke: synthetic detect + SHACL + overlay.
999pub fn section15_smoke() -> Result<String, String> {
1000    let r = demo_test(0)?;
1001    if !r.shacl_ok {
1002        return Err("SHACL failed".into());
1003    }
1004    if r.n_pred == 0 && r.n_gt == 0 {
1005        return Err("no detections".into());
1006    }
1007    if !r.overlay_data_url.starts_with("data:image/bmp") {
1008        return Err("no overlay".into());
1009    }
1010    Ok(format!(
1011        "section15_smoke OK backend={} dets={} quins={}",
1012        r.backend, r.n_pred, r.quins_written
1013    ))
1014}
1015
1016/// Phase 11–12: twin eligibility + closed-form elasticity preview (A1, not FEA).
1017pub fn twin_elasticity_demo() -> Result<serde_json::Value, String> {
1018    use qualia_vision::spatial::{
1019        closed_form_bar_stretch, promote_elasticity_preview, refuse_fea_unless_eligible,
1020        run_elasticity_preview_if_eligible, BarStretchInput, MeshIR,
1021    };
1022    let mut m = MeshIR::empty();
1023    m.positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1024    m.indices = vec![0, 1, 2];
1025    m.recompute_bounds_and_hash();
1026    let viz = qualia_vision::spatial::assess_twin_eligibility(&m);
1027    let promoted = promote_elasticity_preview(&m);
1028    let refuse_viz = refuse_fea_unless_eligible(viz).is_err();
1029    let r = run_elasticity_preview_if_eligible(
1030        promoted,
1031        BarStretchInput {
1032            force_n: 1000.0,
1033            length_m: 1.0,
1034            area_m2: 0.01,
1035            youngs_pa: 2.0e11,
1036        },
1037    )
1038    .map_err(|e| e.to_string())?;
1039    let _ = closed_form_bar_stretch(BarStretchInput {
1040        force_n: 1.0,
1041        length_m: 1.0,
1042        area_m2: 1.0,
1043        youngs_pa: 1.0e9,
1044    });
1045    Ok(serde_json::json!({
1046        "viz_only_refuses_fea": refuse_viz,
1047        "promoted_domain": format!("{:?}", promoted.domain),
1048        "displacement_m": r.displacement_m,
1049        "assurance": r.assurance_note,
1050        "note": "A1 closed-form bar stretch only — not mesh FEA / not A4."
1051    }))
1052}
1053
1054/// Ensure seed QVWT on disk; load for production path.
1055pub fn ensure_vision_weights(storage_root: &std::path::Path) -> Result<String, String> {
1056    use qualia_vision::detector::{CLASS_MOSTLY_BLUE, CLASS_MOSTLY_GREEN, CLASS_MOSTLY_RED};
1057    let path = storage_root.join("models").join("vision_seed.qvwt");
1058    let classes = [CLASS_MOSTLY_RED, CLASS_MOSTLY_GREEN, CLASS_MOSTLY_BLUE];
1059    if path.is_file() {
1060        let b = VisionWeightBundle::load_path(&path, &classes)?;
1061        return Ok(format!(
1062            "loaded QVWT hash=0x{:016x} path={}",
1063            b.content_hash,
1064            path.display()
1065        ));
1066    }
1067    let b = VisionWeightBundle::from_seed(0x01D1_FACE_u64, 16, &classes);
1068    b.save_path(&path)?;
1069    Ok(format!(
1070        "wrote QVWT seed hash=0x{:016x} path={}",
1071        b.content_hash,
1072        path.display()
1073    ))
1074}
1075
1076/// Detect with QVWT from disk if present.
1077pub fn detect_with_disk_weights(
1078    storage_root: &std::path::Path,
1079    rgb: &[u8],
1080    width: u32,
1081    height: u32,
1082) -> Result<VisionDemoResult, String> {
1083    use qualia_vision::detector::{CLASS_MOSTLY_BLUE, CLASS_MOSTLY_GREEN, CLASS_MOSTLY_RED};
1084    let path = storage_root.join("models").join("vision_seed.qvwt");
1085    let classes = [CLASS_MOSTLY_RED, CLASS_MOSTLY_GREEN, CLASS_MOSTLY_BLUE];
1086    let bundle = if path.is_file() {
1087        VisionWeightBundle::load_path(&path, &classes)?
1088    } else {
1089        let b = VisionWeightBundle::from_seed(0x01D1_FACE_u64, 16, &classes);
1090        b.save_path(&path)?;
1091        b
1092    };
1093    let img = ImageView {
1094        bytes: rgb,
1095        width,
1096        height,
1097        row_stride: width * 3,
1098        format: PixelFormat::Rgb8,
1099    };
1100    let model_hash = bundle.content_hash;
1101    let mut prod = ProductionVision::new(bundle);
1102    let mut preds = [Detection::empty(); MAX_DETECTIONS];
1103    let mut emb = [0.0f32; 32];
1104    let mut ws = [0u8; MAX_DETECTIONS];
1105    let counts = prod
1106        .infer(img, &mut preds, &mut emb, &mut ws)
1107        .map_err(|e| format!("{e:?}"))?;
1108    let digest = media_digest(rgb);
1109    let mut tracker = BoundedTracker::new();
1110    tracker.update(0, &mut preds, counts.detections);
1111    let mut vquins = [VisionQuin::with_parity(0, 0, 0, 0, 0); 256];
1112    let n_q = compile_observation_quins_full(
1113        digest,
1114        &preds[..counts.detections],
1115        model_hash,
1116        &mut vquins,
1117    );
1118    let mut nq = Vec::new();
1119    for q in vquins.iter().take(n_q) {
1120        nq.push(vision_quin_to_nquin(q));
1121    }
1122    let report = validate_vision_observation_graph(&nq);
1123    let mut rgba = vec![0u8; (width * height * 4) as usize];
1124    compose_rgb_overlay_rgba8(
1125        width,
1126        height,
1127        rgb,
1128        &preds,
1129        counts.detections,
1130        [0, 255, 180, 255],
1131        2,
1132        &mut rgba,
1133    )
1134    .map_err(|e| format!("{e:?}"))?;
1135    let mut bmp = vec![0u8; 54 + rgba.len()];
1136    let bmp_n = encode_bmp_rgba8(width, height, &rgba, &mut bmp).map_err(|e| format!("{e:?}"))?;
1137    Ok(VisionDemoResult {
1138        width,
1139        height,
1140        seed: 0,
1141        split: "disk-qvwt".into(),
1142        model_hash: format!("0x{:016x}", model_hash),
1143        media_hash: format!("0x{:016x}", digest.hash),
1144        detections: preds[..counts.detections]
1145            .iter()
1146            .map(|d| det_to_dto(d, false))
1147            .collect(),
1148        n_gt: 0,
1149        n_pred: counts.detections,
1150        quins_written: n_q,
1151        shacl_ok: report.ok,
1152        shacl_observations: report.observation_count,
1153        shacl_human: report.human_attestation_count,
1154        overlay_data_url: format!("data:image/bmp;base64,{}", B64.encode(&bmp[..bmp_n])),
1155        note: "QVWT loaded from models/vision_seed.qvwt (seed weights, not foundation).".into(),
1156        backend: "production_weights".into(),
1157        is_reference_backend: false,
1158        synthetic_match_acc: None,
1159    })
1160}
1161
1162/// Biosense excellence demos (consent-bound recipes; no training).
1163#[derive(Debug, Clone, Serialize)]
1164pub struct BiosensePulseDemo {
1165    pub bpm: f32,
1166    pub confidence: f32,
1167    pub snr: f32,
1168    pub abstained: bool,
1169    pub used_evm: bool,
1170    pub reason: String,
1171}
1172
1173/// Synthetic pulse + optional EVM → rPPG (security consent template for demo).
1174pub fn biosense_self_monitor_pulse_demo(use_evm: bool) -> Result<BiosensePulseDemo, String> {
1175    use qualia_vision::{self_monitor_pulse_evm, synthetic_pulse_sequence, BiosenseConsent};
1176    let seq = synthetic_pulse_sequence(32, 32, 90, 30.0, 72.0).map_err(|e| format!("{e}"))?;
1177    let consent = BiosenseConsent::grant_security_template(1);
1178    let r = self_monitor_pulse_evm(
1179        consent,
1180        seq.as_packed_rgb(),
1181        seq.n_frames,
1182        seq.width,
1183        seq.height,
1184        seq.fps,
1185        use_evm,
1186        0.15,
1187    );
1188    Ok(BiosensePulseDemo {
1189        bpm: r.bpm,
1190        confidence: r.confidence,
1191        snr: r.snr,
1192        abstained: r.abstained,
1193        used_evm: r.used_evm,
1194        reason: r
1195            .reason
1196            .map(|a| format!("{a:?}"))
1197            .unwrap_or_else(|| "ok".into()),
1198    })
1199}
1200
1201/// Super-resolution ("Enhance") result: before/after PNG data URLs + honesty.
1202#[derive(Debug, Clone, serde::Serialize)]
1203pub struct SrResultDto {
1204    pub before_data_url: String,
1205    pub after_data_url: String,
1206    pub backend_id: String,
1207    pub device: String,
1208    pub generative: bool,
1209    pub out_width: u32,
1210    pub out_height: u32,
1211    pub degraded: bool,
1212}
1213
1214/// Decode arbitrary image bytes, classically super-resolve (Enhance), and return
1215/// before/after PNG data URLs plus device/honesty metadata.
1216///
1217/// Mirrors the Listen/audio_capabilities composition: decode → engine op →
1218/// PNG-encode both frames → DTO. Classical SR is non-generative (`Sharpen`).
1219pub fn super_resolve_image(
1220    bytes: &[u8],
1221    scale: u8,
1222    kernel: &str,
1223    prefer_gpu: bool,
1224) -> Result<SrResultDto, String> {
1225    use qualia_core_db::specialized_libs::computer_vision::cv::buffer::RgbView;
1226    use qualia_core_db::specialized_libs::computer_vision::cv::codecs::encode_png;
1227    use qualia_core_db::specialized_libs::computer_vision::gpu::dispatch::VisionComputeDevice;
1228    use qualia_core_db::specialized_libs::computer_vision::gpu::policy::{
1229        ThermalHint, VisionVramBudget,
1230    };
1231    use qualia_core_db::specialized_libs::computer_vision::sr::device_policy::super_resolve_with_policy;
1232    use qualia_core_db::specialized_libs::computer_vision::sr::super_resolve::{
1233        ClassicalKernel, EnhancementMode, SrBackend, SrRequest,
1234    };
1235
1236    if !(2..=4).contains(&scale) {
1237        return Err("scale must be 2..=4".into());
1238    }
1239
1240    // 1. Decode → RGB8.
1241    let img = image::load_from_memory(bytes).map_err(|e| format!("decode image: {e}"))?;
1242    let rgb_img = img.to_rgb8();
1243    let (w, h) = rgb_img.dimensions();
1244    if w == 0 || h == 0 {
1245        return Err("empty image".into());
1246    }
1247    let rgb: Vec<u8> = rgb_img.into_raw();
1248
1249    // 2. Map kernel string (same mapping as mcp vision.rs op).
1250    let ck = match kernel {
1251        "nearest" => ClassicalKernel::Nearest,
1252        "bilinear" => ClassicalKernel::Bilinear,
1253        "lanczos" | "lanczos3" => ClassicalKernel::Lanczos3,
1254        _ => ClassicalKernel::Bicubic,
1255    };
1256    let req = SrRequest {
1257        rgb: &rgb,
1258        width: w,
1259        height: h,
1260        scale,
1261        backend: SrBackend::Classical(ck),
1262        mode: EnhancementMode::Sharpen,
1263    };
1264
1265    // 3. Allocate output and run under device policy.
1266    let ow = w.checked_mul(scale as u32).ok_or("output width overflow")?;
1267    let oh = h
1268        .checked_mul(scale as u32)
1269        .ok_or("output height overflow")?;
1270    let out_len = (ow as usize)
1271        .checked_mul(oh as usize)
1272        .and_then(|n| n.checked_mul(3))
1273        .ok_or("output size overflow")?;
1274    let mut out = vec![0u8; out_len];
1275    let (report, compute) = super_resolve_with_policy(
1276        &req,
1277        prefer_gpu,
1278        ThermalHint::Cool,
1279        VisionVramBudget::default(),
1280        &mut out,
1281    )
1282    .map_err(|e| format!("super_resolve: {e:?}"))?;
1283
1284    // 4. PNG-encode both input and output.
1285    let before_view = RgbView::new(w, h, w.saturating_mul(3), &rgb).ok_or("bad input rgb view")?;
1286    let before_png = encode_png(before_view).map_err(|e| format!("encode before png: {e:?}"))?;
1287    let after_view = RgbView::new(
1288        report.out_width,
1289        report.out_height,
1290        ow.saturating_mul(3),
1291        &out,
1292    )
1293    .ok_or("bad output rgb view")?;
1294    let after_png = encode_png(after_view).map_err(|e| format!("encode after png: {e:?}"))?;
1295
1296    let device = match compute.device {
1297        VisionComputeDevice::Cpu => "cpu",
1298        VisionComputeDevice::SharedGpu => "shared_gpu",
1299        VisionComputeDevice::Unavailable => "unavailable",
1300    };
1301
1302    Ok(SrResultDto {
1303        before_data_url: format!("data:image/png;base64,{}", B64.encode(&before_png)),
1304        after_data_url: format!("data:image/png;base64,{}", B64.encode(&after_png)),
1305        backend_id: report.backend_id.to_string(),
1306        device: device.to_string(),
1307        generative: report.generative,
1308        out_width: report.out_width,
1309        out_height: report.out_height,
1310        degraded: compute.degraded,
1311    })
1312}
1313
1314/// Local CBIR proxy hashes for an RGB buffer (not CLIP).
1315pub fn vision_local_embed_demo(
1316    rgb: &[u8],
1317    width: u32,
1318    height: u32,
1319) -> Result<serde_json::Value, String> {
1320    use qualia_vision::{
1321        ahash_u64, color_hist_embed_rgb, dhash_u64, GrayView, RgbView, COLOR_HIST_EMBED_DIM,
1322    };
1323    let n = (width * height) as usize;
1324    if rgb.len() < n * 3 {
1325        return Err("buffer too small".into());
1326    }
1327    let mut gray = vec![0u8; n];
1328    for i in 0..n {
1329        let o = i * 3;
1330        gray[i] = ((rgb[o] as u16 + rgb[o + 1] as u16 + rgb[o + 2] as u16) / 3) as u8;
1331    }
1332    let g = GrayView::new(width, height, width, &gray).ok_or("bad gray view")?;
1333    let ah = ahash_u64(g).map_err(|e| format!("{e:?}"))?;
1334    let dh = dhash_u64(g).map_err(|e| format!("{e:?}"))?;
1335    let mut hist = [0.0f32; COLOR_HIST_EMBED_DIM];
1336    let rv = RgbView::new(width, height, width * 3, rgb).ok_or("bad rgb view")?;
1337    color_hist_embed_rgb(rv, &mut hist).map_err(|e| format!("{e:?}"))?;
1338    Ok(serde_json::json!({
1339        "ahash": format!("{:016x}", ah),
1340        "dhash": format!("{:016x}", dh),
1341        "hist_dim": COLOR_HIST_EMBED_DIM,
1342        "hist0": hist[0],
1343        "note": "local CBIR proxy; not foundation CLIP"
1344    }))
1345}