dz-phone-value / engine.js
bitsnaps's picture
DZ Phone Value static space: ONNX client-side estimator
4378390 verified
Raw History Blame Contribute Delete
5.34 kB
/**
* 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);