Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.30.0] - 2026-08-26

### Added

- **`categories` on `VideoAnalysisResult`.** `analyzeVideo` already computed a deduped list of visual harm categories across every flagged frame server-side, but only folded it into the free-text `rationale` string — no field existed to read it from without parsing `frame_results` yourself. A client integration that expected a top-level `categories` field (matching every other detection endpoint) got silent empty results instead of an error. **Requires the API deployed on or after 2026-08-26.**

- **`flagProfanity` on `analyzeVideo`, and `profanity` on `VideoAnalysisResult`.** Closes the evasion path left open by shipping profanity on `analyzeImage` alone (2.28.0): burning the same text into a video frame instead of a still image previously never reached the word-list check. The API already runs OCR per frame for video (same vision call as the image endpoint) — it's now aggregated across every frame that has text and checked the same way, with identical precedence to `flagProfanity` elsewhere: explicit per-request value wins, otherwise the account's `default_flag_profanity` setting applies. Only meaningful when at least one frame actually contains OCR text — a video with no on-screen text never gets a `profanity` field regardless of this flag. **Requires the API deployed on or after 2026-08-26.**

## [2.29.0] - 2026-08-26

### Fixed

- **`flagProfanity` is now forwarded through `analyze()`.** Same gap `verdictOnly` had in 2.19, fixed in 2.20: `AnalyzeInput` accepted `flagProfanity`, but `analyze()` never passed it to the `detectBullying`/`detectUnsafe` calls it fans out to. Didn't block anyone — the account's `default_flag_profanity` setting still applied — but per-request override wasn't reachable through the combined method. `result.bullying.profanity`/`result.unsafe.profanity` were already typed correctly and now populate as expected.

## [2.28.0] - 2026-08-25

### Added

- **`flagProfanity` on `analyzeImage`, and `profanity` on `ImageAnalysisResult`.** Extends the `flagProfanity`/`profanity` pair added to `detectBullying`/`detectUnsafe` in 2.27.0 to the image endpoint: the API now runs the same free, deterministic, additive word-list check over an image's OCR'd text (`vision.extracted_text`) when `flag_profanity` is set on the request or the account's `default_flag_profanity` setting is on. Only meaningful when the image actually contains OCR text — `vision.contains_text: false` never produces a `profanity` field regardless of the flag. Never affects `overall_severity`, `recommended_action`, or any `text_analysis` result. Video was evaluated and left out: the API's video analysis has no OCR/text-extraction path to attach this to. **Requires the API deployed on or after 2026-08-25.**

## [2.27.0] - 2026-08-25

### Added

- **`flagProfanity` on `detectBullying` / `detectUnsafe`, and `profanity` / `escalation_capped` / `escalation_capped_reason` on `BullyingResult` / `UnsafeResult`.** The API added `options.flag_profanity` (a free, deterministic, additive word-list flag — never affects `is_bullying`/`unsafe`/`severity`/`risk_score`/`recommended_action`) and an account-level `default_flag_profanity` setting, plus `escalation_capped` on the coded-term corroboration cap, but neither reached the SDK's types or request body. A customer testing `default_flag_profanity` through the SDK had no typed way to override it per-request and no `profanity` field on the result, even though the API already supported both. Explicit `flagProfanity: true` or `flagProfanity: false` both reach the API (an explicit `false` overrides the account default, same precedence as the API itself); omit it to use the account default. **Requires the API deployed on or after 2026-08-25.**

Not yet forwarded through the combined `analyze()` method, which fans out to `detectBullying`/`detectUnsafe` client-side — only the two direct detection methods.

## [2.26.0] - 2026-08-24

### Changed
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tuteliq/sdk",
"version": "2.26.0",
"version": "2.30.0",
"description": "TypeScript SDK for Tuteliq AI child safety API - Detect bullying, grooming, and unsafe content",
"type": "module",
"main": "./dist/index.js",
Expand Down
8 changes: 8 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,7 @@ export class Tuteliq {
const options: Record<string, unknown> = {};
if (input.supportThreshold) options.support_threshold = input.supportThreshold;
if (input.verdictOnly) options.verdict_only = true;
if (input.flagProfanity !== undefined) options.flag_profanity = input.flagProfanity;

return this.requestWithRetry<BullyingResult>(
'POST',
Expand Down Expand Up @@ -819,6 +820,7 @@ export class Tuteliq {
const options: Record<string, unknown> = {};
if (input.supportThreshold) options.support_threshold = input.supportThreshold;
if (input.verdictOnly) options.verdict_only = true;
if (input.flagProfanity !== undefined) options.flag_profanity = input.flagProfanity;

return this.requestWithRetry<UnsafeResult>(
'POST',
Expand Down Expand Up @@ -878,6 +880,9 @@ export class Tuteliq {
// and then copy it into its own result — so `false` read back
// as honoured while both sub-calls still logged an incident.
incident_moderation_enabled: input.incident_moderation_enabled,
// Same gap verdictOnly had before it: accepted on AnalyzeInput
// but never reached the detectors it fans out to.
flagProfanity: input.flagProfanity,
}));
}

Expand All @@ -891,6 +896,7 @@ export class Tuteliq {
customer_id: input.customer_id,
metadata: input.metadata,
incident_moderation_enabled: input.incident_moderation_enabled,
flagProfanity: input.flagProfanity,
}));
}

Expand Down Expand Up @@ -1860,6 +1866,7 @@ export class Tuteliq {
if (input.ageGroup) formData.append('age_group', input.ageGroup);
formData.append('platform', Tuteliq.resolvePlatform(input.platform));
if (input.metadata) formData.append('metadata', JSON.stringify(input.metadata));
if (input.flagProfanity !== undefined) formData.append('flag_profanity', String(input.flagProfanity));

return withRetry(
() => this.multipartRequest<ImageAnalysisResult>(
Expand Down Expand Up @@ -2091,6 +2098,7 @@ export class Tuteliq {
if (input.ageGroup) formData.append('age_group', input.ageGroup);
formData.append('platform', Tuteliq.resolvePlatform(input.platform));
if (input.metadata) formData.append('metadata', JSON.stringify(input.metadata));
if (input.flagProfanity !== undefined) formData.append('flag_profanity', String(input.flagProfanity));

return withRetry(
() => this.multipartRequest<VideoAnalysisResult>(
Expand Down
50 changes: 50 additions & 0 deletions src/types/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,20 @@ export interface AnalyzeVideoInput extends TrackingFields {
ageGroup?: string;
/** Platform name */
platform?: string;
/**
* Additive, deterministic word-list flag for plain profanity/vulgarity
* found in the video's OCR'd text (aggregated across every frame that has
* any). When true, adds a `profanity` field to the response — never
* affects `frame_results`, `overall_severity`, or `recommended_action`.
* Free — no extra credits. Only meaningful when at least one frame
* actually contains OCR text (see `contains_text` semantics on the image
* endpoint) — a video with no on-screen text never gets a `profanity`
* field regardless of this flag. Explicit `true`/`false` here always
* overrides your account's `default_flag_profanity` setting for this
* call; omit to use the account default. **Requires the API deployed on
* or after 2026-08-26.**
*/
flagProfanity?: boolean;
}

export interface VideoAnalysisResult {
Expand All @@ -151,6 +165,14 @@ export interface VideoAnalysisResult {
duration_seconds: number;
/** Per-frame analysis, one entry per sampled frame */
frame_results: VideoFrameResult[];
/**
* Visual harm categories across every flagged frame, deduped. Was
* previously computed server-side but only folded into the free-text
* `rationale` string, with no field to read it from without parsing
* `frame_results` yourself. **Requires the API deployed on or after
* 2026-08-26.**
*/
categories: string[];
/** Points in the video that exceeded the reporting threshold */
flagged_timestamps: VideoFlaggedTimestamp[];
/** Maximum risk score across all findings (0-1) */
Expand Down Expand Up @@ -187,6 +209,14 @@ export interface VideoAnalysisResult {
customer_id?: string;
/** Echo of provided metadata */
metadata?: Record<string, unknown>;
/**
* Present only when `flagProfanity` on this request (or the account-level
* `default_flag_profanity` setting) is true AND at least one frame
* contained OCR text. Deterministic word-list result over the OCR text
* aggregated across frames — additive, never affects `frame_results`,
* `overall_severity`, or `recommended_action`.
*/
profanity?: { detected: boolean; matches: string[] } | null;
}

// =============================================================================
Expand Down Expand Up @@ -223,6 +253,18 @@ export interface AnalyzeImageInput extends TrackingFields {
ageGroup?: string;
/** Platform name */
platform?: string;
/**
* Additive, deterministic word-list flag for plain profanity/vulgarity
* found in the image's OCR'd text. When true, adds a `profanity` field to
* the response — never affects `overall_severity`, `recommended_action`,
* or any `text_analysis` result. Free — no extra credits. Only meaningful
* when the image actually contains OCR text (`vision.contains_text`);
* a no-text image never gets a `profanity` field regardless of this flag.
* Explicit `true`/`false` here always overrides your account's
* `default_flag_profanity` setting for this call; omit to use the
* account default. **Requires the API deployed on or after 2026-08-25.**
*/
flagProfanity?: boolean;
}

export interface ImageAnalysisResult {
Expand Down Expand Up @@ -270,6 +312,14 @@ export interface ImageAnalysisResult {
customer_id?: string;
/** Echo of provided metadata */
metadata?: Record<string, unknown>;
/**
* Present only when `flagProfanity` on this request (or the account-level
* `default_flag_profanity` setting) is true AND the image contained OCR
* text (`vision.contains_text`). Deterministic word-list result over the
* OCR'd text — additive, never affects `overall_severity`,
* `recommended_action`, or any `text_analysis` result.
*/
profanity?: { detected: boolean; matches: string[] } | null;
}

// =============================================================================
Expand Down
63 changes: 63 additions & 0 deletions src/types/safety.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,17 @@ export interface DetectBullyingInput extends TrackingFields {
* conversation. Useful when starting a new chat in the same session.
*/
resetConversation?: boolean;
/**
* Additive, deterministic word-list flag for plain profanity/vulgarity.
* When true, adds a `profanity` field to the response — never affects
* `is_bullying`, `severity`, `risk_score`, or `recommended_action`. Free —
* no extra credits. Not a harm classifier: does not cover slurs or hate
* speech, which the detector itself already handles with full context.
* Explicit `true`/`false` here always overrides your account's
* `default_flag_profanity` setting for this call; omit to use the
* account default. **Requires the API deployed on or after 2026-08-25.**
*/
flagProfanity?: boolean;
}

export interface BullyingResult {
Expand Down Expand Up @@ -285,6 +296,22 @@ export interface BullyingResult {
* why a benign-looking message arrived with elevated conversation risk.
*/
severity_series?: number[];
/**
* True when a coded-term match pushed severity toward critical but this
* endpoint's corroboration-cap logic held `recommended_action` below
* `immediate_intervention` pending independent confirmation. Absent when
* no cap applied.
*/
escalation_capped?: boolean;
/** Human-readable explanation of the cap. Present only when `escalation_capped` is true. */
escalation_capped_reason?: string;
/**
* Present only when `flagProfanity` on this request (or the account-level
* `default_flag_profanity` setting) is true. Deterministic word-list
* result — additive, never affects `is_bullying`/`severity`/`risk_score`/
* `recommended_action`.
*/
profanity?: { detected: boolean; matches: string[] } | null;
/**
* Crisis support resources, present only when the result meets the
* request's `supportThreshold`. Localised to `context.country`.
Expand Down Expand Up @@ -454,6 +481,17 @@ export interface DetectUnsafeInput extends TrackingFields {
* and a smaller payload for real-time screening; the verdict is unchanged.
*/
verdictOnly?: boolean;
/**
* Additive, deterministic word-list flag for plain profanity/vulgarity.
* When true, adds a `profanity` field to the response — never affects
* `unsafe`, `severity`, `risk_score`, or `recommended_action`. Free — no
* extra credits. Not a harm classifier: does not cover slurs or hate
* speech, which the detector itself already handles with full context.
* Explicit `true`/`false` here always overrides your account's
* `default_flag_profanity` setting for this call; omit to use the
* account default. **Requires the API deployed on or after 2026-08-25.**
*/
flagProfanity?: boolean;
}

export interface UnsafeResult {
Expand Down Expand Up @@ -497,6 +535,22 @@ export interface UnsafeResult {
customer_id?: string;
/** Echo of provided metadata (if any) */
metadata?: Record<string, unknown>;
/**
* True when a coded-term match pushed severity toward critical but this
* endpoint's corroboration-cap logic held `recommended_action` below
* `immediate_intervention` pending independent confirmation. Absent when
* no cap applied.
*/
escalation_capped?: boolean;
/** Human-readable explanation of the cap. Present only when `escalation_capped` is true. */
escalation_capped_reason?: string;
/**
* Present only when `flagProfanity` on this request (or the account-level
* `default_flag_profanity` setting) is true. Deterministic word-list
* result — additive, never affects `unsafe`/`severity`/`risk_score`/
* `recommended_action`.
*/
profanity?: { detected: boolean; matches: string[] } | null;
/**
* Crisis support resources, present only when the result meets the
* request's `supportThreshold`. Localised to `context.country`.
Expand Down Expand Up @@ -528,6 +582,15 @@ export interface AnalyzeInput extends TrackingFields {
* on the slower detector rather than the full per-call saving.
*/
verdictOnly?: boolean;
/**
* Forwarded to each detector this call fans out to, same semantics as
* `flagProfanity` on `detectBullying`/`detectUnsafe` directly: an
* additive, deterministic word-list flag that adds a `profanity` field to
* `result.bullying`/`result.unsafe`, never affecting risk scoring or
* `recommended_action`. Omit to use the account's `default_flag_profanity`
* setting. **Requires the API deployed on or after 2026-08-25.**
*/
flagProfanity?: boolean;
}

export interface AnalyzeResult {
Expand Down
Loading