Merge pull request #1564 from PR0M3TH3AN/codex/add-registerkidsfeed-method-implementation

Add kids feed registration, kids scorer overrides, and kids-score sorter
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-23 02:00:08 -05:00
commit b0edefcf50
5 files changed
+228 -2

No files matched your search

+144
View File
@@ -40,6 +40,9 @@ import {
createDedupeByRootStage,
createExploreDiversitySorter,
createExploreScorerStage,
createKidsAudienceFilterStage,
createKidsScorerStage,
createKidsScoreSorter,
createModerationStage,
createResolvePostedAtStage,
createTagPreferenceFilterStage,
@@ -5662,6 +5665,147 @@ class Application {
}
}
/**
* Register the "kids" feed pipeline.
*/
registerKidsFeed() {
if (!this.feedEngine || typeof this.feedEngine.registerFeed !== "function") {
return null;
}
const existingDefinition =
typeof this.feedEngine.getFeedDefinition === "function"
? this.feedEngine.getFeedDefinition("kids")
: null;
if (existingDefinition) {
return existingDefinition;
}
try {
const app = this;
const resolveThresholdFromApp = (key, kidsDefault) => ({
runtimeValue,
defaultValue,
}) => {
if (
Number.isFinite(runtimeValue) ||
runtimeValue === Number.POSITIVE_INFINITY
) {
return runtimeValue;
}
if (app && typeof app.getActiveModerationThresholds === "function") {
const active = app.getActiveModerationThresholds();
const candidate = active && typeof active === "object" ? active[key] : undefined;
if (
Number.isFinite(candidate) ||
candidate === Number.POSITIVE_INFINITY
) {
return candidate;
}
}
if (
Number.isFinite(kidsDefault) ||
kidsDefault === Number.POSITIVE_INFINITY
) {
return kidsDefault;
}
return defaultValue;
};
const kidsDefaults = {
blurThreshold: 1,
trustedReportHideThreshold: 1,
trustedMuteHideThreshold: 1,
};
const disallowedWarnings = [
"nudity",
"sexual",
"graphic-violence",
"self-harm",
"drugs",
];
const moderationStages = ["nudity", "violence", "self-harm"].map(
(reportType) =>
createModerationStage({
stageName: `kids-moderation-${reportType}`,
reportType,
getService: () => this.nostrService.getModerationService(),
autoplayThreshold: resolveThresholdFromApp("autoplayBlockThreshold"),
blurThreshold: resolveThresholdFromApp(
"blurThreshold",
kidsDefaults.blurThreshold,
),
trustedMuteHideThreshold: resolveThresholdFromApp(
"trustedMuteHideThreshold",
kidsDefaults.trustedMuteHideThreshold,
),
trustedReportHideThreshold: resolveThresholdFromApp(
"trustedSpamHideThreshold",
kidsDefaults.trustedReportHideThreshold,
),
}),
);
return this.feedEngine.registerFeed("kids", {
source: createActiveNostrSource({ service: this.nostrService }),
stages: [
createBlacklistFilterStage({
shouldIncludeVideo: (video, options) =>
this.nostrService.shouldIncludeVideo(video, options),
}),
createKidsAudienceFilterStage({
disallowedWarnings,
}),
...moderationStages,
createResolvePostedAtStage(),
createDedupeByRootStage({
dedupe: (videos) => this.dedupeVideosByRoot(videos),
}),
createKidsScorerStage(),
],
sorter: createKidsScoreSorter(),
defaultConfig: {
ageGroup: "preschool",
educationalTags: [],
disallowedWarnings,
},
configSchema: {
ageGroup: {
type: "enum",
values: ["toddler", "preschool", "early", "older"],
description: "Target age group used for kids scoring defaults.",
default: "preschool",
},
educationalTags: {
type: "string[]",
description: "Optional educational tag overrides for kids scoring.",
default: [],
},
disallowedWarnings: {
type: "string[]",
description:
"Content warnings that should exclude videos from the kids feed.",
default: disallowedWarnings,
},
},
hooks: {
timestamps: {
getKnownVideoPostedAt: (video) => this.getKnownVideoPostedAt(video),
resolveVideoPostedAt: (video) => this.resolveVideoPostedAt(video),
},
},
});
} catch (error) {
devLogger.warn("[Application] Failed to register kids feed:", error);
return null;
}
}
/**
* Register the "explore" feed pipeline.
*/
+1
View File
@@ -16,6 +16,7 @@ export { createKidsAudienceFilterStage } from "./kidsAudienceFilterStage.js";
export {
createChronologicalSorter,
createExploreDiversitySorter,
createKidsScoreSorter,
} from "./sorters.js";
export {
createActiveNostrSource,
+35 -2
View File
@@ -250,15 +250,48 @@ export function createKidsScorerStage({
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 ageProfile = resolveAgeProfile(ageGroup);
const resolvedAgeGroup = resolveAgeGroupFromContext(context);
const ageProfile = resolveAgeProfile(resolvedAgeGroup);
const preferredTags = normalizeTagSet(ageProfile.preferredTags);
const defaultEducationalTags = normalizeTagSet(ageProfile.educationalTags);
const educationalTagSet = educationalTags ? normalizeTagSet(educationalTags) : defaultEducationalTags;
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);
+47
View File
@@ -411,3 +411,50 @@ export function createExploreDiversitySorter({
return selected.concat(orderedRest);
};
}
export function createKidsScoreSorter({
stageName = "kids-score-sorter",
} = {}) {
return function kidsScoreSorter(items = [], context = {}) {
if (!Array.isArray(items)) {
return [];
}
const copy = [...items];
copy.sort((a, b) => {
const aScore = Number(a?.metadata?.kidsScore);
const bScore = Number(b?.metadata?.kidsScore);
const aHasScore = Number.isFinite(aScore);
const bHasScore = Number.isFinite(bScore);
const normalizedAScore = aHasScore ? aScore : Number.NEGATIVE_INFINITY;
const normalizedBScore = bHasScore ? bScore : Number.NEGATIVE_INFINITY;
if (normalizedAScore !== normalizedBScore) {
return normalizedBScore - normalizedAScore;
}
const aRootCreatedAt = Number(a?.video?.rootCreatedAt);
const bRootCreatedAt = Number(b?.video?.rootCreatedAt);
const normalizedARoot =
Number.isFinite(aRootCreatedAt) ? aRootCreatedAt : Number.NEGATIVE_INFINITY;
const normalizedBRoot =
Number.isFinite(bRootCreatedAt) ? bRootCreatedAt : Number.NEGATIVE_INFINITY;
if (normalizedARoot !== normalizedBRoot) {
return normalizedBRoot - normalizedARoot;
}
return stableVideoId(a).localeCompare(stableVideoId(b));
});
if (context?.addWhy && typeof context.addWhy === "function") {
context.addWhy({
stage: stageName,
type: "sort",
reason: "kids-score",
});
}
return copy;
};
}
+1
View File
@@ -266,6 +266,7 @@ export default class ApplicationBootstrap {
}
app.registerRecentFeed();
app.registerForYouFeed();
app.registerKidsFeed();
app.registerExploreFeed();
app.registerSubscriptionsFeed();
app.registerWatchHistoryFeed();