Skip to content
Open
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
123 changes: 123 additions & 0 deletions apps/sim/blocks/blocks/resemble.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { ResembleIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { ResembleResponse } from '@/tools/resemble/types'

export const ResembleBlock: BlockConfig<ResembleResponse> = {
type: 'resemble',
name: 'Resemble',
description: 'Deepfake detection, media intelligence, and watermarking',
longDescription:
'Integrate Resemble AI media safety into your workflow: detect deepfakes in audio/image/video, analyze media intelligence, and apply or detect invisible watermarks.',
docsLink: 'https://docs.resemble.ai',
category: 'tools',
integrationType: IntegrationType.Security,
tags: ['deepfake-detection', 'media-safety'],
bgColor: '#2E1AC4',
icon: ResembleIcon,
authMode: AuthMode.ApiKey,

subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Deepfake Detection', id: 'resemble_detect' },
{ label: 'Media Intelligence', id: 'resemble_intelligence' },
{ label: 'Detect Watermark', id: 'resemble_watermark_detect' },
{ label: 'Apply Watermark', id: 'resemble_watermark_apply' },
],
value: () => 'resemble_detect',
},
{
id: 'url',
title: 'Media URL',
type: 'short-input',
placeholder: 'https://example.com/media.mp4',
required: true,
},
// Detection toggles
{ id: 'runIntelligence', title: 'Run Intelligence', type: 'switch', condition: { field: 'operation', value: 'resemble_detect' } },
{ id: 'audioSourceTracing', title: 'Audio Source Tracing', type: 'switch', condition: { field: 'operation', value: 'resemble_detect' } },
{ id: 'visualize', title: 'Visualize', type: 'switch', condition: { field: 'operation', value: 'resemble_detect' } },
{ id: 'useReverseSearch', title: 'Reverse Image Search', type: 'switch', condition: { field: 'operation', value: 'resemble_detect' } },
{ id: 'useOodDetector', title: 'OOD Detector', type: 'switch', condition: { field: 'operation', value: 'resemble_detect' } },
{ id: 'zeroRetentionMode', title: 'Zero-Retention Mode', type: 'switch', condition: { field: 'operation', value: 'resemble_detect' } },
{
id: 'modelTypes',
title: 'Model Type',
type: 'dropdown',
options: [
{ label: 'Auto', id: 'auto' },
{ label: 'Image', id: 'image' },
{ label: 'Talking Head', id: 'talking_head' },
],
value: () => 'auto',
condition: { field: 'operation', value: 'resemble_detect' },
},
// Intelligence options
{
id: 'mediaType',
title: 'Media Type',
type: 'dropdown',
options: [
{ label: 'Auto', id: 'auto' },
{ label: 'Audio', id: 'audio' },
{ label: 'Video', id: 'video' },
{ label: 'Image', id: 'image' },
],
value: () => 'auto',
condition: { field: 'operation', value: 'resemble_intelligence' },
Comment on lines +59 to +71

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 structuredJson parameter has no corresponding UI control

intelligenceTool declares a structuredJson boolean param that controls whether the API returns structured JSON fields. Because no subBlock exists for it in the block definition, the value is always undefined in the UI, which causes the body builder to evaluate p.structuredJson !== false as true — permanently fixing it to structured mode. Users who need raw/unstructured output from the Intelligence operation cannot opt out from the block UI.

},
// Apply-watermark options
{ id: 'strength', title: 'Strength (0–1)', type: 'short-input', placeholder: '0.2', condition: { field: 'operation', value: 'resemble_watermark_apply' } },
{ id: 'customMessage', title: 'Custom Message', type: 'short-input', placeholder: 'resembleai', condition: { field: 'operation', value: 'resemble_watermark_apply' } },
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your Resemble API key',
required: true,
password: true,
},
],

