Enforce commenter gender targeting
This commit is contained in:
1 parent
8523873831
commit
12e2dc56f1
5 files changed
+38
-6
No files matched your search
@@ -30,7 +30,7 @@ Never document real values. `.env` is ignored and may contain production credent
|
|||||||
|
|
||||||
### `reddit`
|
### `reddit`
|
||||||
|
|
||||||
- `subreddits`: at least one item with `name`, `enabled`, `type`, optional `targetingMode`, optional `weight`, `tone`, optional scripted opener, and conversation policy. `targetingMode` defaults to `post-author`; `comment-author` is valid only for image subreddits and selects high-confidence comments expressing a desire to take the depicted role. The normal image screen separately enforces `reddit.targetGenders`; the comment classifier does not claim to identify the commenter's actual gender.
|
- `subreddits`: at least one item with `name`, `enabled`, `type`, optional `targetingMode`, optional `weight`, `tone`, optional scripted opener, and conversation policy. `targetingMode` defaults to `post-author`; `comment-author` is valid only for image subreddits and selects high-confidence comments expressing a desire to take the depicted role. When `reddit.targetGenders` is non-empty, the gender perspective expressed by the comment must be included; `unknown` fails closed unless explicitly targeted. The normal image screen separately enforces the same list against the depicted role. The comment classifier does not claim to identify the commenter's actual gender.
|
||||||
- `pollIntervalMs`: legacy/scheduled polling cadence.
|
- `pollIntervalMs`: legacy/scheduled polling cadence.
|
||||||
- `userAgent`: Reddit JSON request identifier.
|
- `userAgent`: Reddit JSON request identifier.
|
||||||
- `targetGenders`: allowed parsed values.
|
- `targetGenders`: allowed parsed values.
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Configured subreddits have a source type:
|
|||||||
- `image`: image-led posts are downloaded and analyzed before an opener is generated.
|
- `image`: image-led posts are downloaded and analyzed before an opener is generated.
|
||||||
- `chat`: text-led posts require a parseable self-declared gender and adult age, then use the chat-opener prompt.
|
- `chat`: text-led posts require a parseable self-declared gender and adult age, then use the chat-opener prompt.
|
||||||
|
|
||||||
Image subreddits may separately target either the post author (the default) or a qualifying commenter. Comment-author targeting scans a bounded set of top-level comments on newly listed image posts and uses a fail-closed AI classification. A comment qualifies only when it personally expresses a desire to experience or take the depicted role and confidence is at least 0.8. The normal image screen then verifies that the depicted role matches `reddit.targetGenders`. This is an interpretation of role alignment in the public comment, not a claim about the author's actual gender or identity. Generic praise, ambiguous reactions, deleted/moderator comments, and the post author's own comments do not qualify. The original image remains scene context, but commenter openers use a separate prompt contract and must ground the message in what the commenter expressed. Drafts fail closed when they attribute the pictured body, pose, post, or image to the commenter, request another image, or omit a clear connection to the comment.
|
Image subreddits may separately target either the post author (the default) or a qualifying commenter. Comment-author targeting scans a bounded set of top-level comments on newly listed image posts and uses a fail-closed AI classification. A comment qualifies only when it personally expresses a desire to experience or take the depicted role, confidence is at least 0.8, and the gender perspective expressed by the comment is included in `reddit.targetGenders` when that list is non-empty. `unknown` therefore fails closed unless it is explicitly targeted. The normal image screen also verifies that the depicted role matches `reddit.targetGenders`. The comment verdict is an interpretation of public text, not a claim about the author's actual gender or identity. Generic praise, ambiguous reactions, deleted/moderator comments, and the post author's own comments do not qualify. The original image remains scene context, but commenter openers use a separate prompt contract and must ground the message in what the commenter expressed. Drafts fail closed when they attribute the pictured body, pose, post, or image to the commenter, request another image, or omit a clear connection to the comment.
|
||||||
|
|
||||||
Image analysis may produce a subjective 1–10 visible-physique score. It is not a medical assessment and remains unknown when the image does not show enough of the body for a grounded judgment. The scale is deliberately critical: 5 is broadly average, 6 requires visible above-average leanness or definition, and 8–10 are reserved for rare athletic or model-like physiques. Nudity and photographic presentation do not increase the score. Physique targeting is disabled by default. When an operator enables an inclusive minimum/maximum range, image candidates outside that range—and unscorable image candidates—are rejected before opener generation; chat candidates are unaffected.
|
Image analysis may produce a subjective 1–10 visible-physique score. It is not a medical assessment and remains unknown when the image does not show enough of the body for a grounded judgment. The scale is deliberately critical: 5 is broadly average, 6 requires visible above-average leanness or definition, and 8–10 are reserved for rare athletic or model-like physiques. Nudity and photographic presentation do not increase the score. Physique targeting is disabled by default. When an operator enables an inclusive minimum/maximum range, image candidates outside that range—and unscorable image candidates—are rejected before opener generation; chat candidates are unaffected.
|
||||||
|
|
||||||
|
|||||||
@@ -426,7 +426,9 @@ Use every applicable tag; tags may overlap. Set safety to "nsfw" when nudity, ex
|
|||||||
+ 'the comment to state a gender. Generic praise, jokes, requests directed at '
|
+ 'the comment to state a gender. Generic praise, jokes, requests directed at '
|
||||||
+ 'the post author, emojis, and ambiguous reactions do not qualify. expressedGender is the gender perspective the '
|
+ 'the post author, emojis, and ambiguous reactions do not qualify. expressedGender is the gender perspective the '
|
||||||
+ 'comment explicitly states or strongly communicates in relation to the depicted role, not a claim about the '
|
+ 'comment explicitly states or strongly communicates in relation to the depicted role, not a claim about the '
|
||||||
+ 'author\'s real identity. Use unknown whenever the text does not explicitly support one. Return JSON only: '
|
+ 'author\'s real identity. Self-referential sex characteristics strongly communicate the matching perspective; '
|
||||||
|
+ 'for example, references to "my bulge", "my cock", or "my dick" communicate male. Use unknown whenever the text '
|
||||||
|
+ 'does not explicitly support or strongly communicate a perspective. Return JSON only: '
|
||||||
+ '{"results":[{"id":"c1","qualifies":true,"expressedGender":"female","confidence":0.9,"evidence":"short reason"}]}. '
|
+ '{"results":[{"id":"c1","qualifies":true,"expressedGender":"female","confidence":0.9,"evidence":"short reason"}]}. '
|
||||||
+ 'Confidence is between 0 and 1; keep evidence under 200 characters.';
|
+ 'Confidence is between 0 and 1; keep evidence under 200 characters.';
|
||||||
const user = comments.map((comment) => (
|
const user = comments.map((comment) => (
|
||||||
|
|||||||
@@ -287,6 +287,8 @@ export class MonitorService {
|
|||||||
return pairs.flatMap(({ post, comment }): RedditPost[] => {
|
return pairs.flatMap(({ post, comment }): RedditPost[] => {
|
||||||
const verdict = this.commentReactionCache.get(comment.fullname);
|
const verdict = this.commentReactionCache.get(comment.fullname);
|
||||||
if (!verdict?.qualifies || verdict.confidence < COMMENT_REACTION_CONFIDENCE) return [];
|
if (!verdict?.qualifies || verdict.confidence < COMMENT_REACTION_CONFIDENCE) return [];
|
||||||
|
const targetGenders = this.config.reddit.targetGenders;
|
||||||
|
if (targetGenders.length > 0 && !targetGenders.includes(verdict.expressedGender)) return [];
|
||||||
return [{
|
return [{
|
||||||
...post,
|
...post,
|
||||||
id: comment.fullname,
|
id: comment.fullname,
|
||||||
|
|||||||
+31
-3
@@ -727,7 +727,7 @@ describe('MonitorService', () => {
|
|||||||
expect(screener.screenCommentReactions).toHaveBeenCalledTimes(1);
|
expect(screener.screenCommentReactions).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails closed on low-confidence or non-qualifying reactions', async () => {
|
it('fails closed on low-confidence, non-qualifying, or non-targeted reactions', async () => {
|
||||||
const source = {
|
const source = {
|
||||||
id: 'post1', name: 't3_post1', subreddit: 'scene', author: 'poster', title: 'A scene',
|
id: 'post1', name: 't3_post1', subreddit: 'scene', author: 'poster', title: 'A scene',
|
||||||
url: 'https://example.com/scene.jpg', permalink: '/r/scene/comments/post1/', thumbnail: '',
|
url: 'https://example.com/scene.jpg', permalink: '/r/scene/comments/post1/', thumbnail: '',
|
||||||
@@ -736,11 +736,13 @@ describe('MonitorService', () => {
|
|||||||
const redditApi = createMockRedditApi([source]);
|
const redditApi = createMockRedditApi([source]);
|
||||||
redditApi.getBrowsingComments = vi.fn().mockResolvedValue([
|
redditApi.getBrowsingComments = vi.fn().mockResolvedValue([
|
||||||
{ fullname: 't1_low', author: 'a', text: 'I want this as well', removed: false, createdAt: 101_000, permalink: '/low' },
|
{ fullname: 't1_low', author: 'a', text: 'I want this as well', removed: false, createdAt: 101_000, permalink: '/low' },
|
||||||
{ fullname: 't1_male', author: 'b', text: 'As a man I want this', removed: false, createdAt: 102_000, permalink: '/male' },
|
{ fullname: 't1_male', author: 'b', text: 'I want to put my bulge there', removed: false, createdAt: 102_000, permalink: '/male' },
|
||||||
|
{ fullname: 't1_unknown', author: 'c', text: 'I would love this', removed: false, createdAt: 103_000, permalink: '/unknown' },
|
||||||
]);
|
]);
|
||||||
const screener = { screenCommentReactions: vi.fn().mockResolvedValue([
|
const screener = { screenCommentReactions: vi.fn().mockResolvedValue([
|
||||||
{ id: 't1_low', qualifies: true, expressedGender: 'female', confidence: 0.79, evidence: '' },
|
{ id: 't1_low', qualifies: true, expressedGender: 'female', confidence: 0.79, evidence: '' },
|
||||||
{ id: 't1_male', qualifies: false, expressedGender: 'male', confidence: 0.99, evidence: '' },
|
{ id: 't1_male', qualifies: true, expressedGender: 'male', confidence: 0.99, evidence: 'self-referential male anatomy' },
|
||||||
|
{ id: 't1_unknown', qualifies: true, expressedGender: 'unknown', confidence: 0.95, evidence: 'no gender perspective' },
|
||||||
]) };
|
]) };
|
||||||
const config = getTestConfig({ reddit: {
|
const config = getTestConfig({ reddit: {
|
||||||
subreddits: [{ name: 'scene', type: 'image', targetingMode: 'comment-author' }],
|
subreddits: [{ name: 'scene', type: 'image', targetingMode: 'comment-author' }],
|
||||||
@@ -752,5 +754,31 @@ describe('MonitorService', () => {
|
|||||||
|
|
||||||
await expect(monitor.fetchCandidates(config.reddit.subreddits[0])).resolves.toEqual([]);
|
await expect(monitor.fetchCandidates(config.reddit.subreddits[0])).resolves.toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows an expressed gender when it is explicitly targeted', async () => {
|
||||||
|
const source = {
|
||||||
|
id: 'post1', name: 't3_post1', subreddit: 'scene', author: 'poster', title: 'A scene',
|
||||||
|
url: 'https://example.com/scene.jpg', permalink: '/r/scene/comments/post1/', thumbnail: '',
|
||||||
|
is_self: false, created_utc: 100, over_18: true, spoiler: false,
|
||||||
|
} as RedditPost;
|
||||||
|
const redditApi = createMockRedditApi([source]);
|
||||||
|
redditApi.getBrowsingComments = vi.fn().mockResolvedValue([
|
||||||
|
{ fullname: 't1_unknown', author: 'a', text: 'I would love this', removed: false, createdAt: 101_000, permalink: '/unknown' },
|
||||||
|
]);
|
||||||
|
const screener = { screenCommentReactions: vi.fn().mockResolvedValue([
|
||||||
|
{ id: 't1_unknown', qualifies: true, expressedGender: 'unknown', confidence: 0.95, evidence: '' },
|
||||||
|
]) };
|
||||||
|
const config = getTestConfig({ reddit: {
|
||||||
|
subreddits: [{ name: 'scene', type: 'image', targetingMode: 'comment-author' }],
|
||||||
|
targetGenders: ['unknown'],
|
||||||
|
} });
|
||||||
|
monitor = new MonitorService(
|
||||||
|
redditApi, createMockPostsRepo(), createMockMessagesRepo(), config, logger, 0, undefined, screener,
|
||||||
|
);
|
||||||
|
|
||||||
|
const candidates = await monitor.fetchCandidates(config.reddit.subreddits[0]);
|
||||||
|
|
||||||
|
expect(candidates.map((candidate) => candidate.id)).toEqual(['t1_unknown']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user