diff --git a/packages/twenty-apps/public/call-recorder/SETUP.md b/packages/twenty-apps/public/call-recorder/SETUP.md index 5cf8ab2881..0ab5982473 100644 --- a/packages/twenty-apps/public/call-recorder/SETUP.md +++ b/packages/twenty-apps/public/call-recorder/SETUP.md @@ -16,7 +16,12 @@ and to receive recordings back. Two things must be configured: update, and cancel bots. 2. A **webhook** from Recall.ai back to your deployment, so the app learns when a recording is ready and can ingest it. -3. A **Gladia API key** in the Recall.ai dashboard (**Transcription → Gladia**, per region) — transcripts fail without it. + +Transcription runs on Recall.ai's own provider by default, which needs no extra +credentials. If a workspace switches `CALL_RECORDER_TRANSCRIPT_PROVIDER` to +Gladia, add a **Gladia API key** in the Recall.ai dashboard +(**Transcription → Gladia**) for every region in use, or transcripts for that +workspace fail. ## Server variables @@ -31,7 +36,8 @@ Set these on the application registration after installing | `RECALL_WEBHOOK_SECRET` | Yes | Svix signing secret (`whsec_…`) used to verify incoming Recall webhooks. | > **Bot behavior settings** (display name, join timing, lobby and leave -> timeouts) and the summary settings (`CALL_RECORDER_SUMMARY_ENABLED`, +> timeouts), the transcription provider (`CALL_RECORDER_TRANSCRIPT_PROVIDER`) +> and the summary settings (`CALL_RECORDER_SUMMARY_ENABLED`, > `CALL_RECORDER_ADDITIONAL_SUMMARY_PROMPT`) are **application variables** > that a workspace admin tunes inside the app — not server variables. diff --git a/packages/twenty-apps/public/call-recorder/package.json b/packages/twenty-apps/public/call-recorder/package.json index a961186231..cfaa9e8a5d 100644 --- a/packages/twenty-apps/public/call-recorder/package.json +++ b/packages/twenty-apps/public/call-recorder/package.json @@ -1,6 +1,6 @@ { "name": "@twentyhq/call-recorder", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "engines": { "node": "^24.5.0", diff --git a/packages/twenty-apps/public/call-recorder/src/__tests__/application-config.test.ts b/packages/twenty-apps/public/call-recorder/src/__tests__/application-config.test.ts new file mode 100644 index 0000000000..f2862729cd --- /dev/null +++ b/packages/twenty-apps/public/call-recorder/src/__tests__/application-config.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; + +import applicationConfig from 'src/application-config'; +import { CALL_RECORDER_TRANSCRIPT_PROVIDER_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-transcript-provider-env-var-name'; +import { RECALL_ASYNC_TRANSCRIPT_PROVIDERS } from 'src/logic-functions/constants/recall-async-transcript-providers'; + +describe('application config', () => { + it('offers every supported transcript provider as a select option', () => { + const options = + applicationConfig.config.applicationVariables?.[ + CALL_RECORDER_TRANSCRIPT_PROVIDER_ENV_VAR_NAME + ]?.options ?? []; + + expect(options.map((option) => option.value).sort()).toEqual( + Object.keys(RECALL_ASYNC_TRANSCRIPT_PROVIDERS).sort(), + ); + }); +}); diff --git a/packages/twenty-apps/public/call-recorder/src/application-config.ts b/packages/twenty-apps/public/call-recorder/src/application-config.ts index 94c2b7505a..b0c50cb48b 100644 --- a/packages/twenty-apps/public/call-recorder/src/application-config.ts +++ b/packages/twenty-apps/public/call-recorder/src/application-config.ts @@ -10,6 +10,7 @@ import { CALL_RECORDER_NAME_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/consta import { CALL_RECORDER_NOONE_JOINED_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recorder-noone-joined-timeout-seconds-app-variable-universal-identifier'; import { CALL_RECORDER_ADDITIONAL_SUMMARY_PROMPT_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recorder-additional-summary-prompt-app-variable-universal-identifier'; import { CALL_RECORDER_SUMMARY_ENABLED_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recorder-summary-enabled-app-variable-universal-identifier'; +import { CALL_RECORDER_TRANSCRIPT_PROVIDER_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recorder-transcript-provider-app-variable-universal-identifier'; import { CALL_RECORDER_USE_WORKSPACE_LOGO_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recorder-use-workspace-logo-app-variable-universal-identifier'; import { CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recorder-waiting-room-timeout-seconds-app-variable-universal-identifier'; import { CALL_RECORDER_BOT_IMAGE_BACKGROUND_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-bot-image-background-env-var-name'; @@ -22,6 +23,7 @@ import { CALL_RECORDER_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/log import { CALL_RECORDER_RECORDING_RETENTION_HOURS_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-recording-retention-hours-env-var-name'; import { CALL_RECORDER_ADDITIONAL_SUMMARY_PROMPT_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-additional-summary-prompt-env-var-name'; import { CALL_RECORDER_SUMMARY_ENABLED_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-summary-enabled-env-var-name'; +import { CALL_RECORDER_TRANSCRIPT_PROVIDER_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-transcript-provider-env-var-name'; import { CALL_RECORDER_USE_WORKSPACE_LOGO_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-use-workspace-logo-env-var-name'; import { CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS } from 'src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds'; import { CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds-env-var-name'; @@ -30,6 +32,7 @@ import { DEFAULT_CALL_RECORDER_JOIN_EARLY_MINUTES } from 'src/logic-functions/co import { DEFAULT_CALL_RECORDER_NAME } from 'src/logic-functions/constants/default-call-recorder-name'; import { DEFAULT_CALL_RECORDER_RECORDING_RETENTION_HOURS } from 'src/logic-functions/constants/default-call-recorder-recording-retention-hours'; import { DEFAULT_CALL_RECORDER_SUMMARY_ENABLED } from 'src/logic-functions/constants/default-call-recorder-summary-enabled'; +import { DEFAULT_CALL_RECORDER_TRANSCRIPT_PROVIDER } from 'src/logic-functions/constants/default-call-recorder-transcript-provider'; import { DEFAULT_CALL_RECORDER_USE_WORKSPACE_LOGO } from 'src/logic-functions/constants/default-call-recorder-use-workspace-logo'; import { DEFAULT_RECALL_REGION } from 'src/logic-functions/constants/default-recall-region'; import { RECALL_API_KEY_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-api-key-env-var-name'; @@ -88,6 +91,19 @@ export default defineApplication({ type: FieldType.NUMBER, value: CALL_RECORDER_EVERYONE_LEFT_TIMEOUT_SECONDS, }, + [CALL_RECORDER_TRANSCRIPT_PROVIDER_ENV_VAR_NAME]: { + universalIdentifier: + CALL_RECORDER_TRANSCRIPT_PROVIDER_APP_VARIABLE_UNIVERSAL_IDENTIFIER, + description: + 'Speech-to-text provider used to transcribe recordings once the call ends. Recall.ai transcription needs no extra setup. Gladia re-detects the spoken language per utterance, which transcribes mixed-language calls more accurately, but requires a Gladia API key in the Recall.ai dashboard (Transcription > Gladia) for each region in use.', + isSecret: false, + type: FieldType.SELECT, + options: [ + { label: 'Recall.ai', value: 'recallai_async' }, + { label: 'Gladia (multilingual)', value: 'gladia_v2_async' }, + ], + value: DEFAULT_CALL_RECORDER_TRANSCRIPT_PROVIDER, + }, [CALL_RECORDER_SUMMARY_ENABLED_ENV_VAR_NAME]: { universalIdentifier: CALL_RECORDER_SUMMARY_ENABLED_APP_VARIABLE_UNIVERSAL_IDENTIFIER, diff --git a/packages/twenty-apps/public/call-recorder/src/constants/call-recorder-transcript-provider-app-variable-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/call-recorder-transcript-provider-app-variable-universal-identifier.ts new file mode 100644 index 0000000000..9fc807dc0c --- /dev/null +++ b/packages/twenty-apps/public/call-recorder/src/constants/call-recorder-transcript-provider-app-variable-universal-identifier.ts @@ -0,0 +1,2 @@ +export const CALL_RECORDER_TRANSCRIPT_PROVIDER_APP_VARIABLE_UNIVERSAL_IDENTIFIER = + '598c1003-2767-4ccf-bc95-c251e6b92189'; diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-transcript-provider-env-var-name.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-transcript-provider-env-var-name.ts new file mode 100644 index 0000000000..057c95f8ca --- /dev/null +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-transcript-provider-env-var-name.ts @@ -0,0 +1,2 @@ +export const CALL_RECORDER_TRANSCRIPT_PROVIDER_ENV_VAR_NAME = + 'CALL_RECORDER_TRANSCRIPT_PROVIDER'; diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/default-call-recorder-transcript-provider.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/default-call-recorder-transcript-provider.ts new file mode 100644 index 0000000000..0edfff0463 --- /dev/null +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/default-call-recorder-transcript-provider.ts @@ -0,0 +1,4 @@ +import { type RecallAsyncTranscriptProvider } from 'src/logic-functions/types/recall-async-transcript-provider.type'; + +export const DEFAULT_CALL_RECORDER_TRANSCRIPT_PROVIDER: RecallAsyncTranscriptProvider = + 'recallai_async'; diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-async-transcript-provider.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-async-transcript-provider.ts deleted file mode 100644 index 3924ff0333..0000000000 --- a/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-async-transcript-provider.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const RECALL_ASYNC_TRANSCRIPT_PROVIDER = { - gladia_v2_async: { - language_config: { code_switching: true }, - }, -} as const; diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-async-transcript-providers.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-async-transcript-providers.ts new file mode 100644 index 0000000000..b04ae586fd --- /dev/null +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-async-transcript-providers.ts @@ -0,0 +1,4 @@ +export const RECALL_ASYNC_TRANSCRIPT_PROVIDERS = { + recallai_async: { language_code: 'auto' }, + gladia_v2_async: { language_config: { code_switching: true } }, +} as const; diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts index 7072e14cd5..2b9aa95867 100644 --- a/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts @@ -12,6 +12,7 @@ import { retrieveRecallTranscript } from 'src/logic-functions/recall-api/retriev import { scheduleRecallBot } from 'src/logic-functions/recall-api/schedule-recall-bot.util'; import { CALL_RECORDER_NAME_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-name-env-var-name'; import { CALL_RECORDER_RECORDING_RETENTION_HOURS_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-recording-retention-hours-env-var-name'; +import { CALL_RECORDER_TRANSCRIPT_PROVIDER_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-transcript-provider-env-var-name'; import { RECALL_API_KEY_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-api-key-env-var-name'; import { RECALL_REGION_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-region-env-var-name'; @@ -27,6 +28,7 @@ const ENV_VAR_NAMES = [ RECALL_REGION_ENV_VAR_NAME, CALL_RECORDER_NAME_ENV_VAR_NAME, CALL_RECORDER_RECORDING_RETENTION_HOURS_ENV_VAR_NAME, + CALL_RECORDER_TRANSCRIPT_PROVIDER_ENV_VAR_NAME, ] as const; const ORIGINAL_ENV_VALUES = ENV_VAR_NAMES.map( (envVarName) => [envVarName, process.env[envVarName]] as const, @@ -39,6 +41,7 @@ describe('recall bot api', () => { vi.useFakeTimers(); vi.setSystemTime(NOW); delete process.env[CALL_RECORDER_RECORDING_RETENTION_HOURS_ENV_VAR_NAME]; + delete process.env[CALL_RECORDER_TRANSCRIPT_PROVIDER_ENV_VAR_NAME]; process.env[RECALL_API_KEY_ENV_VAR_NAME] = 'recall-api-key'; process.env[RECALL_REGION_ENV_VAR_NAME] = 'ap-northeast-1'; process.env[CALL_RECORDER_NAME_ENV_VAR_NAME] = 'Call Recorder'; @@ -551,7 +554,7 @@ describe('recall bot api', () => { }); }); - it('creates an async transcript with the locked provider settings', async () => { + it('creates an async transcript with the default provider settings', async () => { fetchMock.mockResolvedValue({ ok: true, status: 201, @@ -567,6 +570,25 @@ describe('recall bot api', () => { 'https://ap-northeast-1.recall.ai/api/v1/recording/recall-recording-id/create_transcript/', expect.objectContaining({ method: 'POST' }), ); + expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ + provider: { recallai_async: { language_code: 'auto' } }, + diarization: { use_separate_streams_when_available: true }, + }); + }); + + it('creates an async transcript with the configured provider', async () => { + process.env[CALL_RECORDER_TRANSCRIPT_PROVIDER_ENV_VAR_NAME] = + 'gladia_v2_async'; + fetchMock.mockResolvedValue({ + ok: true, + status: 201, + json: async () => ({ id: 'recall-transcript-id' }), + }); + + await createAsyncRecallTranscript({ + externalRecordingId: 'recall-recording-id', + }); + expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ provider: { gladia_v2_async: { language_config: { code_switching: true } }, diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/create-async-recall-transcript.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/create-async-recall-transcript.util.ts index 8b748970b9..1e6bfd2ae2 100644 --- a/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/create-async-recall-transcript.util.ts +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/create-async-recall-transcript.util.ts @@ -1,9 +1,9 @@ import { isString } from '@sniptt/guards'; -import { RECALL_ASYNC_TRANSCRIPT_PROVIDER } from 'src/logic-functions/constants/recall-async-transcript-provider'; import { type RecallBotOperationFailure } from 'src/logic-functions/types/recall-bot-operation-result.type'; import { getRecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util'; import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-api-request.util'; +import { getRecallAsyncTranscriptProvider } from 'src/logic-functions/utils/get-recall-async-transcript-provider.util'; type CreateAsyncRecallTranscriptResult = | { ok: true; transcriptId: string } @@ -25,7 +25,7 @@ export const createAsyncRecallTranscript = async ({ path: `/recording/${externalRecordingId}/create_transcript/`, method: 'POST', body: { - provider: RECALL_ASYNC_TRANSCRIPT_PROVIDER, + provider: getRecallAsyncTranscriptProvider(), diarization: { use_separate_streams_when_available: true }, }, maxAttempts: 1, diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/types/recall-async-transcript-provider.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/recall-async-transcript-provider.type.ts new file mode 100644 index 0000000000..1d98e6661b --- /dev/null +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/recall-async-transcript-provider.type.ts @@ -0,0 +1,4 @@ +import { type RECALL_ASYNC_TRANSCRIPT_PROVIDERS } from 'src/logic-functions/constants/recall-async-transcript-providers'; + +export type RecallAsyncTranscriptProvider = + keyof typeof RECALL_ASYNC_TRANSCRIPT_PROVIDERS; diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/utils/__tests__/get-recall-async-transcript-provider.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/utils/__tests__/get-recall-async-transcript-provider.test.ts new file mode 100644 index 0000000000..c4f6dc98bb --- /dev/null +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/utils/__tests__/get-recall-async-transcript-provider.test.ts @@ -0,0 +1,29 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { CALL_RECORDER_TRANSCRIPT_PROVIDER_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-transcript-provider-env-var-name'; +import { getRecallAsyncTranscriptProvider } from 'src/logic-functions/utils/get-recall-async-transcript-provider.util'; + +describe('getRecallAsyncTranscriptProvider', () => { + beforeEach(() => { + delete process.env[CALL_RECORDER_TRANSCRIPT_PROVIDER_ENV_VAR_NAME]; + }); + + afterEach(() => { + delete process.env[CALL_RECORDER_TRANSCRIPT_PROVIDER_ENV_VAR_NAME]; + }); + + it('defaults to Recall.ai transcription when unset', () => { + expect(getRecallAsyncTranscriptProvider()).toEqual({ + recallai_async: { language_code: 'auto' }, + }); + }); + + it('falls back to the default for an unsupported provider', () => { + process.env[CALL_RECORDER_TRANSCRIPT_PROVIDER_ENV_VAR_NAME] = + 'deepgram_async'; + + expect(getRecallAsyncTranscriptProvider()).toEqual({ + recallai_async: { language_code: 'auto' }, + }); + }); +}); diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/utils/get-recall-async-transcript-provider.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/utils/get-recall-async-transcript-provider.util.ts new file mode 100644 index 0000000000..76a0bdaaeb --- /dev/null +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/utils/get-recall-async-transcript-provider.util.ts @@ -0,0 +1,23 @@ +import { CALL_RECORDER_TRANSCRIPT_PROVIDER_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-transcript-provider-env-var-name'; +import { DEFAULT_CALL_RECORDER_TRANSCRIPT_PROVIDER } from 'src/logic-functions/constants/default-call-recorder-transcript-provider'; +import { RECALL_ASYNC_TRANSCRIPT_PROVIDERS } from 'src/logic-functions/constants/recall-async-transcript-providers'; +import { type RecallAsyncTranscriptProvider } from 'src/logic-functions/types/recall-async-transcript-provider.type'; +import { getApplicationVariableValue } from 'src/logic-functions/utils/get-application-variable-value.util'; +import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; + +const isRecallAsyncTranscriptProvider = ( + value: unknown, +): value is RecallAsyncTranscriptProvider => + isNonEmptyString(value) && value in RECALL_ASYNC_TRANSCRIPT_PROVIDERS; + +export const getRecallAsyncTranscriptProvider = () => { + const rawValue = getApplicationVariableValue( + CALL_RECORDER_TRANSCRIPT_PROVIDER_ENV_VAR_NAME, + )?.trim(); + + const providerId = isRecallAsyncTranscriptProvider(rawValue) + ? rawValue + : DEFAULT_CALL_RECORDER_TRANSCRIPT_PROVIDER; + + return { [providerId]: RECALL_ASYNC_TRANSCRIPT_PROVIDERS[providerId] }; +};