tools: {
access: ['resemble_detect', 'resemble_intelligence', 'resemble_watermark_detect', 'resemble_watermark_apply'],
config: {
tool: (params) => {
switch (params.operation) {
case 'resemble_intelligence':
return 'resemble_intelligence'
case 'resemble_watermark_detect':
return 'resemble_watermark_detect'
case 'resemble_watermark_apply':
return 'resemble_watermark_apply'
default:
return 'resemble_detect'
}
},
},
},

inputs: {
operation: { type: 'string', description: 'Operation to perform' },
url: { type: 'string', description: 'Public HTTPS URL to the media' },
runIntelligence: { type: 'boolean', description: 'Also run media intelligence' },
audioSourceTracing: { type: 'boolean', description: 'Trace the source platform of fake audio' },
visualize: { type: 'boolean', description: 'Generate heatmap artifacts' },
useReverseSearch: { type: 'boolean', description: 'Image-only reverse image search' },
useOodDetector: { type: 'boolean', description: 'Out-of-distribution detection' },
zeroRetentionMode: { type: 'boolean', description: 'Auto-delete media after analysis' },
modelTypes: { type: 'string', description: 'auto | image | talking_head' },
mediaType: { type: 'string', description: 'auto | audio | video | image' },
strength: { type: 'number', description: 'Watermark strength 0–1' },
customMessage: { type: 'string', description: 'Watermark message' },
apiKey: { type: 'string', description: 'Resemble API key' },
},

outputs: {
result: { type: 'json', description: 'Result from the selected Resemble operation' },
},
}
2 changes: 2 additions & 0 deletions apps/sim/blocks/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ import { SearchBlock } from '@/blocks/blocks/search'
import { SecretsManagerBlock } from '@/blocks/blocks/secrets_manager'
import { SendGridBlock } from '@/blocks/blocks/sendgrid'
import { SentryBlock } from '@/blocks/blocks/sentry'
import { ResembleBlock } from '@/blocks/blocks/resemble'
import { SerperBlock } from '@/blocks/blocks/serper'
import { ServiceNowBlock } from '@/blocks/blocks/servicenow'
import { SESBlock } from '@/blocks/blocks/ses'
Expand Down Expand Up @@ -461,6 +462,7 @@ export const registry: Record<string, BlockConfig> = {
search: SearchBlock,
sendgrid: SendGridBlock,
sentry: SentryBlock,
resemble: ResembleBlock,
serper: SerperBlock,
servicenow: ServiceNowBlock,
sftp: SftpBlock,
Expand Down
19 changes: 19 additions & 0 deletions apps/sim/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7391,3 +7391,22 @@ export function WizaIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}

