-
Notifications
You must be signed in to change notification settings - Fork 3.7k
feat(integrations): add Resemble Detect + Intelligence block #4925
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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' }, | ||
| }, | ||
| // 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' }, | ||
| }, | ||
| } | ||
| 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).' } }, | ||
| } |
| 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' |
| 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']) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 */ | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wrong intelligence poll URLMedium Severity When media intelligence is still in a non-terminal state, polling uses Reviewed by Cursor Bugbot for commit c2e3d83. Configure here.
Comment on lines
+41
to
+45
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When The same pattern appears in |
||
| } | ||
| return { success: true, output: { result: sanitize(data) } } | ||
| }, | ||
| outputs: { result: { type: 'json', description: 'Structured intelligence analysis.' } }, | ||
| } | ||
| 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 | ||
| } | ||
| } |
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Polling stops without status fieldMedium Severity
Additional Locations (1)Reviewed by Cursor Bugbot for commit c2e3d83. Configure here.
Comment on lines
+55
to
+57
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if (Date.now() >= deadline) return last | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Timeout returns incomplete successHigh Severity After 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) | ||
| } | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
structuredJsonparameter has no corresponding UI controlintelligenceTooldeclares astructuredJsonboolean param that controls whether the API returns structured JSON fields. Because no subBlock exists for it in the block definition, the value is alwaysundefinedin the UI, which causes the body builder to evaluatep.structuredJson !== falseastrue— permanently fixing it to structured mode. Users who need raw/unstructured output from the Intelligence operation cannot opt out from the block UI.