mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
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:
5 files changed
+228
-2
No files matched your search
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,7 @@ export { createKidsAudienceFilterStage } from "./kidsAudienceFilterStage.js";
|
||||
export {
|
||||
createChronologicalSorter,
|
||||
createExploreDiversitySorter,
|
||||
createKidsScoreSorter,
|
||||
} from "./sorters.js";
|
||||
export {
|
||||
createActiveNostrSource,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -266,6 +266,7 @@ export default class ApplicationBootstrap {
|
||||
}
|
||||
app.registerRecentFeed();
|
||||
app.registerForYouFeed();
|
||||
app.registerKidsFeed();
|
||||
app.registerExploreFeed();
|
||||
app.registerSubscriptionsFeed();
|
||||
app.registerWatchHistoryFeed();
|
||||
|
||||
Reference in new issue
Block a user