diff --git a/.gitignore b/.gitignore index 44f9f8f..0e08a79 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ node_modules/ pipeline/.genprops/ web/assets/models/ pipeline/.peds_orig/ +pipeline/.props_orig/ diff --git a/E-progress.md b/E-progress.md index 329b246..9da926a 100644 --- a/E-progress.md +++ b/E-progress.md @@ -4,6 +4,21 @@ Reviewed against `docs/LANES/LANE_E_ASSETS.md` + `docs/LANES/ROUND3_INSTRUCTIONS.md`. Nothing in other lanes' files was touched.* +## Round 5 (Fable) — hero-prop bake + fal fallbacks + depot growth + +- **E1 — hero-prop bake-to-lowpoly: DONE + committed.** New `pipeline/bake_lowpoly.py` (Blender + headless): voxel-remesh → decimate → **Cycles base-colour bake** onto fresh UVs (R3's lesson: + simplify-only floors on shell-soup; the re-bake is the point). All **6 heavy TRELLIS props ≤8k + tris** (drinks_fridge 77k→8k, milkshake_mixer 73k→8k, listening_booth 54k→8k, counter_till + 32k→8k, novelty_record 26k→8k, arcade_cabinet 25k→8k). **Fleet 288k→48k tris, 18M→5.2M.** Each + eyeballed (`docs/shots/laneE/props_baked.png` — fridge needed a coarser voxel re-bake, rest clean) + + verified loading in the vendored GLTFLoader. Re-published; `validate --depot` 0 err. Originals + gitignored in `pipeline/.props_orig/`. → Lane C re-measures the record interior (was ~273k). +- **E3 — fal.ai fallbacks (authorized, decision #4):** glass-case + bus-shelter (+ optional + magazine-rack) via fal Hunyuan v2, ≤2 attempts each, costs logged in AUDIT. *(in progress)* +- **E2 — depot growth for v2:** ~8–12 library props serving C's dig / D's roster / B's windows. + *(in progress)* + ## Round 4 (Fable) — ped decimation (gate-3 critical path) + asset fixes - **E1 — ped decimation: DONE + committed** (`518678d`, ref `lane-e/round4-peds`). The 19 ped rigs diff --git a/docs/LANES/LANE_E_NOTES.md b/docs/LANES/LANE_E_NOTES.md index a5a1542..c838f46 100644 --- a/docs/LANES/LANE_E_NOTES.md +++ b/docs/LANES/LANE_E_NOTES.md @@ -1,4 +1,33 @@ -# LANE E — cross-lane notes (round 4) +# LANE E — cross-lane notes + +## Round 5 + +### → Lane C: hero props BAKED to ≤8k tris — re-measure the record interior +The 6 heavy TRELLIS props are re-baked and **live on the depot** (same filenames, footprints +unchanged, so **no re-map needed** — they upgrade in place): + +| prop | tris before → after | +|---|---| +| drinks_fridge | 77k → 8.0k | +| milkshake_mixer | 73k → 8.0k | +| listening_booth | 54k → 8.0k | +| counter_till | 32k → 8.0k | +| novelty_record | 26k → 8.0k | +| arcade_cabinet | 25k → 8.0k | + +**Fleet 288k → 48k tris.** Method: voxel-remesh → decimate → Cycles base-colour bake (see +`pipeline/bake_lowpoly.py`, AUDIT R5). Verified loading in the vendored GLTFLoader; base colour +baked (no separate metallic/normal maps → also dodges the record_crate dangling-texture class). +→ **Please re-measure the record interior** (was ~273k tris in your R4 note) and record the new +number in LANE_C_GLB_VALIDATION.md — it should drop by ~most of the prop weight. + +### → Fable: E1 (bake) status +DONE + committed. 6 props ≤8k, published, `validate --depot` 0 err, qa gate green. Feeds C's +re-measure + your v1.1 tag. E3 (fal glass-case/bus-shelter) + E2 (depot growth) next. + +--- + +## Round 4 ## → Lane D (task D2): decimated peds ready to validate diff --git a/docs/shots/laneE/props_baked.png b/docs/shots/laneE/props_baked.png new file mode 100644 index 0000000..9a4fa73 Binary files /dev/null and b/docs/shots/laneE/props_baked.png differ diff --git a/pipeline/AUDIT.md b/pipeline/AUDIT.md index 844fb9e..029d73d 100644 --- a/pipeline/AUDIT.md +++ b/pipeline/AUDIT.md @@ -194,3 +194,28 @@ re-measures gate 3 (~24-rig near cap × ≤3k ≈ ≤67k fleet contribution). props). **Provenance: 100% synthetic/on-device** (FLUX.2-klein-4B + TRELLIS.2 on the M3 Ultra), no third-party source — cleanest licence zone. Replaces the R3 4 m till-less counter for the interior counter slot (Lane C maps `counter` → `counter_till`); the balcao 4 m `counter` stays for long-bar use. + +## Round 5 — E1: hero-prop bake-to-lowpoly (un-parked) + +The 6 heavy TRELLIS hero props were **25k–77k tris** (shell-soup — simplify-only floors there, +R3's lesson). Rebuilt topology + re-baked base colour via `pipeline/bake_lowpoly.py` (Blender +headless): **voxel-remesh → triangulate → decimate ≤8k → smart-UV → Cycles DIFFUSE(COLOR) bake +HP→LP → WebP export**. All **≤8k tris**, base colour baked onto fresh UVs, self-illuminated a +touch (matches normalize). Originals backed up (gitignored) at `pipeline/.props_orig/`; batch spec +`pipeline/batch_bake.json`; results `pipeline/_bake_results.json`. + +| prop | tris before → after | voxel | size on disk | note | +|---|---|---|---|---| +| drinks_fridge | 77047 → 8000 | 0.03 | 5.3M → 0.9M | hollow glass-door box — needed a coarser voxel (0.012 grabbed the internal shelves into wireframe) | +| milkshake_mixer | 73492 → 7991 | 0.005 | 4.7M → 0.9M | small part, fine voxel | +| listening_booth | 53502 → 7986 | 0.012 | 3.4M → 0.9M | panel + stool | +| counter_till | 32226 → 7999 | 0.012 | 1.9M → 0.9M | timber counter + till | +| novelty_record | 26486 → 8000 | 0.014 | 1.8M → 0.9M | thick disc + base | +| arcade_cabinet | 25142 → 8000 | 0.012 | 1.5M → 0.8M | boxy cabinet | + +Fleet: **288k → 48k tris**, **18M → 5.2M**. Every one **eyeballed** (contact sheet +`docs/shots/laneE/props_baked.png` — no melted labels/faces; the fridge re-bake was the only redo) +and **verified loading in the vendored `GLTFLoader`** (tris ≤8k, baked texture present). +Re-published to the depot (`validate --depot` 0 errors). **Provenance unchanged: 100% synthetic/ +on-device** (FLUX.2-klein-4B + TRELLIS.2 + Blender bake, no third-party source). → Lane C +re-measures the record interior (was ~273k tris). diff --git a/pipeline/_props_results.json b/pipeline/_props_results.json index 6494623..dd86378 100644 --- a/pipeline/_props_results.json +++ b/pipeline/_props_results.json @@ -5,16 +5,16 @@ "file": "procity_fit_listening_booth_01.glb", "out": "pipeline/_normalized/procity_fit_listening_booth_01.glb", "thumb": "web/assets/thumbs/procity_fit_listening_booth_01.png", - "tri_before": 146946, - "tri_after": 53578, + "tri_before": 53502, + "tri_after": 7986, "size_m": [ - 0.879, - 1.6, - 0.71 + 0.876, + 1.595, + 0.703 ], "footprint": [ 0.88, - 0.71 + 0.7 ] }, { @@ -22,16 +22,16 @@ "file": "procity_fit_drinks_fridge_01.glb", "out": "pipeline/_normalized/procity_fit_drinks_fridge_01.glb", "thumb": "web/assets/thumbs/procity_fit_drinks_fridge_01.png", - "tri_before": 191640, - "tri_after": 77412, + "tri_before": 77047, + "tri_after": 8000, "size_m": [ - 0.849, - 1.6, - 0.962 + 0.842, + 1.591, + 0.932 ], "footprint": [ - 0.85, - 0.96 + 0.84, + 0.93 ] }, { @@ -39,12 +39,12 @@ "file": "procity_fit_arcade_cabinet_01.glb", "out": "pipeline/_normalized/procity_fit_arcade_cabinet_01.glb", "thumb": "web/assets/thumbs/procity_fit_arcade_cabinet_01.png", - "tri_before": 196336, - "tri_after": 25143, + "tri_before": 25142, + "tri_after": 8000, "size_m": [ - 0.75, - 1.7, - 0.853 + 0.749, + 1.692, + 0.85 ], "footprint": [ 0.75, @@ -56,12 +56,12 @@ "file": "procity_fit_milkshake_mixer_01.glb", "out": "pipeline/_normalized/procity_fit_milkshake_mixer_01.glb", "thumb": "web/assets/thumbs/procity_fit_milkshake_mixer_01.png", - "tri_before": 185106, - "tri_after": 73498, + "tri_before": 73492, + "tri_after": 7991, "size_m": [ - 0.381, - 0.5, - 0.271 + 0.378, + 0.498, + 0.268 ], "footprint": [ 0.38, @@ -73,15 +73,15 @@ "file": "procity_street_novelty_record_01.glb", "out": "pipeline/_normalized/procity_street_novelty_record_01.glb", "thumb": "web/assets/thumbs/procity_street_novelty_record_01.png", - "tri_before": 193253, - "tri_after": 26506, + "tri_before": 26486, + "tri_after": 8000, "size_m": [ - 1.909, - 2.0, - 0.934 + 1.896, + 1.992, + 0.933 ], "footprint": [ - 1.91, + 1.9, 0.93 ] }, @@ -90,16 +90,16 @@ "file": "procity_fit_counter_till_01.glb", "out": "/tmp/counter_norm/procity_fit_counter_till_01.glb", "thumb": "web/assets/thumbs/procity_fit_counter_till_01.png", - "tri_before": 195285, - "tri_after": 32234, + "tri_before": 32226, + "tri_after": 7999, "size_m": [ - 1.609, - 1.15, - 0.642 + 1.608, + 1.146, + 0.606 ], "footprint": [ 1.61, - 0.64 + 0.61 ] } ], diff --git a/pipeline/bake_lowpoly.py b/pipeline/bake_lowpoly.py new file mode 100644 index 0000000..a1e4c1e --- /dev/null +++ b/pipeline/bake_lowpoly.py @@ -0,0 +1,200 @@ +"""PROCITY hero-prop bake-to-lowpoly — turn a heavy TRELLIS shell-soup GLB into a game-ready +≤8k-tri prop with its base colour BAKED onto fresh UVs. + +Runs headless on this box's Blender: + BL=/Applications/Blender.app/Contents/MacOS/Blender + "$BL" --background --python bake_lowpoly.py -- BATCH.json OUT_DIR [THUMB_DIR] + +Why bake (R3's lesson): raw TRELLIS meshes are dense multi-shell scans; plain decimate / +meshopt-simplify floors at 25–77k because they can't merge across the shells. So we throw the +topology away and rebuild it: + 1. import the normalized HP (already metres / +Y up / base-origin / textured) → bake SOURCE + 2. voxel-remesh a copy → one watertight manifold surface (no more shell-soup) + 3. decimate the remesh to the tri budget (≤8k) → LP + 4. smart-UV unwrap LP + 5. Cycles bake DIFFUSE(COLOR) HP → LP's new UVs → a single baked base-colour texture + 6. export LP GLB (WebP, embedded, self-illuminated a touch like normalize.py) + 256 thumbnail + +BATCH.json: [ {"src":"pipeline/_normalized/procity_fit_drinks_fridge_01.glb", + "id":"drinks_fridge", "category":"fit", + "voxel":0.012, "tris":8000, "img":1024} , ... ] +""" +import bpy, sys, os, json, math +from mathutils import Vector + +ARGV = sys.argv[sys.argv.index("--") + 1:] +BATCH, OUT_DIR = ARGV[0], (ARGV[1] if len(ARGV) > 1 else "/tmp/procity_bake") +THUMB_DIR = ARGV[2] if len(ARGV) > 2 else os.path.join(OUT_DIR, "thumbs") +os.makedirs(OUT_DIR, exist_ok=True); os.makedirs(THUMB_DIR, exist_ok=True) + + +def wipe(): + for o in list(bpy.data.objects): + bpy.data.objects.remove(o, do_unlink=True) + for coll in (bpy.data.meshes, bpy.data.materials, bpy.data.images): + for d in list(coll): + if d.users == 0: + coll.remove(d) + + +def import_join(src): + bpy.ops.import_scene.gltf(filepath=src) + meshes = [o for o in bpy.data.objects if o.type == 'MESH'] + bpy.ops.object.select_all(action='DESELECT') + for m in meshes: + m.select_set(True) + bpy.context.view_layer.objects.active = meshes[0] + if len(meshes) > 1: + bpy.ops.object.join() + obj = bpy.context.view_layer.objects.active + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + return obj + + +def bounds(obj): + bb = [obj.matrix_world @ Vector(c) for c in obj.bound_box] + mn = Vector((min(v.x for v in bb), min(v.y for v in bb), min(v.z for v in bb))) + mx = Vector((max(v.x for v in bb), max(v.y for v in bb), max(v.z for v in bb))) + return mn, mx + + +def make_lowpoly(hp, voxel, tris): + """Duplicate HP → voxel remesh → decimate to budget → smart UV. Returns the LP object.""" + bpy.ops.object.select_all(action='DESELECT') + hp.select_set(True); bpy.context.view_layer.objects.active = hp + bpy.ops.object.duplicate() + lp = bpy.context.view_layer.objects.active + lp.name = "LP" + # strip HP's materials off the LP — it gets one fresh baked material + lp.data.materials.clear() + rm = lp.modifiers.new("remesh", 'REMESH') + rm.mode = 'VOXEL'; rm.voxel_size = voxel; rm.adaptivity = 0.0 + bpy.ops.object.modifier_apply(modifier=rm.name) + # triangulate so len(polygons) == triangle count → the decimate ratio targets triangles honestly + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.quads_convert_to_tris() + bpy.ops.object.mode_set(mode='OBJECT') + n = len(lp.data.polygons) + if n > tris: + dm = lp.modifiers.new("dec", 'DECIMATE') + dm.ratio = max(0.005, tris / n) + bpy.ops.object.modifier_apply(modifier=dm.name) + # UVs + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.uv.smart_project(island_margin=0.02, angle_limit=math.radians(66)) + bpy.ops.object.mode_set(mode='OBJECT') + return lp + + +def bake_color(hp, lp, img_size): + img = bpy.data.images.new("baked", img_size, img_size) + mat = bpy.data.materials.new("baked_mat"); mat.use_nodes = True + nt = mat.node_tree + bsdf = nt.nodes.get("Principled BSDF") + tex = nt.nodes.new("ShaderNodeTexImage"); tex.image = img + nt.links.new(tex.outputs["Color"], bsdf.inputs["Base Color"]) + lp.data.materials.clear(); lp.data.materials.append(mat) + nt.nodes.active = tex # the active image node = bake target + + scn = bpy.context.scene + scn.render.engine = 'CYCLES' + try: + scn.cycles.device = 'GPU' + except Exception: + pass + scn.cycles.samples = 4 + scn.render.bake.use_selected_to_active = True + scn.render.bake.cage_extrusion = 0.06 + scn.render.bake.max_ray_distance = 0.12 + scn.render.bake.margin = max(4, img_size // 128) + + bpy.ops.object.select_all(action='DESELECT') + hp.select_set(True); lp.select_set(True) + bpy.context.view_layer.objects.active = lp # active = bake TARGET + bpy.ops.object.bake(type='DIFFUSE', pass_filter={'COLOR'}, use_clear=True) + + # self-illuminate a touch so it reads in any viewer (matches normalize.py bright) + ec = bsdf.inputs.get("Emission Color") or bsdf.inputs.get("Emission") + if ec is not None: + nt.links.new(tex.outputs["Color"], ec) + es = bsdf.inputs.get("Emission Strength") + if es is not None: + es.default_value = 0.28 + return img + + +def base_origin(obj): + mn, mx = bounds(obj) + obj.location -= Vector(((mn.x + mx.x) / 2, (mn.y + mx.y) / 2, mn.z)) + bpy.ops.object.transform_apply(location=True) + + +def render_thumb(lp, png_name): + scn = bpy.context.scene + mn, mx = bounds(lp); ctr = (mn + mx) / 2 + size = max((mx - mn).x, (mx - mn).y, (mx - mn).z) or 1.0 + for o in list(bpy.data.objects): + if o.type in ('CAMERA', 'LIGHT'): + bpy.data.objects.remove(o, do_unlink=True) + w = bpy.data.worlds.get("tw") or bpy.data.worlds.new("tw") + scn.world = w; w.use_nodes = True + w.node_tree.nodes["Background"].inputs[1].default_value = 1.05 + bpy.ops.object.light_add(type='AREA', location=(ctr.x + size, ctr.y - size, mx.z + size)) + bpy.context.object.data.energy = 800 * size; bpy.context.object.data.size = 6 + bpy.ops.object.camera_add(); cam = bpy.context.object; scn.camera = cam + r = size * 1.9 + cam.location = Vector((ctr.x + r * 0.8, ctr.y - r, ctr.z + size * 0.6)) + cam.rotation_euler = (ctr - cam.location).to_track_quat('-Z', 'Y').to_euler() + scn.render.engine = 'BLENDER_WORKBENCH' + scn.display.shading.light = 'STUDIO'; scn.display.shading.color_type = 'TEXTURE' + scn.display.shading.show_cavity = True + scn.view_settings.view_transform = 'Standard' + scn.render.film_transparent = True + scn.render.resolution_x = scn.render.resolution_y = 256 + p = os.path.join(THUMB_DIR, png_name); scn.render.filepath = p + bpy.ops.render.render(write_still=True) + return p + + +def bake_one(spec): + wipe() + src = spec["src"] if os.path.isabs(spec["src"]) else os.path.join(os.getcwd(), spec["src"]) + hp = import_join(src) + tri_before = len(hp.data.polygons) + lp = make_lowpoly(hp, spec.get("voxel", 0.02), spec.get("tris", 8000)) + bake_color(hp, lp, spec.get("img", 1024)) + bpy.data.objects.remove(hp, do_unlink=True) # HP done; export LP only + base_origin(lp) + lp.select_set(True); bpy.context.view_layer.objects.active = lp + name = f"procity_{spec['category']}_{spec['id']}_01.glb" + out = os.path.join(OUT_DIR, name) + bpy.ops.export_scene.gltf(filepath=out, export_format='GLB', export_yup=True, + use_selection=True, export_image_format='WEBP', export_image_quality=88) + thumb = render_thumb(lp, name.replace(".glb", ".png")) + mn, mx = bounds(lp) + return {"file": name, "tri_before": tri_before, "tri_after": len(lp.data.polygons), + "size_m": [round(mx.x - mn.x, 3), round(mx.z - mn.z, 3), round(mx.y - mn.y, 3)], + "footprint": [round(mx.x - mn.x, 2), round(mx.y - mn.y, 2)], "thumb": thumb, "out": out} + + +def main(): + specs = json.load(open(BATCH)) + results, errors = [], [] + for spec in specs: + try: + r = bake_one(spec) + results.append(r) + print(f"OK {r['file']:36} {r['tri_before']:>6}→{r['tri_after']:>5} tris " + f"footprint {r['footprint']} h {r['size_m'][1]}") + except Exception as e: + import traceback; traceback.print_exc() + errors.append({"id": spec.get("id"), "error": str(e)}) + print(f"ERR {spec.get('id')}: {e}") + json.dump({"results": results, "errors": errors}, + open(os.path.join(OUT_DIR, "_bake_results.json"), "w"), indent=2) + print(f"\nBAKE_DONE ok={len(results)} err={len(errors)} → {OUT_DIR}") + + +main() diff --git a/pipeline/batch_bake.json b/pipeline/batch_bake.json new file mode 100644 index 0000000..7cbe081 --- /dev/null +++ b/pipeline/batch_bake.json @@ -0,0 +1,50 @@ +[ + { + "src": "pipeline/_normalized/procity_fit_drinks_fridge_01.glb", + "id": "drinks_fridge", + "category": "fit", + "voxel": 0.03, + "tris": 8000, + "img": 1024 + }, + { + "src": "pipeline/_normalized/procity_fit_milkshake_mixer_01.glb", + "id": "milkshake_mixer", + "category": "fit", + "voxel": 0.005, + "tris": 8000, + "img": 1024 + }, + { + "src": "pipeline/_normalized/procity_fit_arcade_cabinet_01.glb", + "id": "arcade_cabinet", + "category": "fit", + "voxel": 0.012, + "tris": 8000, + "img": 1024 + }, + { + "src": "pipeline/_normalized/procity_fit_listening_booth_01.glb", + "id": "listening_booth", + "category": "fit", + "voxel": 0.012, + "tris": 8000, + "img": 1024 + }, + { + "src": "pipeline/_normalized/procity_street_novelty_record_01.glb", + "id": "novelty_record", + "category": "street", + "voxel": 0.014, + "tris": 8000, + "img": 1024 + }, + { + "src": "pipeline/_normalized/procity_fit_counter_till_01.glb", + "id": "counter_till", + "category": "fit", + "voxel": 0.012, + "tris": 8000, + "img": 1024 + } +] \ No newline at end of file diff --git a/web/assets/manifest.json b/web/assets/manifest.json index 0c3e6dd..6567017 100644 --- a/web/assets/manifest.json +++ b/web/assets/manifest.json @@ -652,18 +652,18 @@ "file": "procity_fit_listening_booth_01.glb", "footprint": [ 0.88, - 0.71 + 0.7 ], - "height": 1.6, + "height": 1.595, "thumb": "thumbs/procity_fit_listening_booth_01.png" }, "drinks_fridge": { "file": "procity_fit_drinks_fridge_01.glb", "footprint": [ - 0.85, - 0.96 + 0.84, + 0.93 ], - "height": 1.6, + "height": 1.591, "thumb": "thumbs/procity_fit_drinks_fridge_01.png" }, "arcade_cabinet": { @@ -672,7 +672,7 @@ 0.75, 0.85 ], - "height": 1.7, + "height": 1.692, "thumb": "thumbs/procity_fit_arcade_cabinet_01.png" }, "milkshake_mixer": { @@ -681,16 +681,16 @@ 0.38, 0.27 ], - "height": 0.5, + "height": 0.498, "thumb": "thumbs/procity_fit_milkshake_mixer_01.png" }, "counter_till": { "file": "procity_fit_counter_till_01.glb", "footprint": [ 1.61, - 0.64 + 0.61 ], - "height": 1.15, + "height": 1.146, "thumb": "thumbs/procity_fit_counter_till_01.png" }, "cash_register": { @@ -769,10 +769,10 @@ "novelty_record": { "file": "procity_street_novelty_record_01.glb", "footprint": [ - 1.91, + 1.9, 0.93 ], - "height": 2.0, + "height": 1.992, "thumb": "thumbs/procity_street_novelty_record_01.png" } }, diff --git a/web/assets/thumbs/procity_fit_arcade_cabinet_01.png b/web/assets/thumbs/procity_fit_arcade_cabinet_01.png index 9fa849f..ffa6fc2 100644 Binary files a/web/assets/thumbs/procity_fit_arcade_cabinet_01.png and b/web/assets/thumbs/procity_fit_arcade_cabinet_01.png differ diff --git a/web/assets/thumbs/procity_fit_counter_till_01.png b/web/assets/thumbs/procity_fit_counter_till_01.png index 696abcc..10a9917 100644 Binary files a/web/assets/thumbs/procity_fit_counter_till_01.png and b/web/assets/thumbs/procity_fit_counter_till_01.png differ diff --git a/web/assets/thumbs/procity_fit_drinks_fridge_01.png b/web/assets/thumbs/procity_fit_drinks_fridge_01.png index 89442fe..696fc17 100644 Binary files a/web/assets/thumbs/procity_fit_drinks_fridge_01.png and b/web/assets/thumbs/procity_fit_drinks_fridge_01.png differ diff --git a/web/assets/thumbs/procity_fit_listening_booth_01.png b/web/assets/thumbs/procity_fit_listening_booth_01.png index 6bd4bab..35558c4 100644 Binary files a/web/assets/thumbs/procity_fit_listening_booth_01.png and b/web/assets/thumbs/procity_fit_listening_booth_01.png differ diff --git a/web/assets/thumbs/procity_fit_milkshake_mixer_01.png b/web/assets/thumbs/procity_fit_milkshake_mixer_01.png index a2638ad..ff98429 100644 Binary files a/web/assets/thumbs/procity_fit_milkshake_mixer_01.png and b/web/assets/thumbs/procity_fit_milkshake_mixer_01.png differ diff --git a/web/assets/thumbs/procity_street_novelty_record_01.png b/web/assets/thumbs/procity_street_novelty_record_01.png index 3451835..7eb3ac4 100644 Binary files a/web/assets/thumbs/procity_street_novelty_record_01.png and b/web/assets/thumbs/procity_street_novelty_record_01.png differ