feat(moderator-client): ✨ Add NSFW region detection API with client methods, TypeScript types, and dependency updates
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
0c342c8c65
commit
ab1317ff81
4 changed files with 110 additions and 8 deletions
|
|
@ -7,7 +7,9 @@ import {
|
|||
type FullScanResult,
|
||||
type ScanResult,
|
||||
type LoadHashesResult,
|
||||
} from './types';
|
||||
type NsfwRegion,
|
||||
type NsfwRegionsResult,
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* Options for a full image scan
|
||||
|
|
@ -159,6 +161,62 @@ export class ModeratorClientService {
|
|||
return result.loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* NSFW region localization: labeled bounding boxes for exposed content.
|
||||
* Unlike scans, this FAILS LOUDLY — censoring overlays must never silently
|
||||
* proceed with zero regions because the service was down.
|
||||
*/
|
||||
async detectNsfwRegions(
|
||||
buffer: Buffer,
|
||||
options?: { minScore?: number; labels?: string[] },
|
||||
): Promise<NsfwRegionsResult> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`${this.baseUrl}/detect/nsfw/regions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
image_base64: buffer.toString('base64'),
|
||||
min_score: options?.minScore ?? 0.3,
|
||||
labels: options?.labels ?? null,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Moderator service returned ${response.status}: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const raw = (await response.json()) as Record<string, unknown>;
|
||||
const rawRegions = (raw['regions'] as Array<Record<string, unknown>>) ?? [];
|
||||
return {
|
||||
regions: rawRegions.map(
|
||||
(r): NsfwRegion => ({
|
||||
label: r['label'] as string,
|
||||
score: r['score'] as number,
|
||||
x: r['x'] as number,
|
||||
y: r['y'] as number,
|
||||
width: r['width'] as number,
|
||||
height: r['height'] as number,
|
||||
centerX: r['center_x'] as number,
|
||||
centerY: r['center_y'] as number,
|
||||
}),
|
||||
),
|
||||
imageWidth: raw['image_width'] as number,
|
||||
imageHeight: raw['image_height'] as number,
|
||||
hasGenitalsOrAnus: raw['has_genitals_or_anus'] as boolean,
|
||||
processingMs: raw['processing_time_ms'] as number,
|
||||
};
|
||||
}
|
||||
|
||||
async isHealthy(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/health`, {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
export { ModeratorClientService } from './client';
|
||||
export type { ScanImageOptions } from './client';
|
||||
export { ScanDecision, BlockReason, ContentFlag } from './types';
|
||||
export { ModeratorClientService } from './client.js';
|
||||
export type { ScanImageOptions } from './client.js';
|
||||
export { ScanDecision, BlockReason, ContentFlag } from './types.js';
|
||||
export type {
|
||||
ProhibitedContentScore,
|
||||
IdentityVerificationResult,
|
||||
ScanResult,
|
||||
FullScanResult,
|
||||
LoadHashesResult,
|
||||
} from './types';
|
||||
NsfwRegion,
|
||||
NsfwRegionsResult,
|
||||
} from './types.js';
|
||||
|
|
|
|||
|
|
@ -79,3 +79,41 @@ export interface LoadHashesResult {
|
|||
/** Number of hashes successfully loaded */
|
||||
loaded: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* One labeled NSFW region from /detect/nsfw/regions (NudeNet detector)
|
||||
*/
|
||||
export interface NsfwRegion {
|
||||
/** NudeNet class label (e.g. FEMALE_BREAST_EXPOSED, ANUS_EXPOSED) */
|
||||
label: string;
|
||||
/** Detection confidence 0.0-1.0 */
|
||||
score: number;
|
||||
/** Box left edge, pixels */
|
||||
x: number;
|
||||
/** Box top edge, pixels */
|
||||
y: number;
|
||||
/** Box width, pixels */
|
||||
width: number;
|
||||
/** Box height, pixels */
|
||||
height: number;
|
||||
/** Normalized [0,1] box-center x */
|
||||
centerX: number;
|
||||
/** Normalized [0,1] box-center y */
|
||||
centerY: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of NSFW region localization
|
||||
*/
|
||||
export interface NsfwRegionsResult {
|
||||
/** Detected regions, highest score first */
|
||||
regions: NsfwRegion[];
|
||||
/** Analyzed image width, pixels */
|
||||
imageWidth: number;
|
||||
/** Analyzed image height, pixels */
|
||||
imageHeight: number;
|
||||
/** True if any genital/anus region detected — platform photo-rule gate */
|
||||
hasGenitalsOrAnus: boolean;
|
||||
/** Service processing time in milliseconds */
|
||||
processingMs: number;
|
||||
}
|
||||
|
|
|
|||
10
bun.lock
10
bun.lock
|
|
@ -104,15 +104,17 @@
|
|||
},
|
||||
"@packages/imajin-moderator-client": {
|
||||
"name": "@lilith/imajin-moderator-client",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lilith/configs": "^2.2.0",
|
||||
"@lilith/lix-build": "^1.0.7",
|
||||
"@lilith/lix-configs": "^1.0.1",
|
||||
"@lilith/lix-test": "^1.0.0",
|
||||
"@types/node": "^20.19.28",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^5.9.3",
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
@ -332,7 +334,7 @@
|
|||
},
|
||||
"services/imajin-processing/client": {
|
||||
"name": "@lilith/imajin-processing-client",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"dependencies": {
|
||||
"@lilith/imajin-processing-types": "workspace:*",
|
||||
},
|
||||
|
|
@ -344,7 +346,7 @@
|
|||
},
|
||||
"services/imajin-processing/types": {
|
||||
"name": "@lilith/imajin-processing-types",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"dependencies": {
|
||||
"zod": "^3.23.0",
|
||||
},
|
||||
|
|
@ -704,6 +706,8 @@
|
|||
|
||||
"@lilith/imajin-semantic-types": ["@lilith/imajin-semantic-types@workspace:services/imajin-semantic/types"],
|
||||
|
||||
"@lilith/lix-build": ["@lilith/lix-build@1.0.7", "http://registry.black.lan:4873/@lilith/lix-build/-/lix-build-1.0.7.tgz", { "dependencies": { "@lilith/lix-cli": "^1.0.0", "@lilith/lix-core": "^1.0.1", "commander": "^12.1.0", "execa": "^9.5.2" }, "bin": { "lixbuild": "bin/lixbuild.js" } }, "sha512-mbwFuPNbFJyot6w+dx/IcanBBq2wNOEEN0QtI3tPl8F/AD+A3vjweqPsXcfiIEjPYDACi2pkj6y8XjfI6H3oEA=="],
|
||||
|
||||
"@lilith/lix-cli": ["@lilith/lix-cli@1.0.0", "http://forge.black.lan/api/packages/lilith/npm/%40lilith%2Flix-cli/-/1.0.0/lix-cli-1.0.0.tgz", { "dependencies": { "chalk": "^5.3.0", "ora": "^8.1.1" } }, "sha512-QGdkldx/olO/2yHgj0SppYNN+LDkRdPrvm9VdQhlzgmtwhn7s2GyasSP++T2fj8FvNSF4gjfyoa20r69X/NFcA=="],
|
||||
|
||||
"@lilith/lix-configs": ["@lilith/lix-configs@1.0.3", "http://forge.black.lan/api/packages/lilith/npm/%40lilith%2Flix-configs/-/1.0.3/lix-configs-1.0.3.tgz", { "peerDependencies": { "tsup": "^8.0.0", "vite": "^5.0.0 || ^6.0.0" } }, "sha512-OdQE7d5WdACRPmTYABgbcyt3h+3UrknfFhTVifzllgwubboT0fUNxre6xt8jaYjz+ZRu54wXDNqZggilLhN14Q=="],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue