Skip to content

Commit 38506f8

Browse files
Merge pull request #1839 from CapSoftware/ai-generation-language-preferences
feat: add AI generation language preferences
2 parents 6859fb4 + 7fc140a commit 38506f8

11 files changed

Lines changed: 513 additions & 63 deletions

File tree

apps/web/__tests__/unit/generate-ai-title.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ vi.mock("workflow", () => ({
3131

3232
vi.mock("server-only", () => ({}));
3333

34-
import { shouldReplaceVideoTitle } from "@/workflows/generate-ai";
34+
import {
35+
getAiLanguageInstruction,
36+
shouldReplaceVideoTitle,
37+
} from "@/workflows/generate-ai";
3538

3639
describe("shouldReplaceVideoTitle", () => {
3740
it("replaces default Cap titles", () => {
@@ -84,3 +87,15 @@ describe("shouldReplaceVideoTitle", () => {
8487
).toBe(false);
8588
});
8689
});
90+
91+
describe("getAiLanguageInstruction", () => {
92+
it("uses transcript language when auto-detect is selected", () => {
93+
expect(getAiLanguageInstruction("auto")).toContain(
94+
"same language as the transcript",
95+
);
96+
});
97+
98+
it("uses the selected language name", () => {
99+
expect(getAiLanguageInstruction("es")).toContain("Spanish");
100+
});
101+
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
vi.mock("@cap/database", () => ({
4+
db: vi.fn(),
5+
}));
6+
7+
vi.mock("@cap/env", () => ({
8+
serverEnv: vi.fn(() => ({})),
9+
}));
10+
11+
vi.mock("@cap/utils", () => ({
12+
userIsPro: vi.fn(),
13+
}));
14+
15+
vi.mock("@cap/web-backend", () => ({
16+
Storage: {},
17+
}));
18+
19+
vi.mock("@deepgram/sdk", () => ({
20+
createClient: vi.fn(),
21+
}));
22+
23+
vi.mock("@/lib/audio-enhance", () => ({
24+
ENHANCED_AUDIO_CONTENT_TYPE: "audio/mpeg",
25+
ENHANCED_AUDIO_EXTENSION: "mp3",
26+
enhanceAudioFromUrl: vi.fn(),
27+
}));
28+
29+
vi.mock("@/lib/audio-extract", () => ({
30+
checkHasAudioTrack: vi.fn(),
31+
extractAudioFromUrl: vi.fn(),
32+
}));
33+
34+
vi.mock("@/lib/generate-ai", () => ({
35+
startAiGeneration: vi.fn(),
36+
}));
37+
38+
vi.mock("@/lib/media-client", () => ({
39+
checkHasAudioTrackViaMediaServer: vi.fn(),
40+
extractAudioViaMediaServer: vi.fn(),
41+
isMediaServerConfigured: vi.fn(),
42+
probeVideoViaMediaServer: vi.fn(),
43+
}));
44+
45+
vi.mock("@/lib/server", () => ({
46+
runPromise: vi.fn(),
47+
}));
48+
49+
vi.mock("@/lib/transcribe-utils", () => ({
50+
formatToWebVTT: vi.fn(),
51+
}));
52+
53+
vi.mock("@/lib/video-storage", () => ({
54+
decodeStorageVideo: vi.fn(),
55+
}));
56+
57+
vi.mock("workflow", () => ({
58+
FatalError: class FatalError extends Error {},
59+
}));
60+
61+
import {
62+
AI_GENERATION_LANGUAGES,
63+
isAiGenerationLanguage,
64+
parseAiGenerationLanguage,
65+
} from "@cap/web-domain";
66+
import { getDeepgramTranscriptionOptions } from "@/workflows/transcribe";
67+
68+
describe("AI generation language support", () => {
69+
it("does not expose unsupported transcription languages", () => {
70+
expect(AI_GENERATION_LANGUAGES).not.toHaveProperty("pa");
71+
expect(isAiGenerationLanguage("pa")).toBe(false);
72+
expect(parseAiGenerationLanguage("pa")).toBe("auto");
73+
});
74+
75+
it("constrains Deepgram auto-detection to detectable languages", () => {
76+
expect(getDeepgramTranscriptionOptions("auto")).toMatchObject({
77+
model: "nova-3",
78+
detect_language: expect.arrayContaining(["en", "es", "zh"]),
79+
});
80+
});
81+
82+
it("passes explicit languages to Deepgram", () => {
83+
expect(getDeepgramTranscriptionOptions("zh")).toMatchObject({
84+
model: "nova-3",
85+
language: "zh",
86+
});
87+
});
88+
});

apps/web/actions/organization/settings.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { db } from "@cap/database";
44
import { getCurrentUser } from "@cap/database/auth/session";
55
import { organizations } from "@cap/database/schema";
66
import { userIsPro } from "@cap/utils";
7+
import {
8+
AI_GENERATION_LANGUAGE_AUTO,
9+
type AiGenerationLanguage,
10+
isAiGenerationLanguage,
11+
} from "@cap/web-domain";
712
import { eq } from "drizzle-orm";
813
import { revalidatePath } from "next/cache";
914
import { requireOrganizationSettingsManager } from "./authorization";
@@ -17,6 +22,7 @@ type OrganizationSettingsInput = {
1722
disableComments?: boolean;
1823
hideShareableLinkCapLogo?: boolean;
1924
shareableLinkUseOrganizationIcon?: boolean;
25+
aiGenerationLanguage?: AiGenerationLanguage;
2026
};
2127

2228
const proOrganizationSettingKeys = [
@@ -25,8 +31,21 @@ const proOrganizationSettingKeys = [
2531
"disableTranscript",
2632
"hideShareableLinkCapLogo",
2733
"shareableLinkUseOrganizationIcon",
34+
"aiGenerationLanguage",
2835
] as const satisfies readonly (keyof OrganizationSettingsInput)[];
2936

37+
const defaultProOrganizationSettings = {
38+
disableSummary: false,
39+
disableChapters: false,
40+
disableTranscript: false,
41+
hideShareableLinkCapLogo: false,
42+
shareableLinkUseOrganizationIcon: false,
43+
aiGenerationLanguage: AI_GENERATION_LANGUAGE_AUTO,
44+
} as const satisfies Pick<
45+
Required<OrganizationSettingsInput>,
46+
(typeof proOrganizationSettingKeys)[number]
47+
>;
48+
3049
const preserveProSettings = (
3150
submittedSettings: OrganizationSettingsInput,
3251
existingSettings: OrganizationSettingsInput | null | undefined,
@@ -35,7 +54,7 @@ const preserveProSettings = (
3554
...Object.fromEntries(
3655
proOrganizationSettingKeys.map((key) => [
3756
key,
38-
existingSettings?.[key] ?? false,
57+
existingSettings?.[key] ?? defaultProOrganizationSettings[key],
3958
]),
4059
),
4160
});
@@ -53,6 +72,13 @@ export async function updateOrganizationSettings(
5372
throw new Error("Settings are required");
5473
}
5574

75+
if (
76+
settings.aiGenerationLanguage !== undefined &&
77+
!isAiGenerationLanguage(settings.aiGenerationLanguage)
78+
) {
79+
throw new Error("Unsupported AI generation language");
80+
}
81+
5682
if (!user.activeOrganizationId) {
5783
throw new Error("Organization not found");
5884
}
Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,4 @@
1-
export const SUPPORTED_LANGUAGES = {
2-
en: "English",
3-
es: "Spanish",
4-
fr: "French",
5-
de: "German",
6-
pt: "Portuguese",
7-
it: "Italian",
8-
nl: "Dutch",
9-
pl: "Polish",
10-
sk: "Slovak",
11-
ru: "Russian",
12-
tr: "Turkish",
13-
ja: "Japanese",
14-
ko: "Korean",
15-
zh: "Chinese (Simplified)",
16-
ar: "Arabic",
17-
hi: "Hindi",
18-
bn: "Bengali",
19-
ta: "Tamil",
20-
te: "Telugu",
21-
mr: "Marathi",
22-
gu: "Gujarati",
23-
pa: "Punjabi",
24-
ur: "Urdu",
25-
fa: "Persian",
26-
he: "Hebrew",
27-
} as const;
28-
29-
export type LanguageCode = keyof typeof SUPPORTED_LANGUAGES;
1+
export {
2+
type LanguageCode,
3+
SUPPORTED_LANGUAGES,
4+
} from "@cap/web-domain";

0 commit comments

Comments
 (0)