Spaces:
Running
Running
fix: TL869IMAX neon row + image load speed + search precision
#7
by bep40 - opened
- dist/assets/index.js +0 -0
- dist/assets/worker-nnOdWzlm.js +0 -0
- src/avatar-interiors.js +5 -0
- src/image-loader.js +3 -3
- src/merge.js +24 -3
- src/product-search.js +5 -1
dist/assets/index.js
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
dist/assets/worker-nnOdWzlm.js
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/avatar-interiors.js
CHANGED
|
@@ -198,6 +198,11 @@ export function searchAvatarInteriors(text, items, limit = RECENT_LIMIT) {
|
|
| 198 |
if (!hits) continue;
|
| 199 |
// Covering the whole query beats matching one rare word hard.
|
| 200 |
score += hits * 10;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
scored.push({ ...item, score });
|
| 202 |
}
|
| 203 |
scored.sort((a, b) => b.score - a.score || (a.addedAt < b.addedAt ? 1 : -1));
|
|
|
|
| 198 |
if (!hits) continue;
|
| 199 |
// Covering the whole query beats matching one rare word hard.
|
| 200 |
score += hits * 10;
|
| 201 |
+
// A single accidental 25-point term (e.g. "tu" matching one caption) is
|
| 202 |
+
// noise: unrelated interiors must not appear at all. Keep only interiors
|
| 203 |
+
// with a real match — either the whole-phrase 900 identity hit, or enough
|
| 204 |
+
// term coverage to reach a meaningful 60 points.
|
| 205 |
+
if (score < 900 && score < 60) continue;
|
| 206 |
scored.push({ ...item, score });
|
| 207 |
}
|
| 208 |
scored.sort((a, b) => b.score - a.score || (a.addedAt < b.addedAt ? 1 : -1));
|
src/image-loader.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
/** Bounded download/decode queue, with at most two image insertions per frame. */
|
| 2 |
-
export function createImageLoader({ concurrency =
|
| 3 |
const queue = [], completed = [];
|
| 4 |
const active = new Set(), cache = new Map();
|
| 5 |
let disposed = false, frame = 0, scheduled = false;
|
|
@@ -14,7 +14,7 @@ export function createImageLoader({ concurrency = 6, cacheSize = 64 } = {}) {
|
|
| 14 |
|
| 15 |
function flush() {
|
| 16 |
frame = 0;
|
| 17 |
-
for (let i = 0; i <
|
| 18 |
const job = completed.shift();
|
| 19 |
if (job.cancelled) remember(job.image);
|
| 20 |
else job.receive(job.image);
|
|
@@ -36,7 +36,7 @@ export function createImageLoader({ concurrency = 6, cacheSize = 64 } = {}) {
|
|
| 36 |
active.add(job);
|
| 37 |
const photo = job.image = new Image();
|
| 38 |
photo.decoding = 'async';
|
| 39 |
-
photo.fetchPriority = job.priority <
|
| 40 |
photo.src = job.url;
|
| 41 |
// Decoding completes before this element is attached to the animated field.
|
| 42 |
photo.decode().then(() => {
|
|
|
|
| 1 |
/** Bounded download/decode queue, with at most two image insertions per frame. */
|
| 2 |
+
export function createImageLoader({ concurrency = 12, cacheSize = 128 } = {}) {
|
| 3 |
const queue = [], completed = [];
|
| 4 |
const active = new Set(), cache = new Map();
|
| 5 |
let disposed = false, frame = 0, scheduled = false;
|
|
|
|
| 14 |
|
| 15 |
function flush() {
|
| 16 |
frame = 0;
|
| 17 |
+
for (let i = 0; i < 4 && completed.length; i++) {
|
| 18 |
const job = completed.shift();
|
| 19 |
if (job.cancelled) remember(job.image);
|
| 20 |
else job.receive(job.image);
|
|
|
|
| 36 |
active.add(job);
|
| 37 |
const photo = job.image = new Image();
|
| 38 |
photo.decoding = 'async';
|
| 39 |
+
photo.fetchPriority = job.priority < 24 ? 'high' : 'low';
|
| 40 |
photo.src = job.url;
|
| 41 |
// Decoding completes before this element is attached to the animated field.
|
| 42 |
photo.decode().then(() => {
|
src/merge.js
CHANGED
|
@@ -249,7 +249,7 @@ export function mergeResults(interiors, products, options = {}) {
|
|
| 249 |
// are enlarged and ringed exactly like the newest interiors, so a product
|
| 250 |
// added on vai-avatar appears in this Space's "mới" front row with a neon
|
| 251 |
// frame, no query needed.
|
| 252 |
-
const RING_LIMIT = options.recentProductRing ??
|
| 253 |
const recents = (options.recentProducts ?? [])
|
| 254 |
.filter(item => item && typeof item === 'object' && item.sku && !seen.has(productKey(item.sku)))
|
| 255 |
.sort((a, b) => (String(a.addedAt || '') < String(b.addedAt || '') ? 1 : String(a.addedAt || '') > String(b.addedAt || '') ? -1 : 0));
|
|
@@ -288,8 +288,24 @@ export function mergeResults(interiors, products, options = {}) {
|
|
| 288 |
const showNewRow = String(options.query ?? "").trim().length === 0;
|
| 289 |
out.unshift(...(showNewRow ? fresh : []));
|
| 290 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
for (const image of interiors ?? []) {
|
| 292 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
seen.add(interiorKey(image.id));
|
| 294 |
out.push(image);
|
| 295 |
if (out.length >= interiorLimit) break;
|
|
@@ -322,7 +338,12 @@ export function mergeResults(interiors, products, options = {}) {
|
|
| 322 |
if (seen.has(key)) continue;
|
| 323 |
seen.add(key);
|
| 324 |
const isExact = exact.has(key);
|
| 325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
if (scoredHits.length >= productLimit) break;
|
| 327 |
}
|
| 328 |
// Products arrive already ranked by the worker; the score field is the
|
|
|
|
| 249 |
// are enlarged and ringed exactly like the newest interiors, so a product
|
| 250 |
// added on vai-avatar appears in this Space's "mới" front row with a neon
|
| 251 |
// frame, no query needed.
|
| 252 |
+
const RING_LIMIT = options.recentProductRing ?? 12;
|
| 253 |
const recents = (options.recentProducts ?? [])
|
| 254 |
.filter(item => item && typeof item === 'object' && item.sku && !seen.has(productKey(item.sku)))
|
| 255 |
.sort((a, b) => (String(a.addedAt || '') < String(b.addedAt || '') ? 1 : String(a.addedAt || '') > String(b.addedAt || '') ? -1 : 0));
|
|
|
|
| 288 |
const showNewRow = String(options.query ?? "").trim().length === 0;
|
| 289 |
out.unshift(...(showNewRow ? fresh : []));
|
| 290 |
|
| 291 |
+
// A relevance floor, so a search never paints tiles that only look like the
|
| 292 |
+
// query by chance. CLIP cosine answers "how close in meaning", and the lowest
|
| 293 |
+
// results in a top-60 pull are semantically unrelated to anything typed. The
|
| 294 |
+
// floor is RELATIVE to the query's own top match, so it adapts to however
|
| 295 |
+
// decisive the query is: "bếp đá" finds a clear winner and keeps nothing
|
| 296 |
+
// below half its strength, while a fuzzy query keeps a slightly broader tail.
|
| 297 |
+
// Tiles without a score (browse mode) are unaffected.
|
| 298 |
+
const cosineFloor = options.cosFloor ?? 0.18;
|
| 299 |
+
const cosineRel = options.cosRel ?? 0.45;
|
| 300 |
+
let topCosine = 0;
|
| 301 |
for (const image of interiors ?? []) {
|
| 302 |
+
if (Number.isFinite(image?.score) && image.score > topCosine) topCosine = image.score;
|
| 303 |
+
}
|
| 304 |
+
const effectiveFloor = Math.max(cosineFloor, topCosine * cosineRel);
|
| 305 |
+
for (const image of interiors ?? []) {
|
| 306 |
+
const score = image?.score;
|
| 307 |
+
const filteredScore = score === undefined || !Number.isFinite(score) || score >= effectiveFloor;
|
| 308 |
+
if (!image?.id || !filteredScore || seen.has(interiorKey(image.id))) continue;
|
| 309 |
seen.add(interiorKey(image.id));
|
| 310 |
out.push(image);
|
| 311 |
if (out.length >= interiorLimit) break;
|
|
|
|
| 338 |
if (seen.has(key)) continue;
|
| 339 |
seen.add(key);
|
| 340 |
const isExact = exact.has(key);
|
| 341 |
+
const productFloor = options.productFloor ?? 20;
|
| 342 |
+
const score = Number.isFinite(item?.score) ? item.score : 0;
|
| 343 |
+
// A product that only scraped a 3-point token overlap is noise, not an
|
| 344 |
+
// answer: it would paint an unrelated photograph next to the query typed.
|
| 345 |
+
if (score < productFloor) continue;
|
| 346 |
+
scoredHits.push({ product, src, key, isExact, score });
|
| 347 |
if (scoredHits.length >= productLimit) break;
|
| 348 |
}
|
| 349 |
// Products arrive already ranked by the worker; the score field is the
|
src/product-search.js
CHANGED
|
@@ -223,7 +223,11 @@ export function searchLikeAvatar(text, items, limit = 24) {
|
|
| 223 |
const scored = [];
|
| 224 |
for (const product of items.values()) {
|
| 225 |
const score = scoreProduct(product, String(text || '').trim(), terms);
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
}
|
| 228 |
scored.sort((a, b) => b.score - a.score || a.sku.localeCompare(b.sku));
|
| 229 |
return scored.slice(0, limit);
|
|
|
|
| 223 |
const scored = [];
|
| 224 |
for (const product of items.values()) {
|
| 225 |
const score = scoreProduct(product, String(text || '').trim(), terms);
|
| 226 |
+
// A score below 24 is a single weak token scrape, not an answer: it would
|
| 227 |
+
// paint an unrelated photograph next to the query typed. Kept in sync with
|
| 228 |
+
// the merge.js productFloor default (20) plus the two strongest weak tiers
|
| 229 |
+
// (16 title + 3+10 hit bonus) — anything under it is noise.
|
| 230 |
+
if (score >= 24) scored.push({ sku: product.sku, score, product });
|
| 231 |
}
|
| 232 |
scored.sort((a, b) => b.score - a.score || a.sku.localeCompare(b.sku));
|
| 233 |
return scored.slice(0, limit);
|