/** * DZ Phone Value — ONNX inference engine (static HuggingFace Space). * * Feature engineering + post-processing are ported 1:1 from the production * engine sp_repo/src/lib/model/predict.ts (sp10-a2-refined), which is pinned * by two gates: model_export/sp_parity_check.mjs (12 Python-exported fixtures, * 0.1%) and scripts/check-golden.ts (byte-exact API goldens). The tree * ensembles run through onnxruntime-web from model.onnx (point / q10 / q90 * log-price margins, base_values baked — ORT output equals the TS engine's * pLog exactly; see scripts/sp_export_onnx.py for the conversion contract). */ (function (global) { "use strict"; let ART = null; function init(artifacts) { ART = artifacts; } function getArtifacts() { return ART; } function modelAgeYears(releaseYear) { var launch = new Date(releaseYear, 5, 30).getTime(); // ~mid-year launch assumption var age = (Date.now() - launch) / (365.25 * 24 * 3600 * 1000); return Math.max(0, Math.min(14, age)); } /** Mirror of predict.ts buildFeatures() — same order, same truthiness. */ function buildFeatures(inp) { var a = ART; var m = a.medians; var f = {}; // numeric block f.log2_storage = inp.storageGb ? Math.log2(inp.storageGb) : m.log2_storage; f.storage_missing = inp.storageGb ? 0 : 1; f.ram_gb = (inp.ramGb === null || inp.ramGb === undefined) ? m.ram_gb : inp.ramGb; f.ram_missing = inp.ramGb ? 0 : 1; f.model_age_years = inp.releaseYear ? modelAgeYears(inp.releaseYear) : m.model_age_years; f.age_missing = inp.releaseYear ? 0 : 1; f.battery_health = (inp.batteryHealth === null || inp.batteryHealth === undefined) ? m.battery_health : inp.batteryHealth; f.battery_missing = inp.batteryHealth ? 0 : 1; f.desc_damage = inp.damage ? 1 : 0; f.desc_original_parts = inp.originalParts ? 1 : 0; f.is_5g = inp.is5g ? 1 : 0; f.dual_sim = inp.dualSim ? 1 : 0; f.acc_count = (inp.accessories.box ? 1 : 0) + (inp.accessories.charger ? 1 : 0) + (inp.accessories.case ? 1 : 0); f.log_repost = Math.log1p(1); // fresh listing // tier dummies (reference: tier_none) var tiers = ["base", "compact", "plus", "pro", "promax", "ultra"]; for (var i = 0; i < tiers.length; i++) f["tier_" + tiers[i]] = inp.tier === tiers[i] ? 1 : 0; // brand dummies (reference: other) for (var b = 0; b < a.brands_top.length; b++) f["brand_" + a.brands_top[b]] = inp.brand === a.brands_top[b] ? 1 : 0; // region dummies (reference: other) for (var r = 0; r < a.regions_top.length; r++) f["region_" + a.regions_top[r]] = inp.region === a.regions_top[r] ? 1 : 0; // priceType dummies (reference: NEGOTIABLE) var pts = ["FIXED", "NONE", "OFFERED"]; for (var p = 0; p < pts.length; p++) f["priceType_" + pts[p]] = inp.priceType === pts[p] ? 1 : 0; // condition dummies (reference: like_new) var conds = ["good", "new", "refurb", "used"]; for (var c = 0; c < conds.length; c++) f["condition_" + conds[c]] = inp.condition === conds[c] ? 1 : 0; return f; } /** b_cols-ordered float vector (the ONNX input). */ function vecFromFeatures(f) { return ART.b_cols.map(function (c) { return f[c]; }); } function runSession(session, vec48) { var t = new ort.Tensor("float32", Float32Array.from(vec48), [1, 48]); return session.run({ input: t }).then(function (o) { return { point: o.point.data[0], q10: o.q10.data[0], q90: o.q90.data[0] }; }); } /** predict.ts post-processing: smear, CQR Q, clamp, round to 100 DZD. */ function postProcess(p_log, q10_log, q90_log) { var a = ART; var price = Math.exp(p_log) * a.smear; var lo = Math.exp(q10_log - a.cqr_Q_log) * a.smear; var hi = Math.exp(q90_log + a.cqr_Q_log) * a.smear; lo = Math.min(lo, price); // display clamp: band must contain the point hi = Math.max(hi, price); // (it can only widen the band, never shrink) var round100 = function (x) { return Math.round(x / 100) * 100; }; return { price: round100(price), lo: round100(lo), hi: round100(hi), priceRaw: price }; } async function predictAsync(inp, session) { var f = buildFeatures(inp); var o = await runSession(session, vecFromFeatures(f)); var r = postProcess(o.point, o.q10, o.q90); r.features = f; r.modelVersion = ART.model_version; return r; } /** Parity bridge: raw 48-feature vector (b_cols order) -> UNROUNDED * price/lo/hi — mirrors predict.ts predictPriceRawFromFeatures (no * round100, no clamp) so fixture parity matches the app's 0.1% contract. */ async function predictFromVectorAsync(vec48, session) { var o = await runSession(session, vec48); var a = ART; return { price: Math.exp(o.point) * a.smear, lo: Math.exp(o.q10 - a.cqr_Q_log) * a.smear, hi: Math.exp(o.q90 + a.cqr_Q_log) * a.smear, }; } var api = { init: init, getArtifacts: getArtifacts, buildFeatures: buildFeatures, modelAgeYears: modelAgeYears, vecFromFeatures: vecFromFeatures, postProcess: postProcess, predictAsync: predictAsync, predictFromVectorAsync: predictFromVectorAsync }; global.__spEngine = api; if (typeof module !== "undefined" && module.exports) module.exports = api; })(typeof window !== "undefined" ? window : globalThis);