From 0f044deba73df0a295722829ec1a1ff8e7f156f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 6 Jun 2026 17:16:03 +0000 Subject: [PATCH 1/5] =?UTF-8?q?feat(pdr-011):=20declarative=20configurator?= =?UTF-8?q?=20=E2=80=94=20spec=20+=20reverse-engineered=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Author PDR-011 proposing a data-driven "Build-a-ZELEX" configurator: a UI generated from a spec rather than hand-coded, where buyer selections form a JSON build document that resolves to a catalogued architecture, SKU, signature, family, and price. - db/configurator_schema.json: declarative axis model (body/head/components), derivations, validation rules, and a build-document JSON Schema. Option domains are extracted from existing db/ data; large sets referenced by source. - db/configurator_examples.json: three strong-data bodies (ZG170C, ZK168B, ZX172E) reverse-engineered into build documents with truthful resolved values, plus a bespoke failure case. - docs/pdr/PDR-011-declarative-configurator.md: method, data grounding, rules, and acceptance review. No live UI yet — proves the model round-trips real bodies before building it. --- db/configurator_examples.json | 139 ++++++++++ db/configurator_schema.json | 225 +++++++++++++++++ docs/pdr/PDR-011-declarative-configurator.md | 252 +++++++++++++++++++ 3 files changed, 616 insertions(+) create mode 100644 db/configurator_examples.json create mode 100644 db/configurator_schema.json create mode 100644 docs/pdr/PDR-011-declarative-configurator.md diff --git a/db/configurator_examples.json b/db/configurator_examples.json new file mode 100644 index 0000000..0c44fc2 --- /dev/null +++ b/db/configurator_examples.json @@ -0,0 +1,139 @@ +{ + "_comment": "Reverse-engineered worked examples: three strong-data ZELEX architectures expressed as configurator build documents, each paired with the values its declarations resolve to. This is the proof that the declarative model in db/configurator_schema.json round-trips real catalogued bodies. `build` = buyer inputs only; `resolved` = output of the schema's derivations; `source` = where each fact came from. All numbers are transcribed from db/, not invented.", + "spec_version": "1.0.0", + "validated_against": "db/configurator_schema.json#/build_document_schema", + + "examples": [ + { + "id": "yvonne-zg170c", + "title": "I-Series Muse — ZG170C (e.g. Yvonne / Valerie cast)", + "why_chosen": "Full 17-field spec card; exact-confidence Muse; 59 live variants — the richest head pool to exercise head/face/tone axes.", + "build": { + "spec_version": "1.0.0", + "body": { "line": "I-Series", "height_cm": 170, "cup": "C", "material": "Silicone" }, + "head": { "head_code": "GE52_2", "face_variant": "Standard", "head_tone_match": true }, + "components": { "skin_tone": "Tan", "breast_type": "Standard", "standing_feet": false, "fingers": false } + }, + "resolved": { + "body_code": "ZG170C", + "family": "The Muse", + "family_confidence": "exact", + "WHR": 0.686, + "BWR": 1.357, + "signature": { + "upper_bust": 87.5, "under_bust": 71.5, "waist": 64.5, "hip": 94, + "neck": 33, "shoulder_width": 40, "arm_length": 68, "hand_length": 18.5, + "leg_length": 78, "foot_length": 23, "thigh_circ": 50, "calf_circ": 32, + "weight_kg": 36.1 + }, + "sku": "GE52_2+ZG170C-Tan", + "price": "3599.00", + "price_basis": "catalogued live variant (authoritative)", + "estimated": false + }, + "movable_jaw_alt": { + "note": "Switching face_variant → Movable Jaw resolves a different head+face and SKU, per db/product_overrides.json.", + "build_delta": { "head.head_code": "GE02_1", "head.face_variant": "Movable Jaw" }, + "resolved_sku": "GE02_1_2(GE46MJ)+ZG170C-Tan" + }, + "source": { + "signature": "db/body_measurements.json#/bodies/ZG170C", + "family_whr_bwr": "db/body_profiles.json#/profiles (ZG170C)", + "sku_price": "db/live_variants.json (body=ZG170C, head=GE52_2, tone=Tan)", + "movable_jaw": "db/product_overrides.json#/GE02_1" + } + }, + + { + "id": "somi-zk168b", + "title": "K-Series Muse — ZK168B (Somi cast)", + "why_chosen": "Full spec card; demonstrates the WHR/BWR classifier landing at a family boundary (WHR just under Icon, BWR low) yet still resolving to a precomputed family value — the case where you trust the stored classification over a re-derive.", + "build": { + "spec_version": "1.0.0", + "body": { "line": "K-Series", "height_cm": 168, "cup": "B", "material": "Silicone" }, + "head": { "head_code": "KE03_1", "face_variant": "Standard", "head_tone_match": true }, + "components": { "skin_tone": "Fair", "breast_type": "Standard", "standing_feet": false, "fingers": false } + }, + "resolved": { + "body_code": "ZK168B", + "family": "The Muse", + "family_confidence": "near", + "WHR": 0.649, + "BWR": 1.238, + "signature": { + "upper_bust": 78, "under_bust": 66, "waist": 63, "hip": 97, + "neck": 27.5, "shoulder_width": 34.5, "arm_length": 71, "hand_length": 16, + "leg_length": 81, "foot_length": 22.5, "thigh_circ": 51, "calf_circ": 33, + "weight_kg": 37.5 + }, + "sku": "KE03_1+ZK168B-1", + "price": "3869.10", + "price_basis": "catalogued live variant (authoritative)", + "estimated": false, + "classifier_note": "WHR 0.649 sits below the Muse floor (0.65) and inside the Icon band (0.60–0.65), but BWR 1.238 is below both. No exact containment → nearest-center wins → The Muse (near). This is why resolve_price prefers the stored db/body_profiles family over a live re-derive." + }, + "source": { + "signature": "db/body_measurements.json#/bodies/ZK168B", + "family_whr_bwr": "db/body_profiles.json#/profiles (ZK168B)", + "sku_price": "db/live_variants.json (body=ZK168B, head=KE03_1)" + } + }, + + { + "id": "zxe215-zx172e", + "title": "SLE Icon — ZX172E (ZXE215_1 cast)", + "why_chosen": "Exercises the SLE-only axes (TPE material, skeleton_version) and the version/tone SKU suffix; exact-confidence Icon. Shows the same architecture priced two ways across skeleton versions.", + "build": { + "spec_version": "1.0.0", + "body": { "line": "SLE 3.0", "height_cm": 172, "cup": "E", "material": "TPE", "skeleton_version": "SLE 3.0" }, + "head": { "head_code": "ZXE215_1", "face_variant": "Standard", "head_tone_match": true }, + "components": { "skin_tone": "Tan", "breast_type": "Standard", "standing_feet": false, "fingers": false } + }, + "resolved": { + "body_code": "ZX172E", + "family": "The Icon", + "family_confidence": "exact", + "WHR": 0.608, + "BWR": 1.504, + "signature": { + "upper_bust": 86.5, "under_bust": 67, "waist": 57.5, "hip": 94.5, + "neck": 32.5, "shoulder_width": 38, "arm_length": 70.5, "hand_length": 19, + "leg_length": 84.5, "foot_length": 21, "thigh_circ": 48.5, "calf_circ": 34, + "weight_kg": 35.7 + }, + "sku": "ZXE215_1+ZX172E-Tan-Sle3.0", + "price": "2000.00", + "price_basis": "catalogued live variant (authoritative)", + "estimated": false + }, + "skeleton_version_alt": { + "note": "Same body, head, tone, but SLE 2.0 skeleton resolves a separate, lower-priced SKU — proving skeleton_version is a real price axis, not cosmetic.", + "build_delta": { "body.skeleton_version": "SLE 2.0" }, + "resolved_sku": "ZXE203_W1+ZX172E-White-Sle2.0", + "resolved_price": "1600.00" + }, + "source": { + "signature": "db/body_measurements.json#/bodies/ZX172E", + "family_whr_bwr": "db/body_profiles.json#/profiles (ZX172E)", + "sku_price": "db/live_variants.json (body=ZX172E, head=ZXE215_1, tone=Tan, Sle3.0)" + } + } + ], + + "bespoke_example": { + "id": "bespoke-empress", + "title": "Bespoke combination — no catalogued architecture", + "why_chosen": "Shows the warn-path: a valid-looking selection that resolve_body_code cannot map to a real body_code.", + "build": { + "spec_version": "1.0.0", + "body": { "line": "K-Series", "height_cm": 175, "cup": "K", "material": "Silicone" }, + "head": { "head_code": "KE03_1", "face_variant": "Standard" }, + "components": { "skin_tone": "Fair" } + }, + "resolved": { + "body_code": null, + "validation_flags": ["bespoke_combination"], + "disposition": "Route to a bespoke inquiry (PDR-003 notify-me pattern), not an instant SKU. ZK175K is not a catalogued architecture." + } + } +} diff --git a/db/configurator_schema.json b/db/configurator_schema.json new file mode 100644 index 0000000..98acbcc --- /dev/null +++ b/db/configurator_schema.json @@ -0,0 +1,225 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "spec_version": "1.0.0", + "title": "ZELEX Declarative Configurator Spec", + "description": "A data-driven specification a configurator UI is GENERATED from (not hand-coded against). Each axis declares its control, type, option source, provenance, and price effect. A buyer's selections form a 'build document' that resolves — through the declarations in `derivations` — to a catalogued body architecture, a SKU, a measurement signature, a Body Family, and a price. See docs/pdr/PDR-011-declarative-configurator.md. Large option sets are referenced by data source rather than inlined, so the catalog stays the single source of truth.", + "generated_from": [ + "db/catalog.json", "db/body_measurements.json", "db/body_profiles.json", + "db/body_types.json", "db/live_variants.json", "db/product_overrides.json" + ], + + "data_sources": { + "catalog": { "file": "db/catalog.json", "note": "series + 255 head+body products" }, + "measurements": { "file": "db/body_measurements.json", "note": "spec-card measurements — source of truth for the signature" }, + "profiles": { "file": "db/body_profiles.json", "note": "WHR/BWR + Body Family classification per body_code" }, + "body_types": { "file": "db/body_types.json", "note": "architecture records (family, slots, photoshoots)" }, + "variants": { "file": "db/live_variants.json", "note": "396 live SKUs — head+body+tone → price" } + }, + + "provenance_legend": { + "catalog": "Enumerated directly from catalogued product data.", + "spec-card": "Read from db/body_measurements.json once a body_code resolves; not buyer-editable.", + "derived": "Computed by a rule in `derivations`; never stored as input.", + "proposed": "A real ZELEX option not yet structured in db/. Flagged so the team can decide to backfill it before this axis goes live." + }, + + "groups": [ + { + "id": "body", + "label": "Body Architecture", + "intent": "Selectors that RESOLVE to one catalogued architecture. They are filters, not free composition — every valid combination must map to an existing body_code (or be flagged bespoke).", + "axes": [ + { + "id": "line", "label": "Series / Line", "control": "select", "type": "string", + "required": true, "provenance": "catalog", + "enum": ["K-Series", "I-Series", "Fusion", "SLE 3.0", "The Classic", "The Sculpt"], + "code_prefix": { "K-Series": "ZK", "I-Series": ["ZG", "ZGX"], "Fusion": "ZF", "SLE 3.0": "ZX", "The Classic": "ZC", "The Sculpt": "ZS" }, + "status": { "The Classic": "in_development", "The Sculpt": "in_development" }, + "note": "Drives the body_code prefix and the head-pool naming grammar." + }, + { + "id": "height_cm", "label": "Height", "control": "slider", "type": "integer", + "unit": "cm", "required": true, "provenance": "catalog", + "min": 153, "max": 175, "step": 1, "default": 168, + "exclude_below": 120, "exclude_reason": "Torso / mini bodies are out of scope (is_torso filter in build_characters.py).", + "note": "Resolves with line+cup to a body_code. Not all heights exist for all lines." + }, + { + "id": "cup", "label": "Cup", "control": "select", "type": "string", + "required": true, "provenance": "catalog", + "enum": ["A", "B", "C", "D", "E", "F", "G", "J", "K"], + "default": "C" + }, + { + "id": "material", "label": "Material", "control": "radio", "type": "string", + "required": true, "provenance": "catalog", + "enum": ["Silicone", "TPE"], + "default_by_line": { "K-Series": "Silicone", "I-Series": "Silicone", "Fusion": "Silicone", "SLE 3.0": "TPE" }, + "note": "Catalog titles mark K/I/Fusion as Silicone; SLE skews TPE. Material is a price driver." + }, + { + "id": "skeleton_version", "label": "Skeleton Version", "control": "select", "type": "string", + "required": false, "provenance": "catalog", + "enum": ["SLE 2.0", "SLE 3.0"], "default": "SLE 3.0", + "visible_when": { "line": "SLE 3.0" }, + "note": "Only meaningful for the SLE line; appears in SKU suffix (…-Sle3.0)." + }, + { + "id": "body_code", "label": "Resolved Architecture", "control": "derived", "type": "string", + "provenance": "derived", "derivation_ref": "resolve_body_code" + }, + { + "id": "signature", "label": "Measurement Signature", "control": "derived", "type": "object", + "provenance": "spec-card", "source_ref": "measurements", + "fields": ["upper_bust", "under_bust", "waist", "hip", "neck", "shoulder_width", "arm_length", "hand_length", "leg_length", "foot_length", "thigh_circ", "calf_circ", "weight_kg"], + "unit": "cm (weight_kg in kg)", + "note": "Locked to the resolved body_code. Read-only — the buyer picks an architecture, not loose tape measures." + }, + { + "id": "family", "label": "Body Family", "control": "derived", "type": "string", + "provenance": "derived", "derivation_ref": "classify_family" + } + ] + }, + + { + "id": "head", + "label": "Head & Face", + "axes": [ + { + "id": "head_code", "label": "Head", "control": "gallery-select", "type": "string", + "required": true, "provenance": "catalog", + "enum_source": { "source": "catalog", "path": "products[*].head_code", "distinct": true, "filter": "matches selected line prefix" }, + "note": "163 distinct heads in catalog; rendered as an image gallery filtered to the line, not a 163-row dropdown." + }, + { + "id": "face_variant", "label": "Jaw / Face Build", "control": "radio", "type": "string", + "required": true, "provenance": "catalog", + "enum": ["Standard", "Movable Jaw"], + "default": "Standard", + "resolves": { "Movable Jaw": "appends MJ face_code (e.g. GE46MJ) per db/product_overrides.json" }, + "note": "~50 catalog variants carry a Movable-Jaw face_code. Availability depends on head_code." + }, + { + "id": "head_tone_match", "label": "Match head tone to body", "control": "toggle", "type": "boolean", + "required": false, "provenance": "catalog", "default": true, + "note": "When true, head skin_tone follows the body skin_tone component; otherwise selectable independently." + } + ] + }, + + { + "id": "components", + "label": "Components & Finish", + "axes": [ + { + "id": "skin_tone", "label": "Skin Tone", "control": "swatch", "type": "string", + "required": true, "provenance": "catalog", + "enum": ["Fair", "Tan", "White"], + "default": "Fair", + "note": "From live_variants tone field (Tan 228, Fair 72, White 23 SKUs); resolves into the SKU suffix (…-Tan)." + }, + { + "id": "breast_type", "label": "Breast Build", "control": "radio", "type": "string", + "required": false, "provenance": "catalog", + "enum": ["Standard", "Gel / Implant"], + "default": "Standard", + "note": "15 catalog SKUs reference an implant/gel build; offered where the body supports it." + }, + { + "id": "standing_feet", "label": "Standing Feet (foot bolts)", "control": "toggle", "type": "boolean", + "required": false, "provenance": "proposed", "default": false, + "note": "PROPOSED — a real ZELEX option not yet in db/. Backfill a per-body 'standing' flag before enabling." + }, + { + "id": "fingers", "label": "Articulated Fingers", "control": "toggle", "type": "boolean", + "required": false, "provenance": "proposed", "default": false, + "note": "PROPOSED — needs a per-body capability flag in db/ before going live." + } + ] + } + ], + + "derivations": [ + { + "id": "resolve_body_code", + "produces": "body.body_code", + "inputs": ["body.line", "body.height_cm", "body.cup"], + "rule": "code = code_prefix(line) + height_cm + cup; lookup must exist in db/body_measurements.json#/bodies. If absent, set body_code=null and raise the `bespoke_combination` validation flag.", + "example": "K-Series + 168 + B → ZK168B" + }, + { + "id": "classify_family", + "produces": "body.family", + "inputs": ["signature.waist", "signature.hip", "signature.upper_bust"], + "rule": "WHR = waist/hip; BWR = upper_bust/waist; choose the family whose whr_range and bwr_range both contain the point (db/body_profiles.json#/families). On no exact containment, pick nearest range-center and label confidence near|loose (see db/family_taxonomy.json#/confidence_labels).", + "note": "Prefer the precomputed value at db/body_profiles.json#/profiles[body_code] when present; recompute only for bespoke combinations." + }, + { + "id": "resolve_sku", + "produces": "resolved.sku", + "inputs": ["head.head_code", "head.face_variant", "body.body_code", "components.skin_tone", "body.skeleton_version"], + "rule": "sku = head_code [ + '(' + faceMJ + ')' if Movable Jaw ] + '+' + body_code [ + '-' + skin_tone ] [ + '-Sle' + version for SLE ]. Match against db/live_variants.json#/sku for an exact catalogued SKU.", + "example": "ZXE215_1 + ZX172E + Tan + SLE 3.0 → ZXE215_1+ZX172E-Tan-Sle3.0" + }, + { + "id": "resolve_price", + "produces": "resolved.price", + "inputs": ["resolved.sku", "body.family", "body.material"], + "rule": "If resolved.sku matches a live variant, use its price (authoritative). Otherwise estimate: base_by_line × material_factor × (1 + family_premium). Mark estimated:true.", + "pricing_model": { + "base_by_line_usd": { "SLE 3.0": 1800, "Fusion": 3200, "I-Series": 3400, "K-Series": 3600 }, + "material_factor": { "Silicone": 1.0, "TPE": 0.85 }, + "family_premium": { "The Classic": 0.20, "The Muse": 0.25, "The Icon": 0.30, "The Siren": 0.35, "The Empress": 0.30, "The Sculpt": 0.25 }, + "note": "Premiums mirror db/body_profiles.json. Catalogued SKU price always wins over the estimate." + } + } + ], + + "validation": [ + { "id": "bespoke_combination", "severity": "warn", "when": "resolve_body_code finds no catalogued body_code", "message": "This line + height + cup is not a catalogued architecture. Treat as a bespoke inquiry, not an instant SKU." }, + { "id": "mj_unsupported", "severity": "error", "when": "face_variant == 'Movable Jaw' and head_code has no MJ face_code in product_overrides", "message": "Selected head has no Movable-Jaw build." }, + { "id": "skeleton_scope", "severity": "error", "when": "skeleton_version is set and line != 'SLE 3.0'", "message": "Skeleton version applies to the SLE line only." }, + { "id": "torso_excluded", "severity": "error", "when": "height_cm < 120", "message": "Torso / mini bodies are out of scope." }, + { "id": "dev_line", "severity": "warn", "when": "line in ['The Classic','The Sculpt']", "message": "This line is in development — route to notify-me, not checkout (see PDR-003)." } + ], + + "build_document_schema": { + "$comment": "JSON Schema a saved build is validated against. Inputs only — derived fields live in `resolved` and are recomputed, never trusted from input.", + "type": "object", + "required": ["spec_version", "body", "head", "components"], + "properties": { + "spec_version": { "const": "1.0.0" }, + "body": { + "type": "object", + "required": ["line", "height_cm", "cup", "material"], + "properties": { + "line": { "enum": ["K-Series", "I-Series", "Fusion", "SLE 3.0", "The Classic", "The Sculpt"] }, + "height_cm": { "type": "integer", "minimum": 120, "maximum": 175 }, + "cup": { "enum": ["A", "B", "C", "D", "E", "F", "G", "J", "K"] }, + "material": { "enum": ["Silicone", "TPE"] }, + "skeleton_version": { "enum": ["SLE 2.0", "SLE 3.0"] } + } + }, + "head": { + "type": "object", + "required": ["head_code", "face_variant"], + "properties": { + "head_code": { "type": "string" }, + "face_variant": { "enum": ["Standard", "Movable Jaw"] }, + "head_tone_match": { "type": "boolean" } + } + }, + "components": { + "type": "object", + "required": ["skin_tone"], + "properties": { + "skin_tone": { "enum": ["Fair", "Tan", "White"] }, + "breast_type": { "enum": ["Standard", "Gel / Implant"] }, + "standing_feet": { "type": "boolean" }, + "fingers": { "type": "boolean" } + } + } + } + } +} diff --git a/docs/pdr/PDR-011-declarative-configurator.md b/docs/pdr/PDR-011-declarative-configurator.md new file mode 100644 index 0000000..31ad427 --- /dev/null +++ b/docs/pdr/PDR-011-declarative-configurator.md @@ -0,0 +1,252 @@ +# PDR-011: Declarative Configurator — Build-a-ZELEX from Catalog Data + +**Branch:** claude/zelex-config-ui-builder-rso32 +**Date:** 2026-06-06 +**Status:** Proposed (PDR + schema + worked examples; no live UI yet) + +--- + +## Decision + +**Build the ZELEX configurator as a *declarative* tool: a UI generated from a data +spec rather than hand-coded, where a buyer's selections form a JSON "build +document" that resolves — through declared rules — to a catalogued body +architecture, a SKU, a measurement signature, a Body Family, and a price.** + +The configurator must not let buyers invent arbitrary bodies. ZELEX's entire +positioning is *documented measurement* — every body is a measured architecture, +not a slider toy. So the body axes (line / height / cup) are **selectors that +resolve to an existing `body_code`**, which then locks in the spec-card +signature and family. Heads, faces, tone, and components compose on top to form a +SKU. This keeps the catalog as the single source of truth and makes the tool +honest about what is real (catalogued) versus bespoke (an inquiry, not a +checkout). + +This package proves the model is sound before any UI is built: + +- `db/configurator_schema.json` — the declarative spec the UI is generated from. +- `db/configurator_examples.json` — three strong-data bodies reverse-engineered + into build documents, with truthful resolved values. +- This PDR — the method, the data grounding, and the rules. + +--- + +## 1. Why declarative, and what "declarative" means here + +A hand-coded configurator hard-codes its options in markup and logic: a +``+recs.map(h=>``).join('')+``; + } if(a.control==='select' || a.control==='gallery-select'){ - const opts = a.id==='head_code' ? headsForLine(st.body.line) : (a.enum||[]); + const opts = a.enum||[]; return ``; } if(a.control==='radio'){ - return `
`+(a.enum||[]).map(o=>``).join('')+`
`; + // face_variant is gated to what the chosen head actually supports + let opts = a.enum||[]; + if(a.id==='face_variant'){ const r=headRec(st.head.head_code); if(r) opts = r.face_variants; } + return `
`+opts.map(o=>``).join('')+`
`; } if(a.control==='swatch'){ const tones={Fair:'#e9d4bf',Tan:'#c79a6e',White:'#f1e6da'}; @@ -309,6 +390,22 @@

${ZX.esc(g.label)}

const ratio = fam.WHR!=null ? `WHR ${fam.WHR} · BWR ${fam.BWR}` : ''; + const hr = headRec(st.head.head_code); + const crossLine = !!(hr && hr.line && hr.line !== st.body.line); + const headThumb = hr ? (hr.representative_image + ? `${ZX.esc(hr.head_code)}` + : `${ZX.esc(hr.head_code.charAt(0))}`) : ''; + const neckBit = hr && hr.neck_class ? ` · neck ${ZX.esc(hr.neck_class)}` : ''; + const lineBit = hr ? (crossLine + ? `${ZX.esc(hr.line)} → ${ZX.esc(st.body.line)} body · custom` + : ZX.esc(hr.line)) : ''; + const headBlock = hr ? `
+ ${headThumb} + ${ZX.esc(hr.head_code)} + ${ZX.esc(st.head.face_variant)}${neckBit} + ${lineBit}${hr.characters.length?` · ${hr.characters.length} cast`:''} +
` : ''; + const noticeHtml = notices.length ? `
`+notices.map(n=>`
${ZX.esc(n.msg)}
`).join('')+`
` : ''; return `
@@ -317,6 +414,7 @@

${ZX.esc(g.label)}

${code?ZX.esc(code):'— bespoke —'}
${famChip}${st.body.height_cm}cm · ${ZX.esc(st.body.cup)}-cup · ${ZX.esc(st.body.material)}
${ratio}
+ ${headBlock}
${sig ? `
Measurement signature
${sigRows}
` : @@ -375,16 +473,37 @@

Configure from the catalog

st[group][id] = value; // changing line can invalidate the head + skeleton scope → reconcile, then full repaint if(id==='line'){ - const heads = headsForLine(value); - if(!heads.includes(st.head.head_code)) st.head.head_code = heads[0] || st.head.head_code; + const hs = headsForLine(value, resolveBodyCode(st.body)); + if(!hs.includes(st.head.head_code)) st.head.head_code = hs[0] || st.head.head_code; if(value!=='SLE 3.0') delete st.body.skeleton_version; else if(!st.body.skeleton_version) st.body.skeleton_version = axisById.skeleton_version.default; paint(); + } else if(id==='head_code'){ + // a new head can change which face builds (Standard / Movable Jaw) are valid + const r = headRec(value); + if(r && !r.face_variants.includes(st.head.face_variant)) st.head.face_variant = 'Standard'; + document.querySelectorAll('.head-gallery .hg').forEach(b=>b.classList.toggle('on', b.dataset.v===value)); + rerenderAxis('face_variant'); + repaintLive(); + } else if(id==='cross_line_heads'){ + // toggling interchangeability changes the head pool; if turning off and the current + // head is now out of pool (cross-line), fall back to the first same-line head + const pool = headsForLine(st.body.line, resolveBodyCode(st.body), value); + if(!pool.includes(st.head.head_code)) st.head.head_code = pool[0] || st.head.head_code; + rerenderAxis('head_code'); + repaintLive(); } else { repaintLive(); syncControlChrome(); } } + // replace one axis's control in place (used when head choice re-gates face_variant) + function rerenderAxis(id){ + const wrap = document.querySelector(`[data-wrap="${id}"]`); + if(!wrap || !visible(axisById[id])) return; + wrap.outerHTML = axisHtml(axisById[id]); + wireAxis(document.querySelector(`[data-wrap="${id}"]`)); + } // keep value-chips / toggle labels / segmented 'on' states in sync without a full repaint function syncControlChrome(){ document.querySelectorAll('[data-wrap]').forEach(w=>{ @@ -397,23 +516,28 @@

Configure from the catalog

}); } - function wire(){ - app.querySelectorAll('select[data-axis]').forEach(el=>el.addEventListener('change',e=>{ - const a=axisById[el.dataset.axis]; set(a._group,a.id,e.target.value); + function wireAxis(root){ + if(!root) return; + root.querySelectorAll('select[data-axis]').forEach(el=>el.addEventListener('change',()=>{ + const a=axisById[el.dataset.axis]; set(a._group,a.id,el.value); + })); + root.querySelectorAll('input[type=range][data-axis]').forEach(el=>el.addEventListener('input',()=>{ + const a=axisById[el.dataset.axis]; set(a._group,a.id, a.type==='integer'?parseInt(el.value,10):+el.value); })); - app.querySelectorAll('input[type=range][data-axis]').forEach(el=>el.addEventListener('input',e=>{ - const a=axisById[el.dataset.axis]; set(a._group,a.id, a.type==='integer'?parseInt(e.target.value,10):+e.target.value); + root.querySelectorAll('.seg[data-axis] button').forEach(el=>el.addEventListener('click',()=>{ + const a=axisById[el.closest('.seg').dataset.axis]; set(a._group,a.id, el.dataset.v); })); - app.querySelectorAll('.seg[data-axis] button').forEach(el=>el.addEventListener('click',e=>{ - const seg=el.closest('.seg'); const a=axisById[seg.dataset.axis]; set(a._group,a.id, el.dataset.v); + root.querySelectorAll('.swatches[data-axis] .swatch').forEach(el=>el.addEventListener('click',()=>{ + const a=axisById[el.closest('.swatches').dataset.axis]; set(a._group,a.id, el.dataset.v); })); - app.querySelectorAll('.swatches[data-axis] .swatch').forEach(el=>el.addEventListener('click',()=>{ - const sw=el.closest('.swatches'); const a=axisById[sw.dataset.axis]; set(a._group,a.id, el.dataset.v); + root.querySelectorAll('.head-gallery[data-axis] .hg').forEach(el=>el.addEventListener('click',()=>{ + set('head','head_code', el.dataset.v); })); - app.querySelectorAll('input[type=checkbox][data-axis]').forEach(el=>el.addEventListener('change',e=>{ - const a=axisById[el.dataset.axis]; set(a._group,a.id, e.target.checked); + root.querySelectorAll('input[type=checkbox][data-axis]').forEach(el=>el.addEventListener('change',()=>{ + const a=axisById[el.dataset.axis]; set(a._group,a.id, el.checked); })); } + function wire(){ app.querySelectorAll('[data-wrap]').forEach(wireAxis); } paint(); } diff --git a/db/configurator_schema.json b/db/configurator_schema.json index 98acbcc..3e3a1b0 100644 --- a/db/configurator_schema.json +++ b/db/configurator_schema.json @@ -104,6 +104,12 @@ "id": "head_tone_match", "label": "Match head tone to body", "control": "toggle", "type": "boolean", "required": false, "provenance": "catalog", "default": true, "note": "When true, head skin_tone follows the body skin_tone component; otherwise selectable independently." + }, + { + "id": "cross_line_heads", "label": "Interchangeable heads (cross-line)", "control": "toggle", "type": "boolean", + "required": false, "provenance": "proposed", "default": false, + "compat_basis": "neck_class (db/heads.json#/neck_classes) — heads attach at the neck; the connector size, not the marketing line, is the constraint.", + "note": "Show every head that physically fits this body's neck connector, across lines. A cross-line pairing is a CUSTOM build (neck-class is estimated from spec cards), never a catalogued SKU — it resolves to a bespoke inquiry." } ] }, @@ -179,6 +185,8 @@ "validation": [ { "id": "bespoke_combination", "severity": "warn", "when": "resolve_body_code finds no catalogued body_code", "message": "This line + height + cup is not a catalogued architecture. Treat as a bespoke inquiry, not an instant SKU." }, { "id": "mj_unsupported", "severity": "error", "when": "face_variant == 'Movable Jaw' and head_code has no MJ face_code in product_overrides", "message": "Selected head has no Movable-Jaw build." }, + { "id": "neck_incompatible", "severity": "error", "when": "head neck_class != body neck_class", "message": "This head's neck connector does not fit this body — different neck class." }, + { "id": "cross_line_head", "severity": "warn", "when": "head line != body line (but neck_class matches)", "message": "Cross-line head pairing — neck-compatible but not a catalogued combination. Custom build / bespoke inquiry, not an instant SKU. Neck fit is estimated; confirm before order." }, { "id": "skeleton_scope", "severity": "error", "when": "skeleton_version is set and line != 'SLE 3.0'", "message": "Skeleton version applies to the SLE line only." }, { "id": "torso_excluded", "severity": "error", "when": "height_cm < 120", "message": "Torso / mini bodies are out of scope." }, { "id": "dev_line", "severity": "warn", "when": "line in ['The Classic','The Sculpt']", "message": "This line is in development — route to notify-me, not checkout (see PDR-003)." } @@ -207,7 +215,8 @@ "properties": { "head_code": { "type": "string" }, "face_variant": { "enum": ["Standard", "Movable Jaw"] }, - "head_tone_match": { "type": "boolean" } + "head_tone_match": { "type": "boolean" }, + "cross_line_heads": { "type": "boolean" } } }, "components": { diff --git a/db/heads.json b/db/heads.json new file mode 100644 index 0000000..1762f89 --- /dev/null +++ b/db/heads.json @@ -0,0 +1,4512 @@ +{ + "version": "1.0.0", + "_comment": "Head catalog reverse-engineered from products + variants + characters + movable-jaw overrides (scripts/build_heads.py). One record per head sculpt: line, movable-jaw availability, skin tones, paired bodies, cast characters, a representative image, and price range. Feeds the configurator head gallery (PDR-011). Regenerate after build_characters.py / make_thumbs.py.", + "generated_from": [ + "db/catalog.json", + "db/live_variants.json", + "db/product_overrides.json", + "db/characters.json" + ], + "series_line_map": { + "Fusion": "Fusion", + "I": "I-Series", + "K": "K-Series", + "SLE": "SLE 3.0" + }, + "neck_classes": { + "_basis": "Neck-connector class is the interchangeability constraint, not the marketing line. Threshold: neck circumference < 29.0cm = 'slim', else 'standard'. ESTIMATED from spec-card neck measurements — confirm against the physical neck joint.", + "slim": "Narrow connector — K-Series (~27cm).", + "standard": "Standard connector — Fusion / I-Series / SLE 3.0 (~31-33cm). Heads here are mutually swappable." + }, + "count": 189, + "heads": { + "GE02_1": { + "head_code": "GE02_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE46MJ" + ], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [ + "I-ZG170C-01" + ], + "product_count": 1, + "representative_image": "assets/thumbs/I-Series/GE02_1/GE02_1-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE03_1": { + "head_code": "GE03_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE70MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C", + "ZG175E" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/I-Series/GE03_1/GE03_1-101.jpg", + "price_range": [ + 3599.0, + 3899.0 + ] + }, + "GE03_2": { + "head_code": "GE03_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE70MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C", + "ZG175E" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/I-Series/GE03_2/GE03_2-101.jpg", + "price_range": [ + 3599.0, + 3899.0 + ] + }, + "GE07_1": { + "head_code": "GE07_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/I-Series/GE07_1_ZG170C/GE07_1_ZG170C-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE07_3": { + "head_code": "GE07_3", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE07_3/GE07_3-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE100_1": { + "head_code": "GE100_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE100_1/GE100_1-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE101_1": { + "head_code": "GE101_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 1, + "representative_image": null, + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE109_1": { + "head_code": "GE109_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE109_1/GE109_1-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE111_1": { + "head_code": "GE111_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 3, + "representative_image": "assets/I-Series/GE111_1/GE111_1-109.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE111_2": { + "head_code": "GE111_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE111_2/GE111_2-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE111_3": { + "head_code": "GE111_3", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE111_3_ZGE175E/GE111_3_ZGE175E-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE113_1": { + "head_code": "GE113_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE50MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE113_1_GE50MJ/GE113_1_GE50MJ_-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE114_1": { + "head_code": "GE114_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE52MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/I-Series/GE114_1/GE114_1-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE115_1": { + "head_code": "GE115_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE55MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C", + "ZG175E" + ], + "characters": [ + "I-ZG175E-01" + ], + "product_count": 2, + "representative_image": "assets/thumbs/I-Series/GE115_1/GE115_1-101.jpg", + "price_range": [ + 3599.0, + 3899.0 + ] + }, + "GE116_1": { + "head_code": "GE116_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE49MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/I-Series/GE116_1/GE116_1-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE122_1": { + "head_code": "GE122_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE122_1/GE122_1-115.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE122_2": { + "head_code": "GE122_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE122_2_ZG170C/GE122_2_ZG170C-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE123_1": { + "head_code": "GE123_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE123_1/GE123_1-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE124_1": { + "head_code": "GE124_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE124_1/GE124_1-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE124_2": { + "head_code": "GE124_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE124_2_ZG170C/GE124_2_ZG170C-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE125_1": { + "head_code": "GE125_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C", + "ZGX165F" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/I-Series/GE125_1/GE125_1-102.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE125_2": { + "head_code": "GE125_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE125_2/GE125_2-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE125_3": { + "head_code": "GE125_3", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZGX165F" + ], + "characters": [], + "product_count": 0, + "representative_image": null, + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE127_1": { + "head_code": "GE127_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE78MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170D" + ], + "characters": [ + "I-ZG170D-01" + ], + "product_count": 1, + "representative_image": "assets/thumbs/I-Series/GE127_1_GE78MJ_ZG170D/GE127_1_GE78MJ_ZG170D-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE134_1": { + "head_code": "GE134_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE122MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZGX165F" + ], + "characters": [ + "I-ZGX165F-03" + ], + "product_count": 1, + "representative_image": "assets/thumbs/I-Series/GE134_1_GE122MJ_ZGX165F/GE134_1_GE122MJ_ZGX165F-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE137_1": { + "head_code": "GE137_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE125MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE137_1_GE125MJ_ZG175E/GE137_1_GE125MJ_ZG175E-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE138_1": { + "head_code": "GE138_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE106MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG175E" + ], + "characters": [ + "I-ZG175E-03" + ], + "product_count": 1, + "representative_image": "assets/thumbs/I-Series/GE138_1_GE106MJ_ZG175E/GE138_1_GE106MJ_ZG175E-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE139_1": { + "head_code": "GE139_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE08MJ" + ], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C", + "ZGX165F" + ], + "characters": [ + "I-ZGX165F-02" + ], + "product_count": 2, + "representative_image": "assets/thumbs/I-Series/GE139_1_GE08MJ_ZGX165F/GE139_1_GE08MJ_ZGX165F-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE139_2": { + "head_code": "GE139_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZGX165F" + ], + "characters": [], + "product_count": 0, + "representative_image": null, + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE141_1": { + "head_code": "GE141_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE72MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZGX165F" + ], + "characters": [ + "I-ZGX165F-01" + ], + "product_count": 1, + "representative_image": "assets/thumbs/I-Series/GE141_1_GE72MJ_ZGX165F/GE141_1_GE72MJ_ZGX165F-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE146_1": { + "head_code": "GE146_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE88MJ" + ], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE146_1_GE88MJ_ZG170C/GE146_1_GE88MJ_ZG170C-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE149_1": { + "head_code": "GE149_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE74MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170D" + ], + "characters": [ + "I-ZG170D-02" + ], + "product_count": 1, + "representative_image": "assets/thumbs/I-Series/GE149_1_GE74MJ_ZG170D/GE149_1_GE74MJ_ZG170D-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE14_1": { + "head_code": "GE14_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE109MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE14_1/GE14_1-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE14_2": { + "head_code": "GE14_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE109MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE14_2/GE14_2-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE14_3": { + "head_code": "GE14_3", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE109MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZGX165F" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE14_3/GE14_3-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE16_1": { + "head_code": "GE16_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE76MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE16_1/GE16_1-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE16_2": { + "head_code": "GE16_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE76MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE16_2/GE16_2-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE173_1": { + "head_code": "GE173_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE110MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE173_1_GE110MJ_ZG170C/GE173_1_GE110MJ_ZG170C-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE24_1": { + "head_code": "GE24_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE24_1_ZG170C/GE24_1_ZG170C-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE41_1": { + "head_code": "GE41_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE98MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG175E" + ], + "characters": [ + "I-ZG175E-02" + ], + "product_count": 2, + "representative_image": "assets/thumbs/I-Series/GE41_1_GE98MJ_ZG175E/GE41_1_GE98MJ_ZG175E-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE45_1": { + "head_code": "GE45_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/I-Series/GE45_1/GE45_1-105.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE45_2": { + "head_code": "GE45_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE45_2/GE45_2-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE45_5": { + "head_code": "GE45_5", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE45_5/GE45_5-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE45_6": { + "head_code": "GE45_6", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE45_6/GE45_6-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE45_7": { + "head_code": "GE45_7", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE45_7_ZG175E/GE45_7_ZG175E-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE45_8": { + "head_code": "GE45_8", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZGX165F" + ], + "characters": [ + "I-ZGX165F-04" + ], + "product_count": 1, + "representative_image": "assets/thumbs/I-Series/GE45_8_ZGX165F/GE45_8_ZGX165F-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE46_2": { + "head_code": "GE46_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE46_2/GE46_2-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE46_3": { + "head_code": "GE46_3", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE46_3/GE46_3-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE46_4": { + "head_code": "GE46_4", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE46_4/GE46_4-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE46_5": { + "head_code": "GE46_5", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE46_5/GE46_5-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE47_1": { + "head_code": "GE47_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 31.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG162D" + ], + "characters": [ + "I-ZG162D-01" + ], + "product_count": 1, + "representative_image": "assets/thumbs/I-Series/GE47_1_ZG162D/GE47_1_ZG162D-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE48_1": { + "head_code": "GE48_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE48_1/GE48_1-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE48_2": { + "head_code": "GE48_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE48_2/GE48_2-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE49_1": { + "head_code": "GE49_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE49_1/GE49_1-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE49_2": { + "head_code": "GE49_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE49_2/GE49_2-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE50_1": { + "head_code": "GE50_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE50_1/GE50_1-123.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE51_2": { + "head_code": "GE51_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE51_2/GE51_2-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE52_1": { + "head_code": "GE52_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [ + "I-ZG170C-03" + ], + "product_count": 1, + "representative_image": "assets/thumbs/I-Series/GE52_1/GE52_1-108.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE52_2": { + "head_code": "GE52_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE52_2/GE52_2-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE53_1": { + "head_code": "GE53_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [ + "I-ZG170C-04" + ], + "product_count": 1, + "representative_image": "assets/thumbs/I-Series/GE53_1/GE53_1-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE53_2": { + "head_code": "GE53_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE53_2/GE53_2-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE53_3": { + "head_code": "GE53_3", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [ + "I-ZG170C-02" + ], + "product_count": 2, + "representative_image": "assets/thumbs/I-Series/GE53_3/GE53_3-108.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE55_3": { + "head_code": "GE55_3", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE55_3/GE55_3-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE58_1": { + "head_code": "GE58_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE58_1_ZG175E/GE58_1_ZG175E-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE59_1": { + "head_code": "GE59_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE58MJ" + ], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE59_1_GE58MJ_ZG175E/GE59_1_GE58MJ_ZG175E-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE60_1": { + "head_code": "GE60_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 31.5, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE06MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG162D" + ], + "characters": [ + "I-ZG162D-02" + ], + "product_count": 1, + "representative_image": "assets/thumbs/I-Series/GE60_1_GE06MJ_ZG162D/GE60_1_GE06MJ_ZG162D-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE64_1": { + "head_code": "GE64_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE24MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZGX165F" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE64_1_GE24MJ_ZGX165F/GE64_1_GE24MJ_ZGX165F-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE68_1": { + "head_code": "GE68_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 31.5, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE37MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG162D" + ], + "characters": [ + "I-ZG162D-03" + ], + "product_count": 1, + "representative_image": "assets/thumbs/I-Series/GE68_1_GE37MJ_ZG162D/GE68_1_GE37MJ_ZG162D-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE76_1": { + "head_code": "GE76_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE76_1/GE76_1-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE78_1": { + "head_code": "GE78_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C", + "ZG175E" + ], + "characters": [], + "product_count": 3, + "representative_image": "assets/I-Series/GE78_1/GE78_1-101.jpg", + "price_range": [ + 3599.0, + 3899.0 + ] + }, + "GE78_2": { + "head_code": "GE78_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE78_2/GE78_2-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE81_1": { + "head_code": "GE81_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZGX165F" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE81_1/GE81_1-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE82_1": { + "head_code": "GE82_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE47MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG175E" + ], + "characters": [ + "I-ZG175E-04" + ], + "product_count": 1, + "representative_image": "assets/thumbs/I-Series/GE82_1_GE47MJ_ZG175E/GE82_1_GE47MJ_ZG175E-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE86_1": { + "head_code": "GE86_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZGX165F" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE86_1/GE86_1-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE88_2": { + "head_code": "GE88_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE88_2_ZG170C/GE88_2_ZG170C-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE94_1": { + "head_code": "GE94_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE94_1/GE94_1-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE94_2": { + "head_code": "GE94_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE45MJ" + ], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE94_2/GE94_2-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE95_1": { + "head_code": "GE95_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE53MJ" + ], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE95_1/GE95_1-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE95_2": { + "head_code": "GE95_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE53MJ" + ], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE95_2/GE95_2-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE95_3": { + "head_code": "GE95_3", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE53MJ" + ], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE95_3/GE95_3-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE95_4": { + "head_code": "GE95_4", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE53MJ" + ], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE95_4_GE53MJ_ZG175E/GE95_4_GE53MJ_ZG175E-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE95_5": { + "head_code": "GE95_5", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE53MJ" + ], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE95_5_ZG175E/GE95_5_ZG175E-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE95_6": { + "head_code": "GE95_6", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE53MJ" + ], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE95_6_GE53MJ_ZG170C/GE95_6_GE53MJ_ZG170C-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE95_7": { + "head_code": "GE95_7", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE53MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE95_7_GE53MJ_ZG170C/GE95_7_GE53MJ_ZG170C-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE97_1": { + "head_code": "GE97_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard", + "Movable Jaw" + ], + "mj_face_codes": [ + "GE07MJ" + ], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE97_1/GE97_1-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "GE98_1": { + "head_code": "GE98_1", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 30, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZG175E" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/I-Series/GE98_1/GE98_1-101.jpg", + "price_range": [ + 3899.0, + 3899.0 + ] + }, + "GE98_2": { + "head_code": "GE98_2", + "line": "I-Series", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZG170C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/I-Series/GE98_2_ZG170C/GE98_2_ZG170C-101.jpg", + "price_range": [ + 3599.0, + 3599.0 + ] + }, + "KE01_1": { + "head_code": "KE01_1", + "line": "K-Series", + "neck_class": "slim", + "neck_cm": 27.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZK168B" + ], + "characters": [ + "K-ZK168B-01" + ], + "product_count": 2, + "representative_image": "assets/thumbs/K-Series/KE01_1+ZK168B-1/KE01_1_ZK168B-101.jpg", + "price_range": [ + 3869.1, + 3869.1 + ] + }, + "KE02_1": { + "head_code": "KE02_1", + "line": "K-Series", + "neck_class": "slim", + "neck_cm": 27.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZK159D", + "ZK168B" + ], + "characters": [ + "K-ZK159D-01", + "K-ZK168B-03" + ], + "product_count": 2, + "representative_image": "assets/thumbs/K-Series/KE02_1+ZK168B-1/KE02_1_ZK168B-101.jpg", + "price_range": [ + 3869.1, + 3869.1 + ] + }, + "KE03_1": { + "head_code": "KE03_1", + "line": "K-Series", + "neck_class": "slim", + "neck_cm": 27.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZK168B" + ], + "characters": [ + "K-ZK168B-02" + ], + "product_count": 2, + "representative_image": "assets/thumbs/K-Series/KE03_1+ZK168B-2/KE03_1_ZK168B-101.jpg", + "price_range": [ + 3869.1, + 3869.1 + ] + }, + "ZFE01_1": { + "head_code": "ZFE01_1", + "line": "Fusion", + "neck_class": "standard", + "neck_cm": 31, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZF161D", + "ZF168B" + ], + "characters": [ + "Fusion-ZF161D-01", + "Fusion-ZF168B-01" + ], + "product_count": 2, + "representative_image": "assets/thumbs/Fusion-Series/ZFE01_1+ZF168B/ZFE01_1+ZF168B-101.jpg", + "price_range": [ + 4299.0, + 4299.0 + ] + }, + "ZFE01_2": { + "head_code": "ZFE01_2", + "line": "Fusion", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZF169C" + ], + "characters": [ + "Fusion-ZF169C-01" + ], + "product_count": 1, + "representative_image": "assets/thumbs/Fusion-Series/ZFE01_2+ZF169C/ZFE01_2+ZF169C-101.jpg", + "price_range": [ + 4299.0, + 4299.0 + ] + }, + "ZFE03_1": { + "head_code": "ZFE03_1", + "line": "Fusion", + "neck_class": "standard", + "neck_cm": 31, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZF168B" + ], + "characters": [ + "Fusion-ZF168B-02" + ], + "product_count": 1, + "representative_image": "assets/thumbs/Fusion-Series/ZFE03_1+ZF168B/ZFE03_1+ZF168B-101.jpg", + "price_range": [ + 4299.0, + 4299.0 + ] + }, + "ZFE05_1": { + "head_code": "ZFE05_1", + "line": "Fusion", + "neck_class": "standard", + "neck_cm": 31, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Fair" + ], + "bodies": [ + "ZF168B" + ], + "characters": [ + "Fusion-ZF168B-03" + ], + "product_count": 1, + "representative_image": "assets/thumbs/Fusion-Series/ZFE05_1+ZF168B/ZFE05_1_ZF168B-101.jpg", + "price_range": [ + 4299.0, + 4299.0 + ] + }, + "ZX201_2": { + "head_code": "ZX201_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX172E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZX201_2_ZX172E/ZX201_2_ZX172E-102.jpg", + "price_range": null + }, + "ZX204_1": { + "head_code": "ZX204_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX172E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZX204_1_ZX172E/ZX204_1_ZX172E-102.jpg", + "price_range": null + }, + "ZX207_1": { + "head_code": "ZX207_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX172E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZX207_1_ZX172E/ZX207_1_ZX172E-105.jpg", + "price_range": null + }, + "ZXE200_1": { + "head_code": "ZXE200_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX164G", + "ZX165D" + ], + "characters": [ + "SLE-ZX165D-04" + ], + "product_count": 2, + "representative_image": "assets/thumbs/SLE-Series/ZXE200_1_ZX165D/ZXE200_1_ZX165D-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE200_2": { + "head_code": "ZXE200_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX160J" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE200_2_ZX160J/ZXE200_2_ZX160J-104.jpg", + "price_range": [ + 2000.0, + 2000.0 + ] + }, + "ZXE200_3": { + "head_code": "ZXE200_3", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX166K" + ], + "characters": [ + "SLE-ZX166K-01" + ], + "product_count": 1, + "representative_image": "assets/thumbs/SLE-Series/ZXE200_3_ZX166K/ZXE200_3_ZX166K-101.jpg", + "price_range": [ + 2000.0, + 2000.0 + ] + }, + "ZXE200_W1": { + "head_code": "ZXE200_W1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX171C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE200_W1_ZX171C/ZXE200_W1_ZX171C-102.jpg", + "price_range": null + }, + "ZXE200_W2": { + "head_code": "ZXE200_W2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX172E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE200_W2_ZX172E/ZXE200_W2_ZX172E-101.jpg", + "price_range": null + }, + "ZXE200_Z1": { + "head_code": "ZXE200_Z1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX164G" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE200_Z1_ZX164G/ZXE200_Z1_ZX164G-101.jpg", + "price_range": null + }, + "ZXE201_1": { + "head_code": "ZXE201_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX160J", + "ZX163E", + "ZX166K", + "ZX171C", + "ZX172E", + "ZX84J" + ], + "characters": [ + "SLE-ZX163E-04", + "SLE-ZX166K-03", + "SLE-ZX172E-02" + ], + "product_count": 5, + "representative_image": "assets/thumbs/SLE-Series/ZXE201_1_ZX172E/ZXE201_1_ZX172E-101.jpg", + "price_range": [ + 1000.0, + 2000.0 + ] + }, + "ZXE201_2": { + "head_code": "ZXE201_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX172E" + ], + "characters": [], + "product_count": 0, + "representative_image": null, + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE201_3": { + "head_code": "ZXE201_3", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX165D", + "ZX85G" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE201_3_ZX85G/ZXE201_3_ZX85G-101.jpg", + "price_range": [ + 1000.0, + 2000.0 + ] + }, + "ZXE201_4": { + "head_code": "ZXE201_4", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX108J", + "ZX109G", + "ZX111K", + "ZX117E", + "ZX164G", + "ZX172E" + ], + "characters": [ + "SLE-ZX164G-04" + ], + "product_count": 5, + "representative_image": "assets/thumbs/SLE-Series/ZXE201_4_ZX164G/ZXE201_4_ZX164G-108.jpg", + "price_range": [ + 1000.0, + 2000.0 + ] + }, + "ZXE201_5": { + "head_code": "ZXE201_5", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX84J", + "ZX91E" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/SLE-Series/ZXE201_5_ZX84J/ZXE201_5_ZX84J-102.jpg", + "price_range": [ + 1000.0, + 1000.0 + ] + }, + "ZXE201_W2": { + "head_code": "ZXE201_W2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX153B", + "ZX172E" + ], + "characters": [ + "SLE-ZX153B-01" + ], + "product_count": 2, + "representative_image": "assets/thumbs/SLE-Series/ZXE201_W2_ZX153B/ZXE201_W2_ZX153B-101.jpg", + "price_range": null + }, + "ZXE201_Z1": { + "head_code": "ZXE201_Z1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX172E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE201_Z1_ZX172E/ZXE201_Z1_ZX172E-103.jpg", + "price_range": null + }, + "ZXE202_1": { + "head_code": "ZXE202_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX160J", + "ZX164G" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/SLE-Series/ZXE202_1_ZX160J/ZXE202_1_ZX160J-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE202_2": { + "head_code": "ZXE202_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX165D" + ], + "characters": [], + "product_count": 0, + "representative_image": null, + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE202_Z1": { + "head_code": "ZXE202_Z1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX160J" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE202_Z1_ZX160J/ZXE202_Z1_ZX160J-101.jpg", + "price_range": null + }, + "ZXE203_1": { + "head_code": "ZXE203_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX108J", + "ZX109G", + "ZX111K", + "ZX117E", + "ZX160J", + "ZX164G", + "ZX171C" + ], + "characters": [], + "product_count": 7, + "representative_image": "assets/SLE-Series/ZXE203_1_ZX108J/ZXE203_1_ZX108J-101.jpg", + "price_range": [ + 1000.0, + 2000.0 + ] + }, + "ZXE203_2": { + "head_code": "ZXE203_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX166K", + "ZX172E" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/SLE-Series/ZXE203_2_ZX166K/ZXE203_2_ZX166K-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE203_3": { + "head_code": "ZXE203_3", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX163E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE203_3_ZX163E/ZXE203_3_ZX163E-101.jpg", + "price_range": [ + 1799.0, + 1799.0 + ] + }, + "ZXE203_4": { + "head_code": "ZXE203_4", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX91E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE203_4_ZX91E/ZXE203_4_ZX91E-101.jpg", + "price_range": [ + 1000.0, + 1000.0 + ] + }, + "ZXE203_5": { + "head_code": "ZXE203_5", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX84J" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE203_5_ZX84J/ZXE203_5_ZX84J-101.jpg", + "price_range": [ + 1000.0, + 1000.0 + ] + }, + "ZXE203_W1": { + "head_code": "ZXE203_W1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX117E", + "ZX171C", + "ZX172E" + ], + "characters": [ + "SLE-ZX172E-03" + ], + "product_count": 3, + "representative_image": "assets/thumbs/SLE-Series/ZXE203_W1_ZX172E/ZXE203_W1_ZX172E-101.jpg", + "price_range": null + }, + "ZXE203_Z1": { + "head_code": "ZXE203_Z1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX165D" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE203_Z1_ZX165D/ZXE203_Z1_ZX165D-101.jpg", + "price_range": null + }, + "ZXE204_1": { + "head_code": "ZXE204_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX108J", + "ZX109G", + "ZX111K", + "ZX160J", + "ZX163E", + "ZX164G", + "ZX166K", + "ZX172E", + "ZX91E" + ], + "characters": [ + "SLE-ZX160J-04", + "SLE-ZX163E-03", + "SLE-ZX166K-02" + ], + "product_count": 9, + "representative_image": "assets/thumbs/SLE-Series/ZXE204_1_ZX166K/ZXE204_1_ZX166k-101.jpg", + "price_range": [ + 1000.0, + 2000.0 + ] + }, + "ZXE204_2": { + "head_code": "ZXE204_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX84J" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE204_2_ZX84J/ZXE204_2_ZX84J-101.jpg", + "price_range": [ + 1000.0, + 1000.0 + ] + }, + "ZXE204_Z1": { + "head_code": "ZXE204_Z1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX172E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE204_Z1_ZX172E/ZXE204_Z1_ZX172E-101.jpg", + "price_range": null + }, + "ZXE205_1": { + "head_code": "ZXE205_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX160J", + "ZX172E" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/SLE-Series/ZXE205_1_ZX160J/ZXE205_1_ZX160J-102.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE205_2": { + "head_code": "ZXE205_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX164G" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE205_2_ZX164G/ZXE205_2_ZX164G-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE205_Z1": { + "head_code": "ZXE205_Z1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX166K" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE205_Z1_ZX166K/ZXE205_Z1_ZX166K-102.jpg", + "price_range": null + }, + "ZXE206_1": { + "head_code": "ZXE206_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX160J" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE206_1_ZX160J/ZXE206_1_ZX160J-101.jpg", + "price_range": [ + 2000.0, + 2000.0 + ] + }, + "ZXE206_2": { + "head_code": "ZXE206_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX172E", + "ZX86K" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE206_2_ZX86K/ZXE206_2_ZX86K-101.jpg", + "price_range": [ + 1000.0, + 2000.0 + ] + }, + "ZXE206_3": { + "head_code": "ZXE206_3", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX163E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE206_3_ZX163E/ZXE206_3_ZX163E-101.jpg", + "price_range": [ + 1799.0, + 1799.0 + ] + }, + "ZXE206_4": { + "head_code": "ZXE206_4", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX109G" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE206_4_ZX109G/ZXE206_4_ZX109G-101.jpg", + "price_range": [ + 1000.0, + 1000.0 + ] + }, + "ZXE206_5": { + "head_code": "ZXE206_5", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX117E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE206_5_ZX117E/ZXE206_5_ZX117E-101.jpg", + "price_range": [ + 1000.0, + 1000.0 + ] + }, + "ZXE206_W1": { + "head_code": "ZXE206_W1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX109G", + "ZX153B" + ], + "characters": [ + "SLE-ZX153B-02" + ], + "product_count": 2, + "representative_image": "assets/thumbs/SLE-Series/ZXE206_W1_ZX153B/ZXE206_W1_ZX153B-120_195fc9d2-52b2-453a-b899-69d67c0ec99c.jpg", + "price_range": null + }, + "ZXE206_W2": { + "head_code": "ZXE206_W2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX117E", + "ZX172E" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/SLE-Series/ZXE206_W2_ZX117E/ZXE206_W2_ZX117E-101.jpg", + "price_range": null + }, + "ZXE206_Z1": { + "head_code": "ZXE206_Z1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX160J" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE206_Z1_ZX160J/ZXE206_Z1_ZX160J-101.jpg", + "price_range": null + }, + "ZXE207_1": { + "head_code": "ZXE207_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX172E" + ], + "characters": [], + "product_count": 0, + "representative_image": null, + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE207_2": { + "head_code": "ZXE207_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX165D" + ], + "characters": [], + "product_count": 0, + "representative_image": null, + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE207_3": { + "head_code": "ZXE207_3", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX164G" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE207_3_ZX164G/ZXE207_3_ZX164G-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE207_Z1": { + "head_code": "ZXE207_Z1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX160J" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE207_Z1_ZX160J/ZXE207_Z1_ZX160J-101.jpg", + "price_range": null + }, + "ZXE208_1": { + "head_code": "ZXE208_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX166K" + ], + "characters": [], + "product_count": 0, + "representative_image": null, + "price_range": [ + 2000.0, + 2000.0 + ] + }, + "ZXE208_4": { + "head_code": "ZXE208_4", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX86K" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE208_4_ZX86K/ZXE208_4_ZX86K-101.jpg", + "price_range": [ + 1000.0, + 1000.0 + ] + }, + "ZXE208_5": { + "head_code": "ZXE208_5", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX160J", + "ZX85G", + "ZX91E" + ], + "characters": [ + "SLE-ZX160J-03" + ], + "product_count": 3, + "representative_image": "assets/thumbs/SLE-Series/ZXE208_5_ZX160J/ZXE208_5_ZX160J-101.jpg", + "price_range": [ + 1000.0, + 2000.0 + ] + }, + "ZXE208_W1": { + "head_code": "ZXE208_W1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX171C" + ], + "characters": [ + "SLE-ZX171C-04" + ], + "product_count": 1, + "representative_image": "assets/thumbs/SLE-Series/ZXE208_W1_ZX171C/ZXE208_W1_ZX171C-101.jpg", + "price_range": null + }, + "ZXE208_Z1": { + "head_code": "ZXE208_Z1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX166K" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE208_Z1_ZX166K/ZXE208_Z1_ZX166K-101.jpg", + "price_range": null + }, + "ZXE209_1": { + "head_code": "ZXE209_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX166K" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE209_1_ZX166K/ZXE209_1_ZX166K-101.jpg", + "price_range": [ + 1799.0, + 2000.0 + ] + }, + "ZXE209_2": { + "head_code": "ZXE209_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX165D" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE209_2_ZX165D/ZXE209_2_ZX165D-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE209_W1": { + "head_code": "ZXE209_W1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX172E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE209_W1_ZX172E/ZXE209_W1_ZX172E-101.jpg", + "price_range": null + }, + "ZXE209_Z1": { + "head_code": "ZXE209_Z1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX172E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE209_Z1_ZX172E/ZXE209_Z1_ZX172E-101.jpg", + "price_range": null + }, + "ZXE210_1": { + "head_code": "ZXE210_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX171C" + ], + "characters": [], + "product_count": 0, + "representative_image": null, + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE210_2": { + "head_code": "ZXE210_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX165D" + ], + "characters": [], + "product_count": 0, + "representative_image": null, + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE211_1": { + "head_code": "ZXE211_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX171C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE211_1_ZX171C/ZXE211_1_ZX171C-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE211_2": { + "head_code": "ZXE211_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX164G" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE211_2_ZX164G/ZXE211_2_ZX164G-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE212_1": { + "head_code": "ZXE212_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX166K", + "ZX171C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE212_1_ZX166K/ZXE212_1_ZX166K-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE212_2": { + "head_code": "ZXE212_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX164G" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE212_2_ZX164G/ZXE212_2_ZX164G-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE212_W1": { + "head_code": "ZXE212_W1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE212_W1-ZX172E/ZXE212_W1-ZX172E-101.jpg", + "price_range": null + }, + "ZXE212_W2": { + "head_code": "ZXE212_W2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX171C" + ], + "characters": [ + "SLE-ZX171C-02" + ], + "product_count": 1, + "representative_image": "assets/thumbs/SLE-Series/ZXE212_W2_ZX171C/ZXE212_W2_ZX171C-101.jpg", + "price_range": null + }, + "ZXE213_1": { + "head_code": "ZXE213_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX166K", + "ZX85G" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/SLE-Series/ZXE213_1_ZX166K/ZXE213_1_ZX166K-101.jpg", + "price_range": [ + 1000.0, + 2000.0 + ] + }, + "ZXE213_2": { + "head_code": "ZXE213_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX165D", + "ZX171C", + "ZX86K" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE213_2_ZX86K/ZXE213_2_ZX86K-101.jpg", + "price_range": [ + 1000.0, + 2000.0 + ] + }, + "ZXE214_1": { + "head_code": "ZXE214_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX171C" + ], + "characters": [], + "product_count": 0, + "representative_image": null, + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE214_2": { + "head_code": "ZXE214_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX165D" + ], + "characters": [], + "product_count": 0, + "representative_image": null, + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE215_1": { + "head_code": "ZXE215_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX108J", + "ZX109G", + "ZX111K", + "ZX117E", + "ZX165D", + "ZX166K", + "ZX171C", + "ZX172E" + ], + "characters": [ + "SLE-ZX172E-01" + ], + "product_count": 6, + "representative_image": "assets/thumbs/SLE-Series/ZXE215_1_ZX172E/ZXE215_1_ZX172E-108.jpg", + "price_range": [ + 1000.0, + 2000.0 + ] + }, + "ZXE215_2": { + "head_code": "ZXE215_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX84J" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE215_2_ZX84J/ZXE215_2_ZX84J-101.jpg", + "price_range": [ + 1000.0, + 1000.0 + ] + }, + "ZXE215_3": { + "head_code": "ZXE215_3", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX91E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE215_3_ZX91E/ZXE215_3_ZX91E-101.jpg", + "price_range": [ + 1000.0, + 1000.0 + ] + }, + "ZXE215_W1": { + "head_code": "ZXE215_W1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX171C", + "ZX172E" + ], + "characters": [ + "SLE-ZX171C-03" + ], + "product_count": 2, + "representative_image": "assets/thumbs/SLE-Series/ZXE215_W1_ZX171C/ZXE215_W1_ZX171C-101.jpg", + "price_range": null + }, + "ZXE216_1": { + "head_code": "ZXE216_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX108J", + "ZX109G", + "ZX111K", + "ZX117E", + "ZX163E", + "ZX164G", + "ZX85G" + ], + "characters": [ + "SLE-ZX163E-01", + "SLE-ZX164G-02" + ], + "product_count": 7, + "representative_image": "assets/thumbs/SLE-Series/ZXE216_1_ZX164G/ZXE216_1_ZX164G-101.jpg", + "price_range": [ + 1000.0, + 2000.0 + ] + }, + "ZXE216_2": { + "head_code": "ZXE216_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX160J", + "ZX84J" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/SLE-Series/ZXE216_2_ZX160J/ZXE216_2_ZX160J-101.jpg", + "price_range": [ + 1000.0, + 2000.0 + ] + }, + "ZXE216_3": { + "head_code": "ZXE216_3", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX109G", + "ZX165D", + "ZX86K" + ], + "characters": [ + "SLE-ZX165D-01" + ], + "product_count": 3, + "representative_image": "assets/thumbs/SLE-Series/ZXE216_3_ZX165D/ZXE216_3_ZX165D-101.jpg", + "price_range": [ + 1000.0, + 2000.0 + ] + }, + "ZXE216_4": { + "head_code": "ZXE216_4", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX166K", + "ZX91E" + ], + "characters": [], + "product_count": 2, + "representative_image": "assets/SLE-Series/ZXE216_4_ZX166K/ZXE216_4_ZX166K-101.jpg", + "price_range": [ + 1000.0, + 2000.0 + ] + }, + "ZXE216_W1": { + "head_code": "ZXE216_W1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX171C", + "ZX172E" + ], + "characters": [ + "SLE-ZX172E-04" + ], + "product_count": 2, + "representative_image": "assets/thumbs/SLE-Series/ZXE216_W1_ZX172E/ZXE216_W1_ZX172E-101.jpg", + "price_range": null + }, + "ZXE217_1": { + "head_code": "ZXE217_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX160J" + ], + "characters": [ + "SLE-ZX160J-02" + ], + "product_count": 1, + "representative_image": "assets/thumbs/SLE-Series/ZXE217_1_ZX160J/ZXE217_1_ZX160J-101_d32a9ded-3b83-4bc1-b5c3-0463117f63b0.jpg", + "price_range": [ + 2000.0, + 2000.0 + ] + }, + "ZXE217_2": { + "head_code": "ZXE217_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX164G" + ], + "characters": [ + "SLE-ZX164G-03" + ], + "product_count": 1, + "representative_image": "assets/thumbs/SLE-Series/ZXE217_2_ZX164G/ZXE217_2_ZX164G-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE217_4": { + "head_code": "ZXE217_4", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX84J" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE217_4_ZX84J/ZXE217_4_ZX84J-101.jpg", + "price_range": [ + 1000.0, + 1000.0 + ] + }, + "ZXE217_5": { + "head_code": "ZXE217_5", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX86K" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE217_5_ZX86K/ZXE217_5_ZX86K-101.jpg", + "price_range": [ + 1000.0, + 1000.0 + ] + }, + "ZXE218_1": { + "head_code": "ZXE218_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX160J", + "ZX171C" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE218_1_ZX160J/ZXE218_1_ZX160J-102.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE218_2": { + "head_code": "ZXE218_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX166K" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE218_2_ZX166K/ZXE218_2_ZX166K-101.jpg", + "price_range": [ + 2000.0, + 2000.0 + ] + }, + "ZXE218_W1": { + "head_code": "ZXE218_W1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 33, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX171C", + "ZX172E" + ], + "characters": [ + "SLE-ZX171C-01" + ], + "product_count": 2, + "representative_image": "assets/thumbs/SLE-Series/ZXE218_W1_ZX171C/ZXE218_W1_ZX171C-101.jpg", + "price_range": null + }, + "ZXE218_W2": { + "head_code": "ZXE218_W2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [], + "bodies": [ + "ZX117E" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE218_W2_ZX117E/ZXE218_W2_ZX117E-101.jpg", + "price_range": null + }, + "ZXE219_1": { + "head_code": "ZXE219_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX163E", + "ZX164G" + ], + "characters": [ + "SLE-ZX163E-02", + "SLE-ZX164G-01" + ], + "product_count": 2, + "representative_image": "assets/thumbs/SLE-Series/ZXE219_1_ZX164G/ZXE219_1_ZX164G-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE219_2": { + "head_code": "ZXE219_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX165D" + ], + "characters": [], + "product_count": 0, + "representative_image": null, + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE220_1": { + "head_code": "ZXE220_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX166K" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE220_1_ZX166K/ZXE220_1_ZX166K-101.jpg", + "price_range": [ + 2000.0, + 2000.0 + ] + }, + "ZXE220_2": { + "head_code": "ZXE220_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX164G" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE220_2_ZX164G/ZXE220_2_ZX164G-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE221_2": { + "head_code": "ZXE221_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX164G" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE221_2_ZX164G/ZXE221_2_ZX164G-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE222_1": { + "head_code": "ZXE222_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX165D" + ], + "characters": [ + "SLE-ZX165D-03" + ], + "product_count": 1, + "representative_image": "assets/thumbs/SLE-Series/ZXE222_1_ZX165D/ZXE222_1_ZX165D-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE223_1": { + "head_code": "ZXE223_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX111K", + "ZX160J", + "ZX166K" + ], + "characters": [ + "SLE-ZX160J-01" + ], + "product_count": 3, + "representative_image": "assets/thumbs/SLE-Series/ZXE223_1_ZX160J/ZXE223_1_ZX160J-101.jpg", + "price_range": [ + 1000.0, + 2000.0 + ] + }, + "ZXE223_2": { + "head_code": "ZXE223_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX86K" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE223_2_ZX86K/ZXE223_2_ZX86K-101.jpg", + "price_range": [ + 1000.0, + 1000.0 + ] + }, + "ZXE223_3": { + "head_code": "ZXE223_3", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": null, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX85G" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE223_3_ZX85G/ZXE223_3_ZX85G-101.jpg", + "price_range": [ + 1000.0, + 1000.0 + ] + }, + "ZXE224_1": { + "head_code": "ZXE224_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX164G" + ], + "characters": [], + "product_count": 1, + "representative_image": "assets/SLE-Series/ZXE224_1_ZG164G/ZXE224_1_ZG164G-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE224_2": { + "head_code": "ZXE224_2", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX165D" + ], + "characters": [ + "SLE-ZX165D-02" + ], + "product_count": 1, + "representative_image": "assets/thumbs/SLE-Series/ZXE224_2_ZX165D/ZXE224_2_ZX165D-108.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE226_1": { + "head_code": "ZXE226_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX166K" + ], + "characters": [ + "SLE-ZX166K-04" + ], + "product_count": 1, + "representative_image": "assets/thumbs/SLE-Series/ZXE226_1_ZX166K/ZXE226_1_ZX166K-101.jpg", + "price_range": [ + 1600.0, + 2000.0 + ] + }, + "ZXE231_1": { + "head_code": "ZXE231_1", + "line": "SLE 3.0", + "neck_class": "standard", + "neck_cm": 32.5, + "face_variants": [ + "Standard" + ], + "mj_face_codes": [], + "skin_tones": [ + "Tan" + ], + "bodies": [ + "ZX170A" + ], + "characters": [ + "SLE-ZX170A-01" + ], + "product_count": 1, + "representative_image": "assets/thumbs/SLE-Series/ZXE231_1_ZX170A/ZXE231_1_ZX170A-101.jpg", + "price_range": [ + 2000.0, + 2000.0 + ] + } + } +} diff --git a/docs/pdr/PDR-011-declarative-configurator.md b/docs/pdr/PDR-011-declarative-configurator.md index 086db27..8a3e982 100644 --- a/docs/pdr/PDR-011-declarative-configurator.md +++ b/docs/pdr/PDR-011-declarative-configurator.md @@ -212,6 +212,18 @@ without bespoke code: prototype's logic was sanity-checked against `db/` for all three worked examples plus the bespoke case. +- **The head generator** — `scripts/build_heads.py` → `db/heads.json`. Heads were + the one axis with no structured model: 189 sculpts scattered across the product + feed, variants, character casts, and movable-jaw overrides. The script + reverse-engineers them (mirroring `body_measurements.json`) into one record per + sculpt: line, movable-jaw availability, skin tones, paired bodies, cast + characters, a representative image, and price range. The configurator's + `head_code` axis now renders as an **image gallery** driven by that file + (replacing the dropdown), ranks heads that pair with the resolved body first, + gates the **Movable Jaw** face variant to heads that actually offer it, and + shows the chosen head's thumbnail in the resolve panel. Falls back to the + variant-derived dropdown if `heads.json` is absent. + **Deliberately not in this package (next step):** - Backfilling the `proposed` component axes (standing feet, articulated fingers) @@ -219,15 +231,61 @@ without bespoke code: - A `build_configurator.py` pipeline step that pre-resolves every catalogued combination into a lookup table (so the runtime resolver is a map, not a search). -- A head **gallery** picker with imagery (the prototype renders `head_code` as a - filtered dropdown; the spec already declares it `gallery-select`). +- Per-head feature attributes (eyes, makeup, hair) — `heads.json` currently + carries identity + pairing data, not a face-feature breakdown. - Adding the configurator to the global nav in `assets/site.js` (kept out to keep this change off the shared runtime; reachable via breadcrumb / direct link for now). --- -## 8. Strategic conclusion +## 8. How heads tie to lines — and the interchangeability pathway + +A core question for any head picker: **are heads locked to a product line, or +can they be swapped across lines?** The data answers both halves. + +**The catalog sells heads line-locked.** Every one of the 189 heads pairs with +bodies of exactly one line (174 heads → one line; 15 → none, having no measured +body). No catalogued product or SKU crosses lines. So a naive picker — the line +filter the first version shipped — is *commercially* correct but physically +over-restrictive. + +**Physically, the constraint is the neck connector, not the marketing line.** +Heads attach at the neck, and the spec-card neck circumferences fall into two +clean clusters: + +| Neck class | Neck circ. | Lines | +|---|---|---| +| `slim` | ~26.5–27.5 cm | K-Series | +| `standard` | ~31–33 cm | Fusion · I-Series · SLE 3.0 | + +So a head whose body neck is ~32 cm physically fits *any* `standard`-neck body +regardless of line — Fusion / I-Series / SLE heads are mutually interchangeable — +while K-Series stands alone on the narrow connector. `build_heads.py` derives a +`neck_class` per head from the necks of the bodies it pairs with (falling back to +a line default), and `db/heads.json#/neck_classes` documents the threshold. + +**The pathway.** The configurator adds an *Interchangeable heads (cross-line)* +toggle (declared in the schema, `provenance: proposed`). Off, the gallery shows +only same-line heads (the catalogued, instant-SKU path). On, it shows every head +whose `neck_class` matches the body — same-line heads first, cross-line heads +flagged with their origin line and a dashed frame. Selecting a cross-line head: + +- never resolves to a catalogued SKU (none exist), so price is **estimated** and + the build is a **custom / bespoke inquiry** (`cross_line_head` warning); +- is blocked outright if neck classes differ (`neck_incompatible` error — e.g. a + K-Series head on an SLE body); +- carries an explicit honesty note: neck fit is *estimated from spec cards* and + must be confirmed against the physical joint before order. + +This keeps the catalogued path clean and instant, while opening a principled, +clearly-labelled custom path — and it surfaces a real commercial insight: a +K-Series body buyer has only 3 interchangeable heads, whereas a `standard`-neck +body opens up 186. + +--- + +## 9. Strategic conclusion 1. Declarative beats hand-coded for a catalog that already regenerates from JSON: new bodies/tones/heads become **data** changes, not code changes. @@ -246,7 +304,7 @@ every build is a measured architecture or an honest inquiry."* --- -## 9. Acceptance Review +## 10. Acceptance Review | Criterion | Status | |---|---| @@ -260,4 +318,7 @@ every build is a measured architecture or an honest inquiry."* | Bespoke / failure path defined | ✓ `bespoke_combination` rule + worked bespoke example | | Reuses existing patterns (no new design system, notify-me reuse) | ✓ `configurator.html` reuses `site-kit-contract` head/boot + `ZX` helpers; dev lines reuse PDR-003 notify-me | | Working prototype generated from the spec (not hand-coded per axis) | ✓ `configurator.html` walks `groups[].axes[]` + `derivations`; resolver verified against `db/` for all 3 examples + bespoke | +| Heads given a structured model (the head generator) | ✓ `scripts/build_heads.py` → `db/heads.json` (189 heads: line, neck_class, MJ, tones, paired bodies, cast, image, price) | +| Head picker is an image gallery driven by that data | ✓ `head_code` renders as a thumbnail gallery; MJ gated to capable heads; falls back to a dropdown if `heads.json` missing | +| Head↔line coupling understood + interchangeability pathway | ✓ neck-class model (slim vs standard); cross-line toggle; `cross_line_head` warn + `neck_incompatible` error; verified no slim/standard leakage | | Both JSON artifacts parse; new page passes site validation | ✓ `validate-site.mjs` green (page added to its KIT_PAGES list) | diff --git a/scripts/build_heads.py b/scripts/build_heads.py new file mode 100644 index 0000000..a62caed --- /dev/null +++ b/scripts/build_heads.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +""" +Build the machine-readable head catalog (db/heads.json) by reverse-engineering +the 177 head sculpts that are otherwise scattered across the product feed, +live variants, character casts, and movable-jaw overrides. + +This is the head-side companion to db/body_measurements.json: where bodies are +measured architectures, heads are sculpts with a series, a movable-jaw +availability, the skin tones they ship in, the bodies they pair with, the +characters cast on them, and a representative image. It is the data foundation +the configurator's head gallery picker reads (PDR-011). + +Sources: + db/catalog.json products: head_code, face_code, body_code, series_id, + cover_image, image_count, price-less product rows + db/live_variants.json head + body + tone + price (the priced SKUs) + db/product_overrides.json movable-jaw face_codes keyed by head + db/characters.json which named characters are cast on each head (+ thumbs) + +Run after build_characters.py / make_thumbs.py (so hero_thumb paths exist). +Usage: python scripts/build_heads.py +""" +import json +import re +from collections import defaultdict +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +DB = ROOT / "db" +OUT = DB / "heads.json" + +# catalog series_id -> the line name used by the configurator schema +SERIES_LINE = {"Fusion": "Fusion", "I": "I-Series", "K": "K-Series", "SLE": "SLE 3.0"} +# fallback: head-code prefix -> line, for heads with no/ambiguous product series_id +PREFIX_LINE = {"ZXE": "SLE 3.0", "GE": "I-Series", "ZFE": "Fusion", "KE": "K-Series"} + +# --- neck-connector classes (the basis for interchangeable heads) ---------------- +# Heads attach at the neck. Marketing line is NOT the constraint; the neck +# circumference is. db/body_measurements.json shows two clusters: K-Series ~27cm +# (narrow) vs Fusion/I-Series/SLE ~31-33cm (standard). Heads sharing a class are +# physically swappable across lines even though the catalog only sells line-matched +# pairs. Threshold is an ESTIMATE from the spec cards — confirm against the physical +# neck joint before treating a cross-line swap as guaranteed-fit. +NECK_SLIM_MAX = 29.0 # cm; below this = the narrow (K-Series) connector + + +def neck_class(neck_cm): + if neck_cm is None: + return None + return "slim" if neck_cm < NECK_SLIM_MAX else "standard" + + +# line -> default neck class, used when a head has no measured paired body +LINE_NECK_CLASS = {"K-Series": "slim", "I-Series": "standard", "Fusion": "standard", "SLE 3.0": "standard"} + + +def load(name): + return json.loads((DB / name).read_text(encoding="utf-8")) + + +def line_for(head_code, series_ids): + """Prefer the product's own series; fall back to the head-code prefix.""" + if series_ids: + # most common series_id wins (a head can appear in one series' folders) + sid = max(series_ids, key=lambda s: series_ids[s]) + if sid in SERIES_LINE: + return SERIES_LINE[sid] + m = re.match(r"^[A-Z]+", head_code or "") + return PREFIX_LINE.get(m.group(0) if m else "", None) + + +def main(): + catalog = load("catalog.json") + variants = load("live_variants.json") + overrides = load("product_overrides.json") + characters = load("characters.json").get("characters", []) + bodies_meas = load("body_measurements.json").get("bodies", {}) + + heads = defaultdict(lambda: { + "series_ids": defaultdict(int), + "bodies": set(), + "tones": set(), + "mj_face_codes": set(), + "images": [], # ordered candidate images (first = representative) + "characters": [], + "product_count": 0, + "prices": [], + }) + + # --- products: the structural backbone (head <-> body pairings + imagery) --- + for p in catalog.get("products", []): + hc = p.get("head_code") + if not hc: + continue + h = heads[hc] + h["product_count"] += 1 + if p.get("series_id"): + h["series_ids"][p["series_id"]] += 1 + if p.get("body_code"): + h["bodies"].add(p["body_code"]) + if p.get("face_code") and "MJ" in p["face_code"]: + h["mj_face_codes"].add(p["face_code"]) + cov = p.get("cover_image") + if cov: + # cover_image is repo-relative under assets/; the front-end serves from there + h["images"].append("assets/" + cov if not cov.startswith("assets/") else cov) + + # --- movable-jaw overrides: authoritative MJ availability per head --- + for o in overrides.values(): + hc, fc = o.get("head_code"), o.get("face_code") + if hc and fc and "MJ" in fc: + heads[hc]["mj_face_codes"].add(fc) + if o.get("body_code"): + heads[hc]["bodies"].add(o["body_code"]) + + # --- live variants: priced SKUs -> tones + price range --- + for v in variants: + hc = v.get("head") + if not hc: + continue + h = heads[hc] + if v.get("tone"): + h["tones"].add(v["tone"]) + if v.get("body"): + h["bodies"].add(v["body"]) + try: + if v.get("price"): + h["prices"].append(float(v["price"])) + except (TypeError, ValueError): + pass + + # --- characters: named cast on each head + a real thumbnail to lead with --- + for c in characters: + hc = (c.get("face") or {}).get("head_code") + if not hc: + continue + h = heads[hc] + if c.get("status") == "live": + h["characters"].append(c["character_id"]) + ps = c.get("photoshoot") or {} + thumb = ps.get("hero_thumb") or ps.get("hero") + if thumb: + h["images"].insert(0, thumb) # a cast hero_thumb leads the cover jpg + tone = (c.get("face") or {}).get("skin_tone") + if tone: + h["tones"].add(tone) + + # --- finalize --- + out_heads = {} + for hc in sorted(heads): + h = heads[hc] + line = line_for(hc, h["series_ids"]) + face_variants = ["Standard"] + (["Movable Jaw"] if h["mj_face_codes"] else []) + prices = sorted(h["prices"]) + # neck-connector class: from the measured necks of paired bodies, else line default + necks = sorted(bodies_meas[bc]["neck"] for bc in h["bodies"] + if bodies_meas.get(bc) and bodies_meas[bc].get("neck") is not None) + neck_cm = necks[len(necks) // 2] if necks else None # median + nclass = neck_class(neck_cm) or LINE_NECK_CLASS.get(line) + # de-dup images, keep order + seen, images = set(), [] + for img in h["images"]: + if img not in seen: + seen.add(img) + images.append(img) + out_heads[hc] = { + "head_code": hc, + "line": line, + "neck_class": nclass, + "neck_cm": neck_cm, + "face_variants": face_variants, + "mj_face_codes": sorted(h["mj_face_codes"]), + "skin_tones": sorted(h["tones"]), + "bodies": sorted(h["bodies"]), + "characters": h["characters"], + "product_count": h["product_count"], + "representative_image": images[0] if images else None, + "price_range": [prices[0], prices[-1]] if prices else None, + } + + payload = { + "version": "1.0.0", + "_comment": ( + "Head catalog reverse-engineered from products + variants + characters + " + "movable-jaw overrides (scripts/build_heads.py). One record per head sculpt: " + "line, movable-jaw availability, skin tones, paired bodies, cast characters, " + "a representative image, and price range. Feeds the configurator head gallery " + "(PDR-011). Regenerate after build_characters.py / make_thumbs.py." + ), + "generated_from": [ + "db/catalog.json", "db/live_variants.json", + "db/product_overrides.json", "db/characters.json", + ], + "series_line_map": SERIES_LINE, + "neck_classes": { + "_basis": "Neck-connector class is the interchangeability constraint, not the marketing line. " + f"Threshold: neck circumference < {NECK_SLIM_MAX}cm = 'slim', else 'standard'. " + "ESTIMATED from spec-card neck measurements — confirm against the physical neck joint.", + "slim": "Narrow connector — K-Series (~27cm).", + "standard": "Standard connector — Fusion / I-Series / SLE 3.0 (~31-33cm). Heads here are mutually swappable.", + }, + "count": len(out_heads), + "heads": out_heads, + } + OUT.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + + # console summary + by_line = defaultdict(int) + mj = 0 + for h in out_heads.values(): + by_line[h["line"]] += 1 + if "Movable Jaw" in h["face_variants"]: + mj += 1 + print(f"Wrote {OUT.relative_to(ROOT)} — {len(out_heads)} heads") + for ln, n in sorted(by_line.items(), key=lambda kv: (-kv[1], str(kv[0]))): + print(f" {str(ln):10} {n}") + print(f" movable-jaw capable: {mj}") + no_img = sum(1 for h in out_heads.values() if not h["representative_image"]) + print(f" without a representative image: {no_img}") + + +if __name__ == "__main__": + main() From fb63ca4ad32ddbd1afdd940399cdf8aec99bff74 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 6 Jun 2026 18:00:58 +0000 Subject: [PATCH 4/5] feat(pdr-011): stored neck-compat data + Face & Finish (proposed) axes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neck data (interchangeability stops being a buried constant): - scripts/build_neck_compat.py → db/neck_compatibility.json: the single source of truth for neck classes. Explicit per-body neck_class, the classes/compatibility matrix, and the threshold in one reviewable place. Each body carries a neck_joint_mm slot (null) for the TRUE connector spec — fill it to replace the circumference-derived estimate with no code change. - build_heads.py imports the shared threshold/helpers from build_neck_compat (one definition, not two). configurator.html reads body neck_class from the data file (runtime calc is only a fallback) and uses the compatibility matrix for fit/validation instead of strict equality. Face & Finish (honest proposed layer): - No structured face-feature data exists in db/ (the only real per-head face attribute is movable-jaw, already modelled). So rather than fabricate per-head eye colours, add a Face & Finish group of provenance:proposed axes (eye colour, makeup, eyebrows, wig, freckles) — global options that compose as a custom finish (no SKU impact) and surface in the resolve panel. - heads.json gains a face_features slot (null) per head as the explicit backfill target; populate it to make these axes head-aware. - configurator state generalised to one bucket per schema group. PDR-011 §8/§9 + README pipeline + acceptance updated. --- README.md | 3 +- configurator.html | 51 +- db/configurator_schema.json | 33 ++ db/heads.json | 567 ++++++++++++------- db/neck_compatibility.json | 184 ++++++ docs/pdr/PDR-011-declarative-configurator.md | 38 +- scripts/build_heads.py | 26 +- scripts/build_neck_compat.py | 99 ++++ 8 files changed, 776 insertions(+), 225 deletions(-) create mode 100644 db/neck_compatibility.json create mode 100644 scripts/build_neck_compat.py diff --git a/README.md b/README.md index 9c9f17e..7e0d9d8 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,8 @@ build_profiles.py # WHR/BWR analysis → classify into the 6 Body Families build_characters.py # Series → Body → 4 characters → photoshoot|placeholder merge_stories.py # fold story/profile inputs into characters.json make_thumbs.py # generate hero/gallery thumbnails (re-run after build_characters) -build_heads.py # reverse-engineer heads → db/heads.json (configurator head gallery) +build_neck_compat.py # neck-connector classes → db/neck_compatibility.json (head interchangeability) +build_heads.py # reverse-engineer heads → db/heads.json (configurator head gallery; imports build_neck_compat) build_package.py # stage the deliverable (code + referenced images) + zip ``` diff --git a/configurator.html b/configurator.html index c506a50..92b9713 100644 --- a/configurator.html +++ b/configurator.html @@ -69,6 +69,10 @@ white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .hg.cross .hg-img{border-style:dashed} .rc-cross{color:var(--blue)} +.rc-finish{margin-top:16px} +.rc-finish .finrow{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px} +.finchip{font-size:11px;color:var(--cream);background:var(--panel2);border:1px solid var(--line); + border-radius:20px;padding:3px 10px} .hg-code{font-family:ui-monospace,monospace;font-size:10px;letter-spacing:.3px;text-align:center; white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .hg:hover .hg-img{border-color:var(--gold)} @@ -145,14 +149,15 @@ fetch('db/body_measurements.json').then(r=>r.json()), fetch('db/live_variants.json').then(r=>r.json()), fetch('db/product_overrides.json').then(r=>r.json()).catch(()=>({})), - fetch('db/heads.json').then(r=>r.json()).catch(()=>({heads:{}})) -]).then(([m, schema, meas, variants, overrides, headsDoc]) => { - render(m, { schema, bodies: meas.bodies||{}, variants: variants||[], overrides: overrides||{}, heads: headsDoc.heads||{} }); + fetch('db/heads.json').then(r=>r.json()).catch(()=>({heads:{}})), + fetch('db/neck_compatibility.json').then(r=>r.json()).catch(()=>({})) +]).then(([m, schema, meas, variants, overrides, headsDoc, neck]) => { + render(m, { schema, bodies: meas.bodies||{}, variants: variants||[], overrides: overrides||{}, heads: headsDoc.heads||{}, neck: neck||{} }); ZX.mountFooter(); }).catch(() => ZX.fail()); function render(m, DATA){ - const { schema, bodies, variants, overrides, heads } = DATA; + const { schema, bodies, variants, overrides, heads, neck } = DATA; const app = document.getElementById('app'); // ---- helpers reading the spec (so logic stays declarative) ---- @@ -161,15 +166,21 @@ const deriv = {}; (schema.derivations||[]).forEach(d => deriv[d.id] = d); const prefixesFor = line => { const p = (axisById.line.code_prefix||{})[line]; return p ? (Array.isArray(p)?p:[p]) : []; }; - // ---- neck-connector classes: the basis for interchangeable heads (db/heads.json#/neck_classes) ---- - // Heads attach at the neck; the connector size — not the marketing line — is the constraint. - const NECK_SLIM_MAX = 29; // cm — below this is the narrow (K-Series) connector + // ---- neck-connector classes: the basis for interchangeable heads ---- + // Authority is db/neck_compatibility.json (built by scripts/build_neck_compat.py), + // which stores an explicit per-body neck_class + a neck_joint_mm slot for the true + // connector spec. We read that stored value; the circumference calc is only a fallback. + const NECK_SLIM_MAX = neck.neck_slim_max_cm || 29; // cm — narrow (K-Series) connector below this const LINE_NECK = { 'K-Series':'slim', 'I-Series':'standard', 'Fusion':'standard', 'SLE 3.0':'standard' }; + const NECK_COMPAT = (neck.compatibility) || { slim:['slim'], standard:['standard'] }; function neckClassOfCm(cm){ return cm==null ? null : (cm < NECK_SLIM_MAX ? 'slim' : 'standard'); } function bodyNeckClass(code, line){ - const b = bodies[code]; + const rec = neck.bodies && neck.bodies[code]; // stored, reviewable per-body value + if(rec && rec.neck_class) return rec.neck_class; + const b = bodies[code]; // fallbacks: circumference, then line return neckClassOfCm(b && b.neck) || LINE_NECK[line] || 'standard'; } + function neckJointKnown(code){ const r = neck.bodies && neck.bodies[code]; return !!(r && r.neck_joint_mm != null); } // Head records for a build, from the reverse-engineered catalog (db/heads.json, // built by scripts/build_heads.py). Cast/imaged heads lead; bodies-on-this-code first. @@ -185,8 +196,9 @@ return [...set].sort().map(code => ({head_code:code, line, face_variants:['Standard'], skin_tones:[], bodies:[], characters:[], representative_image:null, _cross:false})); } const nclass = bodyNeckClass(bodyCode, line); + const fits = h => (NECK_COMPAT[nclass] || [nclass]).includes(h.neck_class); const recs = Object.values(heads) - .filter(h => crossLine ? (h.neck_class === nclass) : (h.line === line)) + .filter(h => crossLine ? fits(h) : (h.line === line)) .map(h => ({ ...h, _cross: h.line !== line })); const rank = h => (h._cross ? 1000 : 0) // same-line block first + (bodyCode && h.bodies.includes(bodyCode) ? 0 : 100) @@ -268,11 +280,11 @@ switch(rule.id){ case 'bespoke_combination': hit = !code; break; case 'mj_unsupported': hit = st.head.face_variant==='Movable Jaw' && !mjOverrideFor(st.head.head_code, code); break; - case 'neck_incompatible': { const hr=headRec(st.head.head_code); - hit = !!hr && hr.neck_class && hr.neck_class !== bodyNeckClass(code, st.body.line); break; } - case 'cross_line_head': { const hr=headRec(st.head.head_code); + case 'neck_incompatible': { const hr=headRec(st.head.head_code); const bcls=bodyNeckClass(code, st.body.line); + hit = !!hr && hr.neck_class && !(NECK_COMPAT[bcls]||[bcls]).includes(hr.neck_class); break; } + case 'cross_line_head': { const hr=headRec(st.head.head_code); const bcls=bodyNeckClass(code, st.body.line); hit = !!hr && hr.line && hr.line !== st.body.line && - (!hr.neck_class || hr.neck_class === bodyNeckClass(code, st.body.line)); break; } + (!hr.neck_class || (NECK_COMPAT[bcls]||[bcls]).includes(hr.neck_class)); break; } case 'skeleton_scope': hit = !!st.body.skeleton_version && st.body.line!=='SLE 3.0'; break; case 'torso_excluded': hit = st.body.height_cm < 120; break; case 'dev_line': hit = ['The Classic','The Sculpt'].includes(st.body.line); break; @@ -283,7 +295,7 @@ } // ---- state: seed each axis from the spec's defaults, then a known-good body ---- - const st = { body:{}, head:{}, components:{} }; + const st = {}; schema.groups.forEach(g => st[g.id] = {}); // one bucket per group (body/head/components/face) schema.groups.forEach(g => g.axes.forEach(a => { if(a.control==='derived') return; let v = a.default; @@ -408,6 +420,16 @@

${ZX.esc(g.label)}

const noticeHtml = notices.length ? `
`+notices.map(n=>`
${ZX.esc(n.msg)}
`).join('')+`
` : ''; + // custom finish summary: face-group selections that differ from their default + const faceGroup = schema.groups.find(g=>g.id==='face'); + const finishBits = (faceGroup?faceGroup.axes:[]).map(a=>{ + const v = (st.face||{})[a.id]; + if(v===a.default || v==='As pictured' || v===false || v==null) return null; + return `${a.label}: ${v===true?'Yes':v}`; + }).filter(Boolean); + const finishBlock = finishBits.length + ? `
Custom finish
${finishBits.map(b=>`${ZX.esc(b)}`).join('')}
` : ''; + return `
Resolved architecture
@@ -419,6 +441,7 @@

${ZX.esc(g.label)}

${sig ? `
Measurement signature
${sigRows}
` : `
No catalogued spec card — this is a bespoke inquiry, not an instant SKU.
`} + ${finishBlock}
${skuR.sku?ZX.esc(skuR.sku):'—'} ${price!=null?'$'+price:'—'}${skuR.estimated&&price!=null?'estimated':''} diff --git a/db/configurator_schema.json b/db/configurator_schema.json index 3e3a1b0..e9e3b4c 100644 --- a/db/configurator_schema.json +++ b/db/configurator_schema.json @@ -143,6 +143,28 @@ "note": "PROPOSED — needs a per-body capability flag in db/ before going live." } ] + }, + + { + "id": "face", + "label": "Face & Finish", + "intent": "Per-head finish options. PROPOSED: these are real ZELEX customizations, but there is no per-head face-feature data in db/ to reverse-engineer from — the enums below are the standard option set, not catalogued per-head availability/defaults. They compose into the build as a custom finish (no SKU impact) and write to the `face_features` slot reserved on each head (db/heads.json). Backfill that slot per head to turn these from global options into head-aware ones.", + "axes": [ + { "id": "eye_color", "label": "Eye Colour", "control": "select", "type": "string", "provenance": "proposed", + "enum": ["As pictured", "Brown", "Hazel", "Blue", "Green", "Grey"], "default": "As pictured", + "note": "PROPOSED — acrylic eye swap. Per-head default unknown (no db/ data); 'As pictured' = the head's shipped eyes." }, + { "id": "makeup", "label": "Makeup", "control": "radio", "type": "string", "provenance": "proposed", + "enum": ["As pictured", "Natural", "Glam"], "default": "As pictured", + "note": "PROPOSED — factory makeup style." }, + { "id": "eyebrows", "label": "Eyebrows", "control": "radio", "type": "string", "provenance": "proposed", + "enum": ["Painted", "Implanted"], "default": "Painted", + "note": "PROPOSED — hand-painted vs implanted brow hair." }, + { "id": "wig_color", "label": "Wig", "control": "select", "type": "string", "provenance": "proposed", + "enum": ["As pictured", "Black", "Brunette", "Blonde", "Red"], "default": "As pictured", + "note": "PROPOSED — removable wig; not a sculpt attribute." }, + { "id": "freckles", "label": "Freckles", "control": "toggle", "type": "boolean", "provenance": "proposed", + "default": false, "note": "PROPOSED — optional painted freckles." } + ] } ], @@ -228,6 +250,17 @@ "standing_feet": { "type": "boolean" }, "fingers": { "type": "boolean" } } + }, + "face": { + "type": "object", + "description": "Optional custom finish (all PROPOSED — no per-head data backing yet).", + "properties": { + "eye_color": { "enum": ["As pictured", "Brown", "Hazel", "Blue", "Green", "Grey"] }, + "makeup": { "enum": ["As pictured", "Natural", "Glam"] }, + "eyebrows": { "enum": ["Painted", "Implanted"] }, + "wig_color": { "enum": ["As pictured", "Black", "Brunette", "Blonde", "Red"] }, + "freckles": { "type": "boolean" } + } } } } diff --git a/db/heads.json b/db/heads.json index 1762f89..b8889f9 100644 --- a/db/heads.json +++ b/db/heads.json @@ -46,7 +46,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE03_1": { "head_code": "GE03_1", @@ -73,7 +74,8 @@ "price_range": [ 3599.0, 3899.0 - ] + ], + "face_features": null }, "GE03_2": { "head_code": "GE03_2", @@ -100,7 +102,8 @@ "price_range": [ 3599.0, 3899.0 - ] + ], + "face_features": null }, "GE07_1": { "head_code": "GE07_1", @@ -123,7 +126,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE07_3": { "head_code": "GE07_3", @@ -146,7 +150,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE100_1": { "head_code": "GE100_1", @@ -169,7 +174,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE101_1": { "head_code": "GE101_1", @@ -192,7 +198,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE109_1": { "head_code": "GE109_1", @@ -215,7 +222,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE111_1": { "head_code": "GE111_1", @@ -238,7 +246,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE111_2": { "head_code": "GE111_2", @@ -261,7 +270,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE111_3": { "head_code": "GE111_3", @@ -284,7 +294,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE113_1": { "head_code": "GE113_1", @@ -310,7 +321,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE114_1": { "head_code": "GE114_1", @@ -336,7 +348,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE115_1": { "head_code": "GE115_1", @@ -365,7 +378,8 @@ "price_range": [ 3599.0, 3899.0 - ] + ], + "face_features": null }, "GE116_1": { "head_code": "GE116_1", @@ -391,7 +405,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE122_1": { "head_code": "GE122_1", @@ -414,7 +429,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE122_2": { "head_code": "GE122_2", @@ -437,7 +453,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE123_1": { "head_code": "GE123_1", @@ -460,7 +477,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE124_1": { "head_code": "GE124_1", @@ -483,7 +501,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE124_2": { "head_code": "GE124_2", @@ -506,7 +525,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE125_1": { "head_code": "GE125_1", @@ -530,7 +550,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE125_2": { "head_code": "GE125_2", @@ -553,7 +574,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE125_3": { "head_code": "GE125_3", @@ -576,7 +598,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE127_1": { "head_code": "GE127_1", @@ -604,7 +627,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE134_1": { "head_code": "GE134_1", @@ -632,7 +656,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE137_1": { "head_code": "GE137_1", @@ -658,7 +683,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE138_1": { "head_code": "GE138_1", @@ -686,7 +712,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE139_1": { "head_code": "GE139_1", @@ -715,7 +742,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE139_2": { "head_code": "GE139_2", @@ -738,7 +766,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE141_1": { "head_code": "GE141_1", @@ -766,7 +795,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE146_1": { "head_code": "GE146_1", @@ -792,7 +822,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE149_1": { "head_code": "GE149_1", @@ -820,7 +851,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE14_1": { "head_code": "GE14_1", @@ -846,7 +878,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE14_2": { "head_code": "GE14_2", @@ -872,7 +905,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE14_3": { "head_code": "GE14_3", @@ -898,7 +932,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE16_1": { "head_code": "GE16_1", @@ -924,7 +959,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE16_2": { "head_code": "GE16_2", @@ -950,7 +986,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE173_1": { "head_code": "GE173_1", @@ -976,7 +1013,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE24_1": { "head_code": "GE24_1", @@ -999,7 +1037,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE41_1": { "head_code": "GE41_1", @@ -1027,7 +1066,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE45_1": { "head_code": "GE45_1", @@ -1050,7 +1090,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE45_2": { "head_code": "GE45_2", @@ -1073,7 +1114,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE45_5": { "head_code": "GE45_5", @@ -1096,7 +1138,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE45_6": { "head_code": "GE45_6", @@ -1119,7 +1162,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE45_7": { "head_code": "GE45_7", @@ -1142,7 +1186,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE45_8": { "head_code": "GE45_8", @@ -1167,7 +1212,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE46_2": { "head_code": "GE46_2", @@ -1190,7 +1236,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE46_3": { "head_code": "GE46_3", @@ -1213,7 +1260,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE46_4": { "head_code": "GE46_4", @@ -1236,7 +1284,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE46_5": { "head_code": "GE46_5", @@ -1259,7 +1308,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE47_1": { "head_code": "GE47_1", @@ -1284,7 +1334,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE48_1": { "head_code": "GE48_1", @@ -1307,7 +1358,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE48_2": { "head_code": "GE48_2", @@ -1330,7 +1382,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE49_1": { "head_code": "GE49_1", @@ -1353,7 +1406,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE49_2": { "head_code": "GE49_2", @@ -1376,7 +1430,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE50_1": { "head_code": "GE50_1", @@ -1399,7 +1454,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE51_2": { "head_code": "GE51_2", @@ -1422,7 +1478,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE52_1": { "head_code": "GE52_1", @@ -1447,7 +1504,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE52_2": { "head_code": "GE52_2", @@ -1470,7 +1528,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE53_1": { "head_code": "GE53_1", @@ -1495,7 +1554,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE53_2": { "head_code": "GE53_2", @@ -1518,7 +1578,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE53_3": { "head_code": "GE53_3", @@ -1543,7 +1604,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE55_3": { "head_code": "GE55_3", @@ -1566,7 +1628,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE58_1": { "head_code": "GE58_1", @@ -1589,7 +1652,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE59_1": { "head_code": "GE59_1", @@ -1615,7 +1679,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE60_1": { "head_code": "GE60_1", @@ -1643,7 +1708,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE64_1": { "head_code": "GE64_1", @@ -1669,7 +1735,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE68_1": { "head_code": "GE68_1", @@ -1697,7 +1764,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE76_1": { "head_code": "GE76_1", @@ -1720,7 +1788,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE78_1": { "head_code": "GE78_1", @@ -1744,7 +1813,8 @@ "price_range": [ 3599.0, 3899.0 - ] + ], + "face_features": null }, "GE78_2": { "head_code": "GE78_2", @@ -1767,7 +1837,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE81_1": { "head_code": "GE81_1", @@ -1790,7 +1861,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE82_1": { "head_code": "GE82_1", @@ -1818,7 +1890,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE86_1": { "head_code": "GE86_1", @@ -1841,7 +1914,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE88_2": { "head_code": "GE88_2", @@ -1864,7 +1938,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE94_1": { "head_code": "GE94_1", @@ -1887,7 +1962,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE94_2": { "head_code": "GE94_2", @@ -1913,7 +1989,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE95_1": { "head_code": "GE95_1", @@ -1939,7 +2016,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE95_2": { "head_code": "GE95_2", @@ -1965,7 +2043,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE95_3": { "head_code": "GE95_3", @@ -1991,7 +2070,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE95_4": { "head_code": "GE95_4", @@ -2017,7 +2097,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE95_5": { "head_code": "GE95_5", @@ -2043,7 +2124,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE95_6": { "head_code": "GE95_6", @@ -2069,7 +2151,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE95_7": { "head_code": "GE95_7", @@ -2095,7 +2178,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE97_1": { "head_code": "GE97_1", @@ -2121,7 +2205,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "GE98_1": { "head_code": "GE98_1", @@ -2144,7 +2229,8 @@ "price_range": [ 3899.0, 3899.0 - ] + ], + "face_features": null }, "GE98_2": { "head_code": "GE98_2", @@ -2167,7 +2253,8 @@ "price_range": [ 3599.0, 3599.0 - ] + ], + "face_features": null }, "KE01_1": { "head_code": "KE01_1", @@ -2190,7 +2277,8 @@ "price_range": [ 3869.1, 3869.1 - ] + ], + "face_features": null }, "KE02_1": { "head_code": "KE02_1", @@ -2215,7 +2303,8 @@ "price_range": [ 3869.1, 3869.1 - ] + ], + "face_features": null }, "KE03_1": { "head_code": "KE03_1", @@ -2238,7 +2327,8 @@ "price_range": [ 3869.1, 3869.1 - ] + ], + "face_features": null }, "ZFE01_1": { "head_code": "ZFE01_1", @@ -2265,7 +2355,8 @@ "price_range": [ 4299.0, 4299.0 - ] + ], + "face_features": null }, "ZFE01_2": { "head_code": "ZFE01_2", @@ -2290,7 +2381,8 @@ "price_range": [ 4299.0, 4299.0 - ] + ], + "face_features": null }, "ZFE03_1": { "head_code": "ZFE03_1", @@ -2315,7 +2407,8 @@ "price_range": [ 4299.0, 4299.0 - ] + ], + "face_features": null }, "ZFE05_1": { "head_code": "ZFE05_1", @@ -2340,7 +2433,8 @@ "price_range": [ 4299.0, 4299.0 - ] + ], + "face_features": null }, "ZX201_2": { "head_code": "ZX201_2", @@ -2358,7 +2452,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZX201_2_ZX172E/ZX201_2_ZX172E-102.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZX204_1": { "head_code": "ZX204_1", @@ -2376,7 +2471,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZX204_1_ZX172E/ZX204_1_ZX172E-102.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZX207_1": { "head_code": "ZX207_1", @@ -2394,7 +2490,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZX207_1_ZX172E/ZX207_1_ZX172E-105.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE200_1": { "head_code": "ZXE200_1", @@ -2420,7 +2517,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE200_2": { "head_code": "ZXE200_2", @@ -2443,7 +2541,8 @@ "price_range": [ 2000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE200_3": { "head_code": "ZXE200_3", @@ -2468,7 +2567,8 @@ "price_range": [ 2000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE200_W1": { "head_code": "ZXE200_W1", @@ -2486,7 +2586,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZXE200_W1_ZX171C/ZXE200_W1_ZX171C-102.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE200_W2": { "head_code": "ZXE200_W2", @@ -2504,7 +2605,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZXE200_W2_ZX172E/ZXE200_W2_ZX172E-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE200_Z1": { "head_code": "ZXE200_Z1", @@ -2522,7 +2624,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZXE200_Z1_ZX164G/ZXE200_Z1_ZX164G-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE201_1": { "head_code": "ZXE201_1", @@ -2554,7 +2657,8 @@ "price_range": [ 1000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE201_2": { "head_code": "ZXE201_2", @@ -2577,7 +2681,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE201_3": { "head_code": "ZXE201_3", @@ -2601,7 +2706,8 @@ "price_range": [ 1000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE201_4": { "head_code": "ZXE201_4", @@ -2631,7 +2737,8 @@ "price_range": [ 1000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE201_5": { "head_code": "ZXE201_5", @@ -2655,7 +2762,8 @@ "price_range": [ 1000.0, 1000.0 - ] + ], + "face_features": null }, "ZXE201_W2": { "head_code": "ZXE201_W2", @@ -2676,7 +2784,8 @@ ], "product_count": 2, "representative_image": "assets/thumbs/SLE-Series/ZXE201_W2_ZX153B/ZXE201_W2_ZX153B-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE201_Z1": { "head_code": "ZXE201_Z1", @@ -2694,7 +2803,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZXE201_Z1_ZX172E/ZXE201_Z1_ZX172E-103.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE202_1": { "head_code": "ZXE202_1", @@ -2718,7 +2828,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE202_2": { "head_code": "ZXE202_2", @@ -2741,7 +2852,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE202_Z1": { "head_code": "ZXE202_Z1", @@ -2759,7 +2871,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZXE202_Z1_ZX160J/ZXE202_Z1_ZX160J-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE203_1": { "head_code": "ZXE203_1", @@ -2788,7 +2901,8 @@ "price_range": [ 1000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE203_2": { "head_code": "ZXE203_2", @@ -2812,7 +2926,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE203_3": { "head_code": "ZXE203_3", @@ -2833,7 +2948,8 @@ "price_range": [ 1799.0, 1799.0 - ] + ], + "face_features": null }, "ZXE203_4": { "head_code": "ZXE203_4", @@ -2856,7 +2972,8 @@ "price_range": [ 1000.0, 1000.0 - ] + ], + "face_features": null }, "ZXE203_5": { "head_code": "ZXE203_5", @@ -2879,7 +2996,8 @@ "price_range": [ 1000.0, 1000.0 - ] + ], + "face_features": null }, "ZXE203_W1": { "head_code": "ZXE203_W1", @@ -2901,7 +3019,8 @@ ], "product_count": 3, "representative_image": "assets/thumbs/SLE-Series/ZXE203_W1_ZX172E/ZXE203_W1_ZX172E-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE203_Z1": { "head_code": "ZXE203_Z1", @@ -2919,7 +3038,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZXE203_Z1_ZX165D/ZXE203_Z1_ZX165D-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE204_1": { "head_code": "ZXE204_1", @@ -2954,7 +3074,8 @@ "price_range": [ 1000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE204_2": { "head_code": "ZXE204_2", @@ -2977,7 +3098,8 @@ "price_range": [ 1000.0, 1000.0 - ] + ], + "face_features": null }, "ZXE204_Z1": { "head_code": "ZXE204_Z1", @@ -2995,7 +3117,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZXE204_Z1_ZX172E/ZXE204_Z1_ZX172E-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE205_1": { "head_code": "ZXE205_1", @@ -3019,7 +3142,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE205_2": { "head_code": "ZXE205_2", @@ -3042,7 +3166,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE205_Z1": { "head_code": "ZXE205_Z1", @@ -3060,7 +3185,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZXE205_Z1_ZX166K/ZXE205_Z1_ZX166K-102.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE206_1": { "head_code": "ZXE206_1", @@ -3083,7 +3209,8 @@ "price_range": [ 2000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE206_2": { "head_code": "ZXE206_2", @@ -3107,7 +3234,8 @@ "price_range": [ 1000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE206_3": { "head_code": "ZXE206_3", @@ -3128,7 +3256,8 @@ "price_range": [ 1799.0, 1799.0 - ] + ], + "face_features": null }, "ZXE206_4": { "head_code": "ZXE206_4", @@ -3151,7 +3280,8 @@ "price_range": [ 1000.0, 1000.0 - ] + ], + "face_features": null }, "ZXE206_5": { "head_code": "ZXE206_5", @@ -3174,7 +3304,8 @@ "price_range": [ 1000.0, 1000.0 - ] + ], + "face_features": null }, "ZXE206_W1": { "head_code": "ZXE206_W1", @@ -3195,7 +3326,8 @@ ], "product_count": 2, "representative_image": "assets/thumbs/SLE-Series/ZXE206_W1_ZX153B/ZXE206_W1_ZX153B-120_195fc9d2-52b2-453a-b899-69d67c0ec99c.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE206_W2": { "head_code": "ZXE206_W2", @@ -3214,7 +3346,8 @@ "characters": [], "product_count": 2, "representative_image": "assets/SLE-Series/ZXE206_W2_ZX117E/ZXE206_W2_ZX117E-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE206_Z1": { "head_code": "ZXE206_Z1", @@ -3232,7 +3365,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZXE206_Z1_ZX160J/ZXE206_Z1_ZX160J-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE207_1": { "head_code": "ZXE207_1", @@ -3255,7 +3389,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE207_2": { "head_code": "ZXE207_2", @@ -3278,7 +3413,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE207_3": { "head_code": "ZXE207_3", @@ -3301,7 +3437,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE207_Z1": { "head_code": "ZXE207_Z1", @@ -3319,7 +3456,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZXE207_Z1_ZX160J/ZXE207_Z1_ZX160J-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE208_1": { "head_code": "ZXE208_1", @@ -3342,7 +3480,8 @@ "price_range": [ 2000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE208_4": { "head_code": "ZXE208_4", @@ -3365,7 +3504,8 @@ "price_range": [ 1000.0, 1000.0 - ] + ], + "face_features": null }, "ZXE208_5": { "head_code": "ZXE208_5", @@ -3392,7 +3532,8 @@ "price_range": [ 1000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE208_W1": { "head_code": "ZXE208_W1", @@ -3412,7 +3553,8 @@ ], "product_count": 1, "representative_image": "assets/thumbs/SLE-Series/ZXE208_W1_ZX171C/ZXE208_W1_ZX171C-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE208_Z1": { "head_code": "ZXE208_Z1", @@ -3430,7 +3572,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZXE208_Z1_ZX166K/ZXE208_Z1_ZX166K-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE209_1": { "head_code": "ZXE209_1", @@ -3453,7 +3596,8 @@ "price_range": [ 1799.0, 2000.0 - ] + ], + "face_features": null }, "ZXE209_2": { "head_code": "ZXE209_2", @@ -3476,7 +3620,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE209_W1": { "head_code": "ZXE209_W1", @@ -3494,7 +3639,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZXE209_W1_ZX172E/ZXE209_W1_ZX172E-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE209_Z1": { "head_code": "ZXE209_Z1", @@ -3512,7 +3658,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZXE209_Z1_ZX172E/ZXE209_Z1_ZX172E-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE210_1": { "head_code": "ZXE210_1", @@ -3535,7 +3682,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE210_2": { "head_code": "ZXE210_2", @@ -3558,7 +3706,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE211_1": { "head_code": "ZXE211_1", @@ -3581,7 +3730,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE211_2": { "head_code": "ZXE211_2", @@ -3604,7 +3754,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE212_1": { "head_code": "ZXE212_1", @@ -3628,7 +3779,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE212_2": { "head_code": "ZXE212_2", @@ -3651,7 +3803,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE212_W1": { "head_code": "ZXE212_W1", @@ -3667,7 +3820,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZXE212_W1-ZX172E/ZXE212_W1-ZX172E-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE212_W2": { "head_code": "ZXE212_W2", @@ -3687,7 +3841,8 @@ ], "product_count": 1, "representative_image": "assets/thumbs/SLE-Series/ZXE212_W2_ZX171C/ZXE212_W2_ZX171C-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE213_1": { "head_code": "ZXE213_1", @@ -3711,7 +3866,8 @@ "price_range": [ 1000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE213_2": { "head_code": "ZXE213_2", @@ -3736,7 +3892,8 @@ "price_range": [ 1000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE214_1": { "head_code": "ZXE214_1", @@ -3759,7 +3916,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE214_2": { "head_code": "ZXE214_2", @@ -3782,7 +3940,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE215_1": { "head_code": "ZXE215_1", @@ -3814,7 +3973,8 @@ "price_range": [ 1000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE215_2": { "head_code": "ZXE215_2", @@ -3837,7 +3997,8 @@ "price_range": [ 1000.0, 1000.0 - ] + ], + "face_features": null }, "ZXE215_3": { "head_code": "ZXE215_3", @@ -3860,7 +4021,8 @@ "price_range": [ 1000.0, 1000.0 - ] + ], + "face_features": null }, "ZXE215_W1": { "head_code": "ZXE215_W1", @@ -3881,7 +4043,8 @@ ], "product_count": 2, "representative_image": "assets/thumbs/SLE-Series/ZXE215_W1_ZX171C/ZXE215_W1_ZX171C-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE216_1": { "head_code": "ZXE216_1", @@ -3913,7 +4076,8 @@ "price_range": [ 1000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE216_2": { "head_code": "ZXE216_2", @@ -3937,7 +4101,8 @@ "price_range": [ 1000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE216_3": { "head_code": "ZXE216_3", @@ -3964,7 +4129,8 @@ "price_range": [ 1000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE216_4": { "head_code": "ZXE216_4", @@ -3988,7 +4154,8 @@ "price_range": [ 1000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE216_W1": { "head_code": "ZXE216_W1", @@ -4009,7 +4176,8 @@ ], "product_count": 2, "representative_image": "assets/thumbs/SLE-Series/ZXE216_W1_ZX172E/ZXE216_W1_ZX172E-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE217_1": { "head_code": "ZXE217_1", @@ -4034,7 +4202,8 @@ "price_range": [ 2000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE217_2": { "head_code": "ZXE217_2", @@ -4059,7 +4228,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE217_4": { "head_code": "ZXE217_4", @@ -4082,7 +4252,8 @@ "price_range": [ 1000.0, 1000.0 - ] + ], + "face_features": null }, "ZXE217_5": { "head_code": "ZXE217_5", @@ -4105,7 +4276,8 @@ "price_range": [ 1000.0, 1000.0 - ] + ], + "face_features": null }, "ZXE218_1": { "head_code": "ZXE218_1", @@ -4129,7 +4301,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE218_2": { "head_code": "ZXE218_2", @@ -4152,7 +4325,8 @@ "price_range": [ 2000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE218_W1": { "head_code": "ZXE218_W1", @@ -4173,7 +4347,8 @@ ], "product_count": 2, "representative_image": "assets/thumbs/SLE-Series/ZXE218_W1_ZX171C/ZXE218_W1_ZX171C-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE218_W2": { "head_code": "ZXE218_W2", @@ -4191,7 +4366,8 @@ "characters": [], "product_count": 1, "representative_image": "assets/SLE-Series/ZXE218_W2_ZX117E/ZXE218_W2_ZX117E-101.jpg", - "price_range": null + "price_range": null, + "face_features": null }, "ZXE219_1": { "head_code": "ZXE219_1", @@ -4218,7 +4394,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE219_2": { "head_code": "ZXE219_2", @@ -4241,7 +4418,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE220_1": { "head_code": "ZXE220_1", @@ -4264,7 +4442,8 @@ "price_range": [ 2000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE220_2": { "head_code": "ZXE220_2", @@ -4287,7 +4466,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE221_2": { "head_code": "ZXE221_2", @@ -4310,7 +4490,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE222_1": { "head_code": "ZXE222_1", @@ -4335,7 +4516,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE223_1": { "head_code": "ZXE223_1", @@ -4362,7 +4544,8 @@ "price_range": [ 1000.0, 2000.0 - ] + ], + "face_features": null }, "ZXE223_2": { "head_code": "ZXE223_2", @@ -4385,7 +4568,8 @@ "price_range": [ 1000.0, 1000.0 - ] + ], + "face_features": null }, "ZXE223_3": { "head_code": "ZXE223_3", @@ -4408,7 +4592,8 @@ "price_range": [ 1000.0, 1000.0 - ] + ], + "face_features": null }, "ZXE224_1": { "head_code": "ZXE224_1", @@ -4431,7 +4616,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE224_2": { "head_code": "ZXE224_2", @@ -4456,7 +4642,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE226_1": { "head_code": "ZXE226_1", @@ -4481,7 +4668,8 @@ "price_range": [ 1600.0, 2000.0 - ] + ], + "face_features": null }, "ZXE231_1": { "head_code": "ZXE231_1", @@ -4506,7 +4694,8 @@ "price_range": [ 2000.0, 2000.0 - ] + ], + "face_features": null } } } diff --git a/db/neck_compatibility.json b/db/neck_compatibility.json new file mode 100644 index 0000000..11dfa73 --- /dev/null +++ b/db/neck_compatibility.json @@ -0,0 +1,184 @@ +{ + "version": "1.0.0", + "_comment": "Neck-connector compatibility — the basis for interchangeable heads (PDR-011 §8). neck_class is the compatibility key; heads fit a body when their classes match (see `compatibility`). neck_class is derived from spec-card neck CIRCUMFERENCE (threshold 29.0cm) until a measured `neck_joint_mm` connector spec is supplied per body — fill neck_joint_mm to replace the estimate. Generated by scripts/build_neck_compat.py; consumed by build_heads.py and configurator.html.", + "generated_from": [ + "db/body_measurements.json" + ], + "neck_slim_max_cm": 29.0, + "classes": { + "slim": { + "neck_circ_cm_max": 29.0, + "typical_lines": [ + "K-Series" + ], + "connector": "narrow" + }, + "standard": { + "neck_circ_cm_min": 29.0, + "typical_lines": [ + "Fusion", + "I-Series", + "SLE 3.0" + ], + "connector": "standard" + } + }, + "compatibility": { + "slim": [ + "slim" + ], + "standard": [ + "standard" + ] + }, + "count": 21, + "bodies": { + "ZC165D": { + "line": "Classic", + "neck_circ_cm": null, + "neck_class": null, + "neck_joint_mm": null, + "class_basis": "line-default" + }, + "ZF161D": { + "line": "Fusion", + "neck_circ_cm": null, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "line-default" + }, + "ZF168B": { + "line": "Fusion", + "neck_circ_cm": 31, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + }, + "ZF169C": { + "line": "Fusion", + "neck_circ_cm": 32, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + }, + "ZG162D": { + "line": "I-Series", + "neck_circ_cm": 31.5, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + }, + "ZG170C": { + "line": "I-Series", + "neck_circ_cm": 33, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + }, + "ZG170D": { + "line": "I-Series", + "neck_circ_cm": 32, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + }, + "ZG175E": { + "line": "I-Series", + "neck_circ_cm": 30, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + }, + "ZGX165F": { + "line": "I-Series", + "neck_circ_cm": 30, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + }, + "ZK159D": { + "line": "K-Series", + "neck_circ_cm": 26.5, + "neck_class": "slim", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + }, + "ZK168B": { + "line": "K-Series", + "neck_circ_cm": 27.5, + "neck_class": "slim", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + }, + "ZS165D": { + "line": "Sculpt", + "neck_circ_cm": null, + "neck_class": null, + "neck_joint_mm": null, + "class_basis": "line-default" + }, + "ZX153B": { + "line": "SLE 3.0", + "neck_circ_cm": null, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "line-default" + }, + "ZX160J": { + "line": "SLE 3.0", + "neck_circ_cm": 32, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + }, + "ZX163E": { + "line": "SLE 3.0", + "neck_circ_cm": null, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "line-default" + }, + "ZX164G": { + "line": "SLE 3.0", + "neck_circ_cm": 32.5, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + }, + "ZX165D": { + "line": "SLE 3.0", + "neck_circ_cm": 32.5, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + }, + "ZX166K": { + "line": "SLE 3.0", + "neck_circ_cm": 32, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + }, + "ZX170A": { + "line": "SLE 3.0", + "neck_circ_cm": 32.5, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + }, + "ZX171C": { + "line": "SLE 3.0", + "neck_circ_cm": 33, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + }, + "ZX172E": { + "line": "SLE 3.0", + "neck_circ_cm": 32.5, + "neck_class": "standard", + "neck_joint_mm": null, + "class_basis": "measured-circumference" + } + } +} diff --git a/docs/pdr/PDR-011-declarative-configurator.md b/docs/pdr/PDR-011-declarative-configurator.md index 8a3e982..b1c5408 100644 --- a/docs/pdr/PDR-011-declarative-configurator.md +++ b/docs/pdr/PDR-011-declarative-configurator.md @@ -283,9 +283,41 @@ clearly-labelled custom path — and it surfaces a real commercial insight: a K-Series body buyer has only 3 interchangeable heads, whereas a `standard`-neck body opens up 186. +**Neck class is now stored data, not a constant in page JS.** +`scripts/build_neck_compat.py` → `db/neck_compatibility.json` is the single source +of truth: an explicit `neck_class` per body, the `classes`/`compatibility` matrix, +and the threshold — all in one reviewable place that both `build_heads.py` (via +import) and `configurator.html` read. Each body also carries a `neck_joint_mm` +slot (currently `null`): the hook for the **true measured connector spec**. Until +that is filled, `neck_class` is circumference-derived and the UI labels cross-line +fit as estimated; once `neck_joint_mm` is populated, classes key off the real +measurement and the estimate disappears — no code change, just data. + +--- + +## 9. Face & finish — an honest proposed layer + +The natural next axis is per-head face features (eyes, makeup, brows, wig, +freckles). The honest finding: **there is no structured face-feature data in +`db/`** to reverse-engineer — the only real per-head face attribute is the +movable-jaw variant (already modelled as `face_variant`). Character profiles +carry persona/energy/tagline copy, and the stories mention "eyes"/"hair" only as +prose. + +So rather than fabricate per-head eye colours, the configurator adds a **Face & +Finish** group whose axes are all `provenance: proposed` — the standard ZELEX +customization option set (eye colour, makeup, eyebrows, wig, freckles), offered +globally at config time, composing into the build as a *custom finish* (no SKU +impact) and summarised in the resolve panel. `db/heads.json` gains a +`face_features` slot per head (`null`) as the explicit backfill target: populate +it per head and these axes become head-aware (real defaults + availability) +instead of global proposals. This mirrors the existing `proposed` component axes +(standing feet, fingers) — surfaced as real options, flagged honestly as +not-yet-backed-by-data. + --- -## 9. Strategic conclusion +## 10. Strategic conclusion 1. Declarative beats hand-coded for a catalog that already regenerates from JSON: new bodies/tones/heads become **data** changes, not code changes. @@ -304,7 +336,7 @@ every build is a measured architecture or an honest inquiry."* --- -## 10. Acceptance Review +## 11. Acceptance Review | Criterion | Status | |---|---| @@ -321,4 +353,6 @@ every build is a measured architecture or an honest inquiry."* | Heads given a structured model (the head generator) | ✓ `scripts/build_heads.py` → `db/heads.json` (189 heads: line, neck_class, MJ, tones, paired bodies, cast, image, price) | | Head picker is an image gallery driven by that data | ✓ `head_code` renders as a thumbnail gallery; MJ gated to capable heads; falls back to a dropdown if `heads.json` missing | | Head↔line coupling understood + interchangeability pathway | ✓ neck-class model (slim vs standard); cross-line toggle; `cross_line_head` warn + `neck_incompatible` error; verified no slim/standard leakage | +| Neck class is stored data with a measured-spec backfill slot | ✓ `scripts/build_neck_compat.py` → `db/neck_compatibility.json` (per-body `neck_class` + `neck_joint_mm:null`); imported by build_heads + read by the page | +| Face features added honestly (no fabricated per-head data) | ✓ Face & Finish group, all `provenance: proposed`; `face_features` backfill slot in `heads.json`; resolve panel "custom finish" summary | | Both JSON artifacts parse; new page passes site validation | ✓ `validate-site.mjs` green (page added to its KIT_PAGES list) | diff --git a/scripts/build_heads.py b/scripts/build_heads.py index a62caed..13d605f 100644 --- a/scripts/build_heads.py +++ b/scripts/build_heads.py @@ -25,6 +25,9 @@ from collections import defaultdict from pathlib import Path +# shared neck-connector definitions (single source of truth for the threshold) +from build_neck_compat import neck_class, NECK_SLIM_MAX, LINE_NECK_CLASS + ROOT = Path(__file__).resolve().parent.parent DB = ROOT / "db" OUT = DB / "heads.json" @@ -34,25 +37,6 @@ # fallback: head-code prefix -> line, for heads with no/ambiguous product series_id PREFIX_LINE = {"ZXE": "SLE 3.0", "GE": "I-Series", "ZFE": "Fusion", "KE": "K-Series"} -# --- neck-connector classes (the basis for interchangeable heads) ---------------- -# Heads attach at the neck. Marketing line is NOT the constraint; the neck -# circumference is. db/body_measurements.json shows two clusters: K-Series ~27cm -# (narrow) vs Fusion/I-Series/SLE ~31-33cm (standard). Heads sharing a class are -# physically swappable across lines even though the catalog only sells line-matched -# pairs. Threshold is an ESTIMATE from the spec cards — confirm against the physical -# neck joint before treating a cross-line swap as guaranteed-fit. -NECK_SLIM_MAX = 29.0 # cm; below this = the narrow (K-Series) connector - - -def neck_class(neck_cm): - if neck_cm is None: - return None - return "slim" if neck_cm < NECK_SLIM_MAX else "standard" - - -# line -> default neck class, used when a head has no measured paired body -LINE_NECK_CLASS = {"K-Series": "slim", "I-Series": "standard", "Fusion": "standard", "SLE 3.0": "standard"} - def load(name): return json.loads((DB / name).read_text(encoding="utf-8")) @@ -176,6 +160,10 @@ def main(): "product_count": h["product_count"], "representative_image": images[0] if images else None, "price_range": [prices[0], prices[-1]] if prices else None, + # Backfill slot for per-head face features (eye colour, makeup, brows, wig, + # freckles). null = no structured data yet; the configurator's Face & Finish + # axes are global PROPOSED options until this is populated per head. + "face_features": None, } payload = { diff --git a/scripts/build_neck_compat.py b/scripts/build_neck_compat.py new file mode 100644 index 0000000..5359a0a --- /dev/null +++ b/scripts/build_neck_compat.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +""" +Build db/neck_compatibility.json — the single source of truth for which heads +physically fit which bodies (the basis for interchangeable heads, PDR-011 §8). + +Heads attach at the neck, so interchangeability is governed by the neck connector, +NOT the marketing line. The only measured proxy on the spec cards is neck +CIRCUMFERENCE, which clusters cleanly: K-Series ~27cm (narrow) vs Fusion / +I-Series / SLE ~31-33cm (standard). This script promotes that into an explicit, +reviewable per-body neck_class — instead of a magic constant living in page JS — +and leaves a `neck_joint_mm` slot (null) for the TRUE connector measurement. +Until that real spec is supplied, neck_class is circumference-derived (an +estimate); once `neck_joint_mm` is filled per body, classes should key off it. + +This module also exports the threshold + helpers that scripts/build_heads.py +imports, so both files share one definition. + +Sources: db/body_measurements.json (neck circumference per body) +Run before build_heads.py. Usage: python scripts/build_neck_compat.py +""" +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +DB = ROOT / "db" +OUT = DB / "neck_compatibility.json" + +# Threshold (cm) below which a neck is the narrow (K-Series) connector. ESTIMATE +# from spec-card circumferences — replace with a measured neck_joint_mm rule when +# the physical connector spec is available. +NECK_SLIM_MAX = 29.0 + +# line -> default neck class, used when a record has no measured neck +LINE_NECK_CLASS = {"K-Series": "slim", "I-Series": "standard", "Fusion": "standard", "SLE 3.0": "standard"} + +# which body classes a head class fits (currently 1:1; widen here if a connector +# turns out to be cross-compatible once real joint specs land) +COMPATIBILITY = {"slim": ["slim"], "standard": ["standard"]} + + +def neck_class(neck_cm): + """Circumference-derived neck class. Returns None when neck_cm is unknown.""" + if neck_cm is None: + return None + return "slim" if neck_cm < NECK_SLIM_MAX else "standard" + + +def main(): + bodies_meas = json.loads((DB / "body_measurements.json").read_text(encoding="utf-8")).get("bodies", {}) + + bodies = {} + for code in sorted(bodies_meas): + b = bodies_meas[code] + neck = b.get("neck") + line = b.get("line") + # normalize the SLE label used in measurements ("SLE 3.0") to the line name + cls = neck_class(neck) or LINE_NECK_CLASS.get(line) + bodies[code] = { + "line": line, + "neck_circ_cm": neck, + "neck_class": cls, + "neck_joint_mm": None, # TRUE connector spec — pending physical confirmation + "class_basis": "measured-circumference" if neck is not None else "line-default", + } + + payload = { + "version": "1.0.0", + "_comment": ( + "Neck-connector compatibility — the basis for interchangeable heads (PDR-011 §8). " + "neck_class is the compatibility key; heads fit a body when their classes match " + "(see `compatibility`). neck_class is derived from spec-card neck CIRCUMFERENCE " + f"(threshold {NECK_SLIM_MAX}cm) until a measured `neck_joint_mm` connector spec is " + "supplied per body — fill neck_joint_mm to replace the estimate. Generated by " + "scripts/build_neck_compat.py; consumed by build_heads.py and configurator.html." + ), + "generated_from": ["db/body_measurements.json"], + "neck_slim_max_cm": NECK_SLIM_MAX, + "classes": { + "slim": {"neck_circ_cm_max": NECK_SLIM_MAX, "typical_lines": ["K-Series"], "connector": "narrow"}, + "standard": {"neck_circ_cm_min": NECK_SLIM_MAX, "typical_lines": ["Fusion", "I-Series", "SLE 3.0"], "connector": "standard"}, + }, + "compatibility": COMPATIBILITY, + "count": len(bodies), + "bodies": bodies, + } + OUT.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + + by_class = {} + for r in bodies.values(): + by_class[r["neck_class"]] = by_class.get(r["neck_class"], 0) + 1 + print(f"Wrote {OUT.relative_to(ROOT)} — {len(bodies)} bodies") + for cls, n in sorted(by_class.items(), key=lambda kv: str(kv[0])): + print(f" {str(cls):10} {n}") + missing = sum(1 for r in bodies.values() if r["neck_joint_mm"] is None) + print(f" awaiting measured neck_joint_mm: {missing} (class currently circumference-derived)") + + +if __name__ == "__main__": + main() From bf27f0d5ef071f3e19383f8422199f3a33bdc654 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 6 Jun 2026 18:03:25 +0000 Subject: [PATCH 5/5] docs(pdr-011): manufacturer data-request templates for neck + face slots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add scripts/build_data_request.py → docs/data-requests/ (re-runnable): fill-in CSV templates + cover README for the two data points that close the estimate/proposed gaps. - neck-joint-request.csv (21 bodies): neck_joint_diameter_mm + type to replace the circumference-derived neck_class. - head-features-request.csv (189 heads, sorted priority-1 = has cast): per-head eye/makeup/eyebrows/wig/freckles defaults + availability for the face_features slot. Each row is pre-filled with known context; returned values load directly into db/neck_compatibility.json and db/heads.json with no schema change. PDR-011 §9 points to the templates. --- docs/data-requests/README.md | 49 +++++ docs/data-requests/head-features-request.csv | 190 +++++++++++++++++++ docs/data-requests/neck-joint-request.csv | 22 +++ docs/pdr/PDR-011-declarative-configurator.md | 8 + scripts/build_data_request.py | 163 ++++++++++++++++ 5 files changed, 432 insertions(+) create mode 100644 docs/data-requests/README.md create mode 100644 docs/data-requests/head-features-request.csv create mode 100644 docs/data-requests/neck-joint-request.csv create mode 100644 scripts/build_data_request.py diff --git a/docs/data-requests/README.md b/docs/data-requests/README.md new file mode 100644 index 0000000..dc6a5f1 --- /dev/null +++ b/docs/data-requests/README.md @@ -0,0 +1,49 @@ +# ZELEX — Manufacturer Data Request + +Two data points let the configurator (PDR-011) stop estimating and start using +real specs. Please fill the `<-- FILL` columns in the two CSVs in this folder and +return them. Pre-filled columns are what we already have — for context, do not +change them. + +## 1. Neck joint spec — `neck-joint-request.csv` (21 bodies) + +**Why:** heads are interchangeable when their neck CONNECTOR matches. We currently +estimate this from neck *circumference* (a soft-tissue proxy). The real connector +spec makes it exact. + +| Column | What we need | +|---|---| +| `neck_joint_diameter_mm` | Diameter (mm) of the head-mount connector at the neck. | +| `neck_joint_type` | Connector mechanism: screw / bayonet / magnet / peg / other. | +| `interchange_group` | If the factory already groups connectors (e.g. "A"/"B"), the group id for this body. Optional. | + +Two bodies with the same diameter + type are physically interchangeable, +regardless of product line. + +## 2. Per-head face features — `head-features-request.csv` (189 heads) + +**Why:** the configurator's *Face & Finish* options (eye colour, makeup, eyebrows, +wig, freckles) are currently GLOBAL proposals — we have no per-head defaults or +availability. Fill these to make each head show its true shipped finish and which +options it actually supports. Rows are sorted by **priority** (1 = heads with a +named character, i.e. the ones buyers see most) — fill those first if time is short. + +| Column | What we need | +|---|---| +| `default_eye_color` | Eye colour the head ships with. | +| `eye_swappable` | Can the acrylic eyes be changed? Y/N. | +| `default_makeup` | Factory makeup style (Natural / Glam / …). | +| `eyebrows` | Painted or implanted brow hair. | +| `default_wig_color` | Wig colour shipped. | +| `wig_removable` | Removable wig? Y/N. | +| `freckles_available` | Optional painted freckles offered? Y/N. | + +## Returning the data + +Return the filled CSVs as-is. We load `neck_joint_mm` into +`db/neck_compatibility.json` (per body) and the face fields into the +`face_features` slot in `db/heads.json` (per head) — no schema changes needed; the +configurator picks them up automatically. + +> Regenerate these templates after any catalog change: +> `python scripts/build_data_request.py` diff --git a/docs/data-requests/head-features-request.csv b/docs/data-requests/head-features-request.csv new file mode 100644 index 0000000..94793dc --- /dev/null +++ b/docs/data-requests/head-features-request.csv @@ -0,0 +1,190 @@ +head_code,line,neck_class,movable_jaw (known),priority (1=has cast),default_eye_color <-- FILL,eye_swappable <-- FILL (Y/N),default_makeup <-- FILL,eyebrows <-- FILL (Painted/Implanted),default_wig_color <-- FILL,wig_removable <-- FILL (Y/N),freckles_available <-- FILL (Y/N),notes +GE02_1,I-Series,standard,Y,1,,,,,,,, +GE115_1,I-Series,standard,Y,1,,,,,,,, +GE127_1,I-Series,standard,Y,1,,,,,,,, +GE134_1,I-Series,standard,Y,1,,,,,,,, +GE138_1,I-Series,standard,Y,1,,,,,,,, +GE139_1,I-Series,standard,Y,1,,,,,,,, +GE141_1,I-Series,standard,Y,1,,,,,,,, +GE149_1,I-Series,standard,Y,1,,,,,,,, +GE41_1,I-Series,standard,Y,1,,,,,,,, +GE45_8,I-Series,standard,N,1,,,,,,,, +GE47_1,I-Series,standard,N,1,,,,,,,, +GE52_1,I-Series,standard,N,1,,,,,,,, +GE53_1,I-Series,standard,N,1,,,,,,,, +GE53_3,I-Series,standard,N,1,,,,,,,, +GE60_1,I-Series,standard,Y,1,,,,,,,, +GE68_1,I-Series,standard,Y,1,,,,,,,, +GE82_1,I-Series,standard,Y,1,,,,,,,, +KE01_1,K-Series,slim,N,1,,,,,,,, +KE02_1,K-Series,slim,N,1,,,,,,,, +KE03_1,K-Series,slim,N,1,,,,,,,, +ZFE01_1,Fusion,standard,N,1,,,,,,,, +ZFE01_2,Fusion,standard,N,1,,,,,,,, +ZFE03_1,Fusion,standard,N,1,,,,,,,, +ZFE05_1,Fusion,standard,N,1,,,,,,,, +ZXE200_1,SLE 3.0,standard,N,1,,,,,,,, +ZXE200_3,SLE 3.0,standard,N,1,,,,,,,, +ZXE201_1,SLE 3.0,standard,N,1,,,,,,,, +ZXE201_4,SLE 3.0,standard,N,1,,,,,,,, +ZXE201_W2,SLE 3.0,standard,N,1,,,,,,,, +ZXE203_W1,SLE 3.0,standard,N,1,,,,,,,, +ZXE204_1,SLE 3.0,standard,N,1,,,,,,,, +ZXE206_W1,SLE 3.0,standard,N,1,,,,,,,, +ZXE208_5,SLE 3.0,standard,N,1,,,,,,,, +ZXE208_W1,SLE 3.0,standard,N,1,,,,,,,, +ZXE212_W2,SLE 3.0,standard,N,1,,,,,,,, +ZXE215_1,SLE 3.0,standard,N,1,,,,,,,, +ZXE215_W1,SLE 3.0,standard,N,1,,,,,,,, +ZXE216_1,SLE 3.0,standard,N,1,,,,,,,, +ZXE216_3,SLE 3.0,standard,N,1,,,,,,,, +ZXE216_W1,SLE 3.0,standard,N,1,,,,,,,, +ZXE217_1,SLE 3.0,standard,N,1,,,,,,,, +ZXE217_2,SLE 3.0,standard,N,1,,,,,,,, +ZXE218_W1,SLE 3.0,standard,N,1,,,,,,,, +ZXE219_1,SLE 3.0,standard,N,1,,,,,,,, +ZXE222_1,SLE 3.0,standard,N,1,,,,,,,, +ZXE223_1,SLE 3.0,standard,N,1,,,,,,,, +ZXE224_2,SLE 3.0,standard,N,1,,,,,,,, +ZXE226_1,SLE 3.0,standard,N,1,,,,,,,, +ZXE231_1,SLE 3.0,standard,N,1,,,,,,,, +GE03_1,I-Series,standard,Y,2,,,,,,,, +GE03_2,I-Series,standard,Y,2,,,,,,,, +GE07_1,I-Series,standard,N,2,,,,,,,, +GE07_3,I-Series,standard,N,2,,,,,,,, +GE100_1,I-Series,standard,N,2,,,,,,,, +GE109_1,I-Series,standard,N,2,,,,,,,, +GE111_1,I-Series,standard,N,2,,,,,,,, +GE111_2,I-Series,standard,N,2,,,,,,,, +GE111_3,I-Series,standard,N,2,,,,,,,, +GE113_1,I-Series,standard,Y,2,,,,,,,, +GE114_1,I-Series,standard,Y,2,,,,,,,, +GE116_1,I-Series,standard,Y,2,,,,,,,, +GE122_1,I-Series,standard,N,2,,,,,,,, +GE122_2,I-Series,standard,N,2,,,,,,,, +GE123_1,I-Series,standard,N,2,,,,,,,, +GE124_1,I-Series,standard,N,2,,,,,,,, +GE124_2,I-Series,standard,N,2,,,,,,,, +GE125_1,I-Series,standard,N,2,,,,,,,, +GE125_2,I-Series,standard,N,2,,,,,,,, +GE137_1,I-Series,standard,Y,2,,,,,,,, +GE146_1,I-Series,standard,Y,2,,,,,,,, +GE14_1,I-Series,standard,Y,2,,,,,,,, +GE14_2,I-Series,standard,Y,2,,,,,,,, +GE14_3,I-Series,standard,Y,2,,,,,,,, +GE16_1,I-Series,standard,Y,2,,,,,,,, +GE16_2,I-Series,standard,Y,2,,,,,,,, +GE173_1,I-Series,standard,Y,2,,,,,,,, +GE24_1,I-Series,standard,N,2,,,,,,,, +GE45_1,I-Series,standard,N,2,,,,,,,, +GE45_2,I-Series,standard,N,2,,,,,,,, +GE45_5,I-Series,standard,N,2,,,,,,,, +GE45_6,I-Series,standard,N,2,,,,,,,, +GE45_7,I-Series,standard,N,2,,,,,,,, +GE46_2,I-Series,standard,N,2,,,,,,,, +GE46_3,I-Series,standard,N,2,,,,,,,, +GE46_4,I-Series,standard,N,2,,,,,,,, +GE46_5,I-Series,standard,N,2,,,,,,,, +GE48_1,I-Series,standard,N,2,,,,,,,, +GE48_2,I-Series,standard,N,2,,,,,,,, +GE49_1,I-Series,standard,N,2,,,,,,,, +GE49_2,I-Series,standard,N,2,,,,,,,, +GE50_1,I-Series,standard,N,2,,,,,,,, +GE51_2,I-Series,standard,N,2,,,,,,,, +GE52_2,I-Series,standard,N,2,,,,,,,, +GE53_2,I-Series,standard,N,2,,,,,,,, +GE55_3,I-Series,standard,N,2,,,,,,,, +GE58_1,I-Series,standard,N,2,,,,,,,, +GE59_1,I-Series,standard,Y,2,,,,,,,, +GE64_1,I-Series,standard,Y,2,,,,,,,, +GE76_1,I-Series,standard,N,2,,,,,,,, +GE78_1,I-Series,standard,N,2,,,,,,,, +GE78_2,I-Series,standard,N,2,,,,,,,, +GE81_1,I-Series,standard,N,2,,,,,,,, +GE86_1,I-Series,standard,N,2,,,,,,,, +GE88_2,I-Series,standard,N,2,,,,,,,, +GE94_1,I-Series,standard,N,2,,,,,,,, +GE94_2,I-Series,standard,Y,2,,,,,,,, +GE95_1,I-Series,standard,Y,2,,,,,,,, +GE95_2,I-Series,standard,Y,2,,,,,,,, +GE95_3,I-Series,standard,Y,2,,,,,,,, +GE95_4,I-Series,standard,Y,2,,,,,,,, +GE95_5,I-Series,standard,Y,2,,,,,,,, +GE95_6,I-Series,standard,Y,2,,,,,,,, +GE95_7,I-Series,standard,Y,2,,,,,,,, +GE97_1,I-Series,standard,Y,2,,,,,,,, +GE98_1,I-Series,standard,N,2,,,,,,,, +GE98_2,I-Series,standard,N,2,,,,,,,, +ZX201_2,SLE 3.0,standard,N,2,,,,,,,, +ZX204_1,SLE 3.0,standard,N,2,,,,,,,, +ZX207_1,SLE 3.0,standard,N,2,,,,,,,, +ZXE200_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE200_W1,SLE 3.0,standard,N,2,,,,,,,, +ZXE200_W2,SLE 3.0,standard,N,2,,,,,,,, +ZXE200_Z1,SLE 3.0,standard,N,2,,,,,,,, +ZXE201_3,SLE 3.0,standard,N,2,,,,,,,, +ZXE201_5,SLE 3.0,standard,N,2,,,,,,,, +ZXE201_Z1,SLE 3.0,standard,N,2,,,,,,,, +ZXE202_1,SLE 3.0,standard,N,2,,,,,,,, +ZXE202_Z1,SLE 3.0,standard,N,2,,,,,,,, +ZXE203_1,SLE 3.0,standard,N,2,,,,,,,, +ZXE203_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE203_3,SLE 3.0,standard,N,2,,,,,,,, +ZXE203_4,SLE 3.0,standard,N,2,,,,,,,, +ZXE203_5,SLE 3.0,standard,N,2,,,,,,,, +ZXE203_Z1,SLE 3.0,standard,N,2,,,,,,,, +ZXE204_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE204_Z1,SLE 3.0,standard,N,2,,,,,,,, +ZXE205_1,SLE 3.0,standard,N,2,,,,,,,, +ZXE205_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE205_Z1,SLE 3.0,standard,N,2,,,,,,,, +ZXE206_1,SLE 3.0,standard,N,2,,,,,,,, +ZXE206_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE206_3,SLE 3.0,standard,N,2,,,,,,,, +ZXE206_4,SLE 3.0,standard,N,2,,,,,,,, +ZXE206_5,SLE 3.0,standard,N,2,,,,,,,, +ZXE206_W2,SLE 3.0,standard,N,2,,,,,,,, +ZXE206_Z1,SLE 3.0,standard,N,2,,,,,,,, +ZXE207_3,SLE 3.0,standard,N,2,,,,,,,, +ZXE207_Z1,SLE 3.0,standard,N,2,,,,,,,, +ZXE208_4,SLE 3.0,standard,N,2,,,,,,,, +ZXE208_Z1,SLE 3.0,standard,N,2,,,,,,,, +ZXE209_1,SLE 3.0,standard,N,2,,,,,,,, +ZXE209_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE209_W1,SLE 3.0,standard,N,2,,,,,,,, +ZXE209_Z1,SLE 3.0,standard,N,2,,,,,,,, +ZXE211_1,SLE 3.0,standard,N,2,,,,,,,, +ZXE211_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE212_1,SLE 3.0,standard,N,2,,,,,,,, +ZXE212_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE212_W1,SLE 3.0,standard,N,2,,,,,,,, +ZXE213_1,SLE 3.0,standard,N,2,,,,,,,, +ZXE213_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE215_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE215_3,SLE 3.0,standard,N,2,,,,,,,, +ZXE216_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE216_4,SLE 3.0,standard,N,2,,,,,,,, +ZXE217_4,SLE 3.0,standard,N,2,,,,,,,, +ZXE217_5,SLE 3.0,standard,N,2,,,,,,,, +ZXE218_1,SLE 3.0,standard,N,2,,,,,,,, +ZXE218_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE218_W2,SLE 3.0,standard,N,2,,,,,,,, +ZXE220_1,SLE 3.0,standard,N,2,,,,,,,, +ZXE220_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE221_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE223_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE223_3,SLE 3.0,standard,N,2,,,,,,,, +ZXE224_1,SLE 3.0,standard,N,2,,,,,,,, +GE101_1,I-Series,standard,N,2,,,,,,,, +GE125_3,I-Series,standard,N,2,,,,,,,, +GE139_2,I-Series,standard,N,2,,,,,,,, +ZXE201_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE202_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE207_1,SLE 3.0,standard,N,2,,,,,,,, +ZXE207_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE208_1,SLE 3.0,standard,N,2,,,,,,,, +ZXE210_1,SLE 3.0,standard,N,2,,,,,,,, +ZXE210_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE214_1,SLE 3.0,standard,N,2,,,,,,,, +ZXE214_2,SLE 3.0,standard,N,2,,,,,,,, +ZXE219_2,SLE 3.0,standard,N,2,,,,,,,, diff --git a/docs/data-requests/neck-joint-request.csv b/docs/data-requests/neck-joint-request.csv new file mode 100644 index 0000000..e6f063b --- /dev/null +++ b/docs/data-requests/neck-joint-request.csv @@ -0,0 +1,22 @@ +body_code,line,neck_circ_cm (known),neck_class (current estimate),neck_joint_diameter_mm <-- FILL,neck_joint_type <-- FILL (screw/bayonet/magnet/peg),"interchange_group <-- FILL (factory connector group, optional)",notes +ZC165D,Classic,,,,,, +ZF161D,Fusion,,standard,,,, +ZF168B,Fusion,31,standard,,,, +ZF169C,Fusion,32,standard,,,, +ZG162D,I-Series,31.5,standard,,,, +ZG170C,I-Series,33,standard,,,, +ZG170D,I-Series,32,standard,,,, +ZG175E,I-Series,30,standard,,,, +ZGX165F,I-Series,30,standard,,,, +ZK159D,K-Series,26.5,slim,,,, +ZK168B,K-Series,27.5,slim,,,, +ZS165D,Sculpt,,,,,, +ZX153B,SLE 3.0,,standard,,,, +ZX160J,SLE 3.0,32,standard,,,, +ZX163E,SLE 3.0,,standard,,,, +ZX164G,SLE 3.0,32.5,standard,,,, +ZX165D,SLE 3.0,32.5,standard,,,, +ZX166K,SLE 3.0,32,standard,,,, +ZX170A,SLE 3.0,32.5,standard,,,, +ZX171C,SLE 3.0,33,standard,,,, +ZX172E,SLE 3.0,32.5,standard,,,, diff --git a/docs/pdr/PDR-011-declarative-configurator.md b/docs/pdr/PDR-011-declarative-configurator.md index b1c5408..3d78ae3 100644 --- a/docs/pdr/PDR-011-declarative-configurator.md +++ b/docs/pdr/PDR-011-declarative-configurator.md @@ -315,6 +315,14 @@ instead of global proposals. This mirrors the existing `proposed` component axes (standing feet, fingers) — surfaced as real options, flagged honestly as not-yet-backed-by-data. +**Closing the data gap.** Both open dependencies — `neck_joint_mm` per body and +`face_features` per head — need manufacturer input. `scripts/build_data_request.py` +generates ready-to-send fill-in templates (`docs/data-requests/`): one row per +real body/head, pre-filled with what we know, blank columns for what we're asking +for, sorted so the heads buyers actually see come first. Returned values load +straight into `db/neck_compatibility.json` and the `face_features` slot in +`db/heads.json` — no schema change; the configurator picks them up automatically. + --- ## 10. Strategic conclusion diff --git a/scripts/build_data_request.py b/scripts/build_data_request.py new file mode 100644 index 0000000..2bedc7b --- /dev/null +++ b/scripts/build_data_request.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +""" +Generate the manufacturer data-request templates — the two data points the +configurator needs to graduate from ESTIMATE/PROPOSED to authoritative +(PDR-011 §8-9): + + 1. neck_joint_mm per body -> replaces the circumference-derived neck_class + 2. face_features per head -> makes the Face & Finish axes head-aware + +Emits ready-to-send CSVs (one row per real body / head, pre-filled with what we +already know, blank columns for what we're asking the factory to supply) plus a +cover README. Re-runnable so the templates never drift from db/. + +Sources: db/neck_compatibility.json, db/heads.json +Usage: python scripts/build_data_request.py +""" +import csv +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +DB = ROOT / "db" +OUT = ROOT / "docs" / "data-requests" + + +def load(name): + return json.loads((DB / name).read_text(encoding="utf-8")) + + +def write_csv(path, header, rows): + with path.open("w", newline="", encoding="utf-8") as f: + w = csv.writer(f) + w.writerow(header) + w.writerows(rows) + + +def neck_rows(neck): + rows = [] + for code, b in sorted(neck["bodies"].items()): + rows.append([ + code, b.get("line") or "", b.get("neck_circ_cm") if b.get("neck_circ_cm") is not None else "", + b.get("neck_class") or "", + "", # neck_joint_diameter_mm (FILL) + "", # neck_joint_type (FILL: screw / bayonet / magnet / peg) + "", # interchange_group (FILL: factory's own connector group id, if any) + "", # notes + ]) + return rows + + +def head_rows(heads): + rows = [] + # priority 1 = has cast characters (the heads buyers actually see) so the + # factory can fill the ones that matter first. + def prio(h): + return (0 if h.get("characters") else 1, 0 if h.get("representative_image") else 1, h["head_code"]) + for h in sorted(heads.values(), key=prio): + rows.append([ + h["head_code"], h.get("line") or "", h.get("neck_class") or "", + "Y" if "Movable Jaw" in h.get("face_variants", []) else "N", + 1 if h.get("characters") else 2, # priority + "", # default_eye_color (FILL: Brown/Hazel/Blue/Green/Grey/...) + "", # eye_swappable (FILL: Y/N) + "", # default_makeup (FILL: Natural/Glam/...) + "", # eyebrows (FILL: Painted/Implanted) + "", # default_wig_color (FILL: Black/Brunette/Blonde/Red/...) + "", # wig_removable (FILL: Y/N) + "", # freckles_available (FILL: Y/N) + "", # notes + ]) + return rows + + +def main(): + OUT.mkdir(parents=True, exist_ok=True) + neck = load("neck_compatibility.json") + heads = load("heads.json")["heads"] + + neck_header = [ + "body_code", "line", "neck_circ_cm (known)", "neck_class (current estimate)", + "neck_joint_diameter_mm <-- FILL", + "neck_joint_type <-- FILL (screw/bayonet/magnet/peg)", + "interchange_group <-- FILL (factory connector group, optional)", + "notes", + ] + head_header = [ + "head_code", "line", "neck_class", "movable_jaw (known)", "priority (1=has cast)", + "default_eye_color <-- FILL", + "eye_swappable <-- FILL (Y/N)", + "default_makeup <-- FILL", + "eyebrows <-- FILL (Painted/Implanted)", + "default_wig_color <-- FILL", + "wig_removable <-- FILL (Y/N)", + "freckles_available <-- FILL (Y/N)", + "notes", + ] + + nrows = neck_rows(neck) + hrows = head_rows(heads) + write_csv(OUT / "neck-joint-request.csv", neck_header, nrows) + write_csv(OUT / "head-features-request.csv", head_header, hrows) + + readme = f"""# ZELEX — Manufacturer Data Request + +Two data points let the configurator (PDR-011) stop estimating and start using +real specs. Please fill the `<-- FILL` columns in the two CSVs in this folder and +return them. Pre-filled columns are what we already have — for context, do not +change them. + +## 1. Neck joint spec — `neck-joint-request.csv` ({len(nrows)} bodies) + +**Why:** heads are interchangeable when their neck CONNECTOR matches. We currently +estimate this from neck *circumference* (a soft-tissue proxy). The real connector +spec makes it exact. + +| Column | What we need | +|---|---| +| `neck_joint_diameter_mm` | Diameter (mm) of the head-mount connector at the neck. | +| `neck_joint_type` | Connector mechanism: screw / bayonet / magnet / peg / other. | +| `interchange_group` | If the factory already groups connectors (e.g. "A"/"B"), the group id for this body. Optional. | + +Two bodies with the same diameter + type are physically interchangeable, +regardless of product line. + +## 2. Per-head face features — `head-features-request.csv` ({len(hrows)} heads) + +**Why:** the configurator's *Face & Finish* options (eye colour, makeup, eyebrows, +wig, freckles) are currently GLOBAL proposals — we have no per-head defaults or +availability. Fill these to make each head show its true shipped finish and which +options it actually supports. Rows are sorted by **priority** (1 = heads with a +named character, i.e. the ones buyers see most) — fill those first if time is short. + +| Column | What we need | +|---|---| +| `default_eye_color` | Eye colour the head ships with. | +| `eye_swappable` | Can the acrylic eyes be changed? Y/N. | +| `default_makeup` | Factory makeup style (Natural / Glam / …). | +| `eyebrows` | Painted or implanted brow hair. | +| `default_wig_color` | Wig colour shipped. | +| `wig_removable` | Removable wig? Y/N. | +| `freckles_available` | Optional painted freckles offered? Y/N. | + +## Returning the data + +Return the filled CSVs as-is. We load `neck_joint_mm` into +`db/neck_compatibility.json` (per body) and the face fields into the +`face_features` slot in `db/heads.json` (per head) — no schema changes needed; the +configurator picks them up automatically. + +> Regenerate these templates after any catalog change: +> `python scripts/build_data_request.py` +""" + (OUT / "README.md").write_text(readme, encoding="utf-8") + + print(f"Wrote templates to {OUT.relative_to(ROOT)}/") + print(f" neck-joint-request.csv {len(nrows)} bodies") + print(f" head-features-request.csv {len(hrows)} heads " + f"({sum(1 for r in hrows if r[4]==1)} priority-1 with cast)") + print(f" README.md") + + +if __name__ == "__main__": + main()