export function ResembleIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.6'
role='img'
xmlns='http://www.w3.org/2000/svg'
>
<path d='M12 2.5l7 2.4v5c0 4.3-2.9 8.2-7 9.6-4.1-1.4-7-5.3-7-9.6v-5l7-2.4z' strokeLinejoin='round' />
<line x1='9' y1='10.5' x2='9' y2='13.5' strokeLinecap='round' />
<line x1='12' y1='8.5' x2='12' y2='15.5' strokeLinecap='round' />
<line x1='15' y1='10' x2='15' y2='14' strokeLinecap='round' />
</svg>
)
}
10 changes: 10 additions & 0 deletions apps/sim/tools/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2664,6 +2664,12 @@ import {
updateIssueTool,
updateProjectTool,
} from '@/tools/sentry'
import {
detectTool as resembleDetectTool,
intelligenceTool as resembleIntelligenceTool,
watermarkApplyTool as resembleWatermarkApplyTool,
watermarkDetectTool as resembleWatermarkDetectTool,
} from '@/tools/resemble'
import { serperSearchTool } from '@/tools/serper'
import {
servicenowCreateRecordTool,
Expand Down Expand Up @@ -3843,6 +3849,10 @@ export const tools: Record<string, ToolConfig> = {
github_repo_info_v2: githubRepoInfoV2Tool,
github_latest_commit: githubLatestCommitTool,
github_latest_commit_v2: githubLatestCommitV2Tool,
resemble_detect: resembleDetectTool,
resemble_intelligence: resembleIntelligenceTool,
resemble_watermark_detect: resembleWatermarkDetectTool,
resemble_watermark_apply: resembleWatermarkApplyTool,
serper_search: serperSearchTool,
similarweb_website_overview: similarwebWebsiteOverviewTool,
similarweb_traffic_visits: similarwebTrafficVisitsTool,
Expand Down
54 changes: 54 additions & 0 deletions apps/sim/tools/resemble/detect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { ResembleDetectParams, ResembleResponse } from '@/tools/resemble/types'
import { authHeaders, baseOf, pollResource, rItem, sanitize } from '@/tools/resemble/utils'
import type { ToolConfig } from '@/tools/types'

export const detectTool: ToolConfig<ResembleDetectParams, ResembleResponse> = {
id: 'resemble_detect',
name: 'Resemble Deepfake Detection',
description: 'Detect whether media (audio, image, or video) is a deepfake / AI-generated.',
version: '1.0.0',
params: {
apiKey: { type: 'string', required: true, visibility: 'user-only', description: 'Resemble API key' },
url: { type: 'string', required: true, visibility: 'user-or-llm', description: 'Public HTTPS URL to the media' },
runIntelligence: { type: 'boolean', required: false, visibility: 'user-only', description: 'Also run media intelligence' },
audioSourceTracing: { type: 'boolean', required: false, visibility: 'user-only', description: 'Trace the source platform of fake audio' },
visualize: { type: 'boolean', required: false, visibility: 'user-only', description: 'Generate heatmap artifacts' },
useReverseSearch: { type: 'boolean', required: false, visibility: 'user-only', description: 'Image-only reverse image search' },
useOodDetector: { type: 'boolean', required: false, visibility: 'user-only', description: 'Out-of-distribution detection' },
zeroRetentionMode: { type: 'boolean', required: false, visibility: 'user-only', description: 'Auto-delete media after analysis' },
modelTypes: { type: 'string', required: false, visibility: 'user-only', description: 'auto | image | talking_head' },
maxWaitSeconds: { type: 'number', required: false, visibility: 'user-only', description: 'Max seconds to poll for the result' },
baseUrl: { type: 'string', required: false, visibility: 'user-only', description: 'API base URL override' },
},
request: {
url: (p) => `${baseOf(p)}/detect`,
method: 'POST',
headers: (p) => authHeaders(p),
body: (p) => {
const b: Record<string, any> = { url: p.url }
if (p.runIntelligence) b.intelligence = true
if (p.audioSourceTracing) b.audio_source_tracing = true
if (p.visualize) b.visualize = true
if (p.useReverseSearch) b.use_reverse_search = true
if (p.useOodDetector) b.use_ood_detector = true
if (p.zeroRetentionMode) b.zero_retention_mode = true
if (p.modelTypes && p.modelTypes !== 'auto') b.model_types = p.modelTypes
return b
},
},
transformResponse: async (response: Response, params?: ResembleDetectParams) => {
let data: any
try {
data = await response.json()
} catch {
data = { raw: await response.text() }
}
if (!response.ok) throw new Error((data && data.message) || `Resemble API error: HTTP ${response.status}`)
const uuid = rItem(data).uuid
if (uuid && params) {
data = await pollResource(baseOf(params), `/detect/${uuid}`, authHeaders(params), params.maxWaitSeconds || 120)
}
return { success: true, output: { result: sanitize(data) } }
},
outputs: { result: { type: 'json', description: 'Detection result (label, score, metrics, optional intelligence).' } },
}
5 changes: 5 additions & 0 deletions apps/sim/tools/resemble/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { detectTool } from '@/tools/resemble/detect'
export { intelligenceTool } from '@/tools/resemble/intelligence'
export { watermarkDetectTool } from '@/tools/resemble/watermark_detect'
export { watermarkApplyTool } from '@/tools/resemble/watermark_apply'
export * from '@/tools/resemble/types'
50 changes: 50 additions & 0 deletions apps/sim/tools/resemble/intelligence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { ResembleIntelligenceParams, ResembleResponse } from '@/tools/resemble/types'
import { authHeaders, baseOf, pollResource, rItem, sanitize } from '@/tools/resemble/utils'
import type { ToolConfig } from '@/tools/types'

const TERMINAL = new Set(['completed', 'failed', 'error', 'cancelled', 'success'])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicate TERMINAL set — divergence risk

utils.ts already defines an identical TERMINAL set on line 2, but it is not exported. intelligence.ts re-declares it locally, so if a new terminal status is ever added to utils.ts (e.g., "timeout"), the pre-poll check here will be silently missed. Exporting TERMINAL from utils.ts and importing it here eliminates the drift risk.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


export const intelligenceTool: ToolConfig<ResembleIntelligenceParams, ResembleResponse> = {
id: 'resemble_intelligence',
name: 'Resemble Media Intelligence',
description: 'Analyze media for transcription, translation, speaker info, emotion, and misinformation.',
version: '1.0.0',
params: {
apiKey: { type: 'string', required: true, visibility: 'user-only', description: 'Resemble API key' },
url: { type: 'string', required: true, visibility: 'user-or-llm', description: 'Public HTTPS URL to the media' },
structuredJson: { type: 'boolean', required: false, visibility: 'user-only', description: 'Return structured JSON fields' },
mediaType: { type: 'string', required: false, visibility: 'user-only', description: 'auto | audio | video | image' },
maxWaitSeconds: { type: 'number', required: false, visibility: 'user-only', description: 'Max seconds to poll' },
baseUrl: { type: 'string', required: false, visibility: 'user-only', description: 'API base URL override' },
},
request: {
url: (p) => `${baseOf(p)}/intelligence`,
method: 'POST',
headers: (p) => authHeaders(p),
body: (p) => {
const b: Record<string, any> = { url: p.url, json: p.structuredJson !== false }
if (p.mediaType && p.mediaType !== 'auto') b.media_type = p.mediaType
return b
},
},
transformResponse: async (response: Response, params?: ResembleIntelligenceParams) => {
let data: any
try {
data = await response.json()
} catch {
data = { raw: await response.text() }
}
if (!response.ok) throw new Error((data && data.message) || `Resemble API error: HTTP ${response.status}`)
const it = rItem(data)
const status = (it.status || '').toString().toLowerCase()
if (it.uuid && status && !TERMINAL.has(status) && params) {
try {
data = await pollResource(baseOf(params), `/intelligence/${it.uuid}`, authHeaders(params), params.maxWaitSeconds || 120)
} catch {
/* poll path may vary; return submit payload */
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong intelligence poll URL

Medium Severity

When media intelligence is still in a non-terminal state, polling uses /intelligence/{uuid} instead of the documented /intelligences/{uuid} GET route. Poll failures are caught and ignored, so the tool can return success: true with the initial submit payload instead of finished analysis.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c2e3d83. Configure here.

Comment on lines +41 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Poll failure silently returns a pending payload as success

When pollResource throws (e.g., the result endpoint returns 404, or a network error occurs), the catch block discards the error and the function returns the submit-phase payload — typically { status: "processing", uuid: "..." } — wrapped as { success: true }. Callers have no way to distinguish "analysis complete" from "polling failed and the job is still running."

The same pattern appears in watermark_apply.ts (lines 39–43). Contrast with detect.ts, which intentionally lets the pollResource error propagate so callers can react to it.

}
return { success: true, output: { result: sanitize(data) } }
},
outputs: { result: { type: 'json', description: 'Structured intelligence analysis.' } },
}
36 changes: 36 additions & 0 deletions apps/sim/tools/resemble/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { ToolResponse } from '@/tools/types'

export interface ResembleBaseParams {
apiKey: string
baseUrl?: string
maxWaitSeconds?: number
}

export interface ResembleDetectParams extends ResembleBaseParams {
url: string
runIntelligence?: boolean
audioSourceTracing?: boolean
visualize?: boolean
useReverseSearch?: boolean
useOodDetector?: boolean
zeroRetentionMode?: boolean
modelTypes?: string
}

export interface ResembleIntelligenceParams extends ResembleBaseParams {
url: string
structuredJson?: boolean
mediaType?: string
}

export interface ResembleWatermarkParams extends ResembleBaseParams {
url: string
strength?: number
customMessage?: string
}

export interface ResembleResponse extends ToolResponse {
output: {
result: any
}
}
63 changes: 63 additions & 0 deletions apps/sim/tools/resemble/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
export const DEFAULT_BASE_URL = 'https://app.resemble.ai/api/v2'
const TERMINAL = new Set(['completed', 'failed', 'error', 'cancelled', 'success'])

export function baseOf(params: any): string {
return (params?.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '')
}

export function authHeaders(params: any, extra?: Record<string, string>): Record<string, string> {
return {
Authorization: `Bearer ${params?.apiKey}`,
'Content-Type': 'application/json',
Accept: 'application/json',
...(extra || {}),
}
}

export function rItem(d: any): any {
return d && typeof d === 'object' && d.item && typeof d.item === 'object' ? d.item : d || {}
}

export function sanitize(d: any, n = 200): any {
if (Array.isArray(d)) return d.map((x) => sanitize(x, n))
if (d && typeof d === 'object') {
const o: any = {}
for (const k of Object.keys(d)) o[k] = sanitize(d[k], n)
return o
}
if (typeof d === 'string' && d.startsWith('data:') && d.length > n) {
return `<inline base64 omitted — ${d.length} chars>`
}
return d
}

export async function getJson(url: string, headers: Record<string, string>): Promise<any> {
const r = await fetch(url, { headers })
let j: any
try {
j = await r.json()
} catch {
j = { raw: await r.text() }
}
if (r.status >= 400) throw new Error((j && j.message) || `Resemble API error: HTTP ${r.status}`)
return j
}

export async function pollResource(
base: string,
path: string,
headers: Record<string, string>,
maxWaitSeconds = 120
): Promise<any> {
const deadline = Date.now() + Math.max(1, maxWaitSeconds) * 1000
let delay = 2000
let last = await getJson(`${base}${path}`, headers)
while (true) {
const s = (rItem(last).status || '').toString().toLowerCase()
if (!s || TERMINAL.has(s)) return last

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polling stops without status field

Medium Severity

pollResource treats a missing status as finished (!s), but watermark apply results signal in-progress work via watermarked_media: null without a status field. The fallback poll after apply can exit on the first GET and return success: true while the asset is still processing.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c2e3d83. Configure here.

Comment on lines +55 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No-status response exits the poll loop immediately

if (!s || TERMINAL.has(s)) return last exits when status is absent. Some intermediate async responses (e.g., queued state before a status key is populated) would cause pollResource to return the still-incomplete payload without retrying. A separate guard like if (TERMINAL.has(s)) return last (and leaving the no-status case to either continue or throw) would be safer.

if (Date.now() >= deadline) return last

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timeout returns incomplete success

High Severity

After maxWaitSeconds, pollResource returns the last non-terminal API payload without error. Detect and other tools always expose that as success: true, so long-running jobs can look completed while still processing and missing scores or media URLs.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c2e3d83. Configure here.

await new Promise((r) => setTimeout(r, delay))
delay = Math.min(10000, delay + 1000)
last = await getJson(`${base}${path}`, headers)
}
}
Loading