Files
bitvid/js/feedEngine/kidsScoring.js
T
google-labs-jules[bot]andPR0M3TH3AN 96dea42692 Optimize object allocation in kids scoring loop
- Replaced array allocation and loop with unrolled scalar comparisons in `kidsScorerStage`.
- Added unit tests and benchmark.
- Measured ~12% performance improvement (282ms -> 249ms for 200k items).

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-08 03:41:50 +00:00

430 lines
12 KiB
JavaScript

// js/feedEngine/kidsScoring.js
import { normalizeHashtag } from "../utils/hashtagNormalization.js";
import { isPlainObject, toSet } from "./utils.js";
const DEFAULT_WEIGHTS = Object.freeze({
w_age: 0.35,
w_edu: 0.25,
w_author: 0.15,
w_pop: 0.1,
w_fresh: 0.1,
w_risk: 0.6,
});
const DEFAULT_FRESHNESS_HALF_LIFE_DAYS = 14;
const AGE_GROUP_DEFAULTS = Object.freeze({
toddler: {
maxDurationSeconds: 5 * 60,
preferredTags: ["toddler", "baby", "nursery", "colors", "shapes", "lullaby"],
educationalTags: ["abc", "numbers", "counting", "learning", "alphabet"],
},
preschool: {
maxDurationSeconds: 10 * 60,
preferredTags: ["preschool", "kindergarten", "storytime", "letters", "phonics"],
educationalTags: ["counting", "alphabet", "reading", "learning", "math"],
},
early: {
maxDurationSeconds: 15 * 60,
preferredTags: ["early", "kids", "reading", "science", "animals", "art"],
educationalTags: ["science", "math", "reading", "history", "geography"],
},
older: {
maxDurationSeconds: 20 * 60,
preferredTags: ["tween", "teens", "tutorial", "stem", "coding", "music"],
educationalTags: ["stem", "coding", "history", "geography", "tutorial"],
},
});
function clamp01(value) {
if (!Number.isFinite(value)) {
return 0;
}
if (value <= 0) {
return 0;
}
if (value >= 1) {
return 1;
}
return value;
}
function normalizeTagSetFromVideo(video) {
const normalized = new Set();
if (!video || typeof video !== "object") {
return normalized;
}
const addTag = (rawTag) => {
if (typeof rawTag !== "string") {
return;
}
const tag = normalizeHashtag(rawTag);
if (tag) {
normalized.add(tag);
}
};
if (Array.isArray(video.tags)) {
for (const tag of video.tags) {
if (Array.isArray(tag) && tag[0] === "t") {
addTag(tag[1]);
}
}
}
if (Array.isArray(video.nip71?.hashtags)) {
for (const tag of video.nip71.hashtags) {
addTag(tag);
}
}
return normalized;
}
function normalizeTagSet(values) {
const normalized = new Set();
if (!values) {
return normalized;
}
for (const value of toSet(values)) {
if (typeof value !== "string") {
continue;
}
const tag = normalizeHashtag(value);
if (tag) {
normalized.add(tag);
}
}
return normalized;
}
function resolveAgeProfile(ageGroup) {
const key = typeof ageGroup === "string" ? ageGroup.trim().toLowerCase() : "";
return AGE_GROUP_DEFAULTS[key] || AGE_GROUP_DEFAULTS.preschool;
}
function resolveDuration(video) {
const candidates = [video?.duration, video?.metadata?.duration];
for (const candidate of candidates) {
const value = Number(candidate);
if (Number.isFinite(value) && value > 0) {
return value;
}
}
return null;
}
function resolveWeights(options, context) {
const weights = { ...DEFAULT_WEIGHTS };
const overrides = [options?.weights, context?.runtime?.kidsWeights];
for (const candidate of overrides) {
if (!isPlainObject(candidate)) {
continue;
}
for (const key of Object.keys(weights)) {
if (Number.isFinite(candidate[key])) {
weights[key] = candidate[key];
}
}
}
return weights;
}
function resolveFreshnessHalfLife(options, context) {
const candidates = [options?.freshnessHalfLifeDays, context?.runtime?.kidsFreshnessHalfLifeDays];
for (const candidate of candidates) {
const value = Number(candidate);
if (Number.isFinite(value) && value > 0) {
return value;
}
}
return DEFAULT_FRESHNESS_HALF_LIFE_DAYS;
}
function resolvePopularityMax(options, context) {
const candidates = [
options?.popularityMax,
context?.runtime?.kidsPopularityMax,
context?.runtime?.explorePopularityMax,
];
for (const candidate of candidates) {
const value = Number(candidate);
if (Number.isFinite(value) && value > 0) {
return value;
}
}
return 0;
}
function resolveNow(context) {
const candidate = Number(context?.runtime?.now);
if (Number.isFinite(candidate) && candidate > 0) {
return Math.floor(candidate);
}
return Math.floor(Date.now() / 1000);
}
function resolveKidsPopularityValue(video, item) {
const candidates = [
video?.kidsViews,
video?.stats?.kidsViews,
video?.metrics?.kidsViews,
item?.metadata?.kidsViews,
];
for (const candidate of candidates) {
const value = Number(candidate);
if (Number.isFinite(value) && value >= 0) {
return value;
}
}
return null;
}
function resolveGlobalPopularityValue(video, item) {
const candidates = [
video?.viewCount,
video?.views,
video?.stats?.views,
video?.stats?.viewCount,
video?.metrics?.views,
item?.metadata?.viewCount,
];
for (const candidate of candidates) {
const value = Number(candidate);
if (Number.isFinite(value) && value >= 0) {
return value;
}
}
return 0;
}
function resolveSafetyScore(item) {
const moderation = item?.metadata?.moderation || item?.video?.moderation || {};
if (moderation.hidden === true) {
return 0;
}
const trustedMuted = moderation.trustedMuted === true;
const trustedCount = Number(moderation.trustedCount ?? moderation.trustedReportCount);
let penalty = 0;
if (Number.isFinite(trustedCount) && trustedCount > 0) {
penalty = Math.min(1, trustedCount / 3) * 0.7;
}
if (trustedMuted) {
penalty = Math.max(penalty, 0.9);
}
return clamp01(1 - penalty);
}
function computeTagMatchScore(tags, preferredTags) {
if (!tags || tags.size === 0 || preferredTags.size === 0) {
return 0.4;
}
let matches = 0;
for (const tag of preferredTags) {
if (tags.has(tag)) {
matches += 1;
}
}
const divisor = Math.min(3, preferredTags.size);
return clamp01(matches / divisor);
}
export function createKidsScorerStage({
stageName = "kids-scorer",
ageGroup = "preschool",
educationalTags,
weights,
...options
} = {}) {
const resolveAgeGroupFromContext = (context) => {
const candidates = [context?.runtime?.ageGroup, context?.config?.ageGroup, ageGroup];
for (const candidate of candidates) {
if (typeof candidate !== "string") {
continue;
}
const trimmed = candidate.trim();
if (trimmed) {
return trimmed;
}
}
return ageGroup;
};
const resolveEducationalTagsFromContext = (context) => {
const candidates = [
context?.runtime?.educationalTags,
context?.config?.educationalTags,
educationalTags,
];
for (const candidate of candidates) {
if (candidate !== undefined && candidate !== null) {
return candidate;
}
}
return null;
};
return async function kidsScorerStage(items = [], context = {}) {
if (!Array.isArray(items) || items.length === 0) {
return items;
}
const resolvedAgeGroup = resolveAgeGroupFromContext(context);
const ageProfile = resolveAgeProfile(resolvedAgeGroup);
const preferredTags = normalizeTagSet(ageProfile.preferredTags);
const defaultEducationalTags = normalizeTagSet(ageProfile.educationalTags);
const resolvedEducationalTags = resolveEducationalTagsFromContext(context);
const educationalTagSet =
resolvedEducationalTags !== null
? normalizeTagSet(resolvedEducationalTags)
: defaultEducationalTags;
const resolvedWeights = resolveWeights({ weights, ...options }, context);
const freshnessHalfLifeDays = resolveFreshnessHalfLife(options, context);
const popularityMax = resolvePopularityMax(options, context);
const now = resolveNow(context);
const trustedAuthors = toSet(context?.runtime?.trustedAuthors);
const trustedAuthorSet = new Set();
for (const author of trustedAuthors) {
if (typeof author === "string") {
trustedAuthorSet.add(author.toLowerCase());
}
}
for (const item of items) {
if (!item || typeof item !== "object") {
continue;
}
const video = item.video;
if (!video || typeof video !== "object") {
continue;
}
const tags = normalizeTagSetFromVideo(video);
const duration = resolveDuration(video);
let durationScore = 0.5;
if (Number.isFinite(duration) && duration > 0) {
const ratio = duration / ageProfile.maxDurationSeconds;
durationScore = ratio <= 1 ? 1 : clamp01(1 / ratio);
}
const tagScore = computeTagMatchScore(tags, preferredTags);
const ageAppropriateness = clamp01(0.6 * durationScore + 0.4 * tagScore);
let educationalBoost = 0;
if (educationalTagSet.size > 0 && tags.size > 0) {
let matches = 0;
for (const tag of educationalTagSet) {
if (tags.has(tag)) {
matches += 1;
}
}
const divisor = Math.min(3, educationalTagSet.size);
educationalBoost = clamp01(matches / divisor);
}
let authorTrust = 0;
if (trustedAuthorSet.size > 0 && typeof video.pubkey === "string") {
authorTrust = trustedAuthorSet.has(video.pubkey.toLowerCase()) ? 1 : 0;
}
let popularityWithinKids = 0;
const kidsViews = resolveKidsPopularityValue(video, item);
const popularityValue = kidsViews === null ? resolveGlobalPopularityValue(video, item) : kidsViews;
if (popularityMax > 0) {
const normalized = Math.log1p(popularityValue) / Math.log1p(popularityMax);
popularityWithinKids = clamp01(normalized);
} else if (popularityValue > 0) {
popularityWithinKids = clamp01(Math.log1p(popularityValue) / Math.log1p(popularityValue + 100));
}
let freshness = 0;
const createdAt = Number(video?.rootCreatedAt ?? video?.nip71Source?.created_at ?? video?.created_at);
if (Number.isFinite(createdAt) && createdAt > 0 && freshnessHalfLifeDays > 0) {
const ageSeconds = Math.max(0, now - Math.floor(createdAt));
const ageDays = ageSeconds / 86400;
const decay = Math.exp(-ageDays / freshnessHalfLifeDays);
freshness = clamp01(decay);
}
const safetyScore = resolveSafetyScore(item);
const baseScore =
resolvedWeights.w_age * ageAppropriateness +
resolvedWeights.w_edu * educationalBoost +
resolvedWeights.w_author * authorTrust +
resolvedWeights.w_pop * popularityWithinKids +
resolvedWeights.w_fresh * freshness;
const riskMultiplier = clamp01(1 - resolvedWeights.w_risk * (1 - safetyScore));
const kidsScore = clamp01(baseScore * riskMultiplier);
if (!isPlainObject(item.metadata)) {
item.metadata = {};
}
item.metadata.kidsScore = kidsScore;
item.metadata.kidsScoreComponents = {
ageAppropriateness,
educationalBoost,
authorTrust,
popularityWithinKids,
freshness,
safetyScore,
};
const videoId = typeof video.id === "string" ? video.id : null;
const pubkey = typeof video.pubkey === "string" ? video.pubkey : null;
let maxKey = "age-appropriateness";
let maxValue = ageAppropriateness;
if (educationalBoost > maxValue) {
maxKey = "educational-boost";
maxValue = educationalBoost;
}
if (authorTrust > maxValue) {
maxKey = "author-trust";
maxValue = authorTrust;
}
if (popularityWithinKids > maxValue) {
maxKey = "popularity";
maxValue = popularityWithinKids;
}
if (freshness > maxValue) {
maxKey = "freshness";
maxValue = freshness;
}
if (maxValue > 0) {
context?.addWhy?.({
stage: stageName,
type: "score",
reason: maxKey,
value: maxValue,
score: kidsScore,
videoId,
pubkey,
});
}
}
return items;
};
}