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
11 changes: 10 additions & 1 deletion components/pages/recording/RecordingDesktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default function RecordingDesktop({
transcription,
title,
_creationTime,
summaryAudioUrl,
} = note;
const [originalIsOpen, setOriginalIsOpen] = useState<boolean>(true);

Expand Down Expand Up @@ -85,7 +86,15 @@ export default function RecordingDesktop({
<div className="grid h-full w-full grid-cols-2 px-[30px] lg:px-[45px]">
<div className="relative min-h-[70vh] w-full border-r px-5 py-3 text-justify text-xl font-[300] leading-[114.3%] tracking-[-0.6px] lg:text-2xl">
{transcription ? (
<div className="">{originalIsOpen ? transcription : summary}</div>
<div>
<div className="">{originalIsOpen ? transcription : summary}</div>
{!originalIsOpen && summaryAudioUrl && (
<div className="mt-6 flex items-center gap-3">
<p className="text-sm opacity-60">Listen to summary</p>
<audio controls src={summaryAudioUrl} className="h-8" />
</div>
)}
</div>
) : (
// Loading state for transcript
<ul className="animate-pulse space-y-3">
Expand Down
8 changes: 7 additions & 1 deletion components/pages/recording/RecordingMobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function RecordingMobile({
note: Doc<'notes'>;
actionItems: Doc<'actionItems'>[];
}) {
const { summary, transcription, title, _creationTime } = note;
const { summary, transcription, title, _creationTime, summaryAudioUrl } = note;
const [transcriptOpen, setTranscriptOpen] = useState<boolean>(true);
const [summaryOpen, setSummaryOpen] = useState<boolean>(false);
const [actionItemOpen, setActionItemOpen] = useState<boolean>(false);
Expand Down Expand Up @@ -78,6 +78,12 @@ export default function RecordingMobile({
{summaryOpen && (
<div className="relative mt-2 min-h-[70vh] w-full px-4 py-3 text-justify font-light">
{summary}
{summaryAudioUrl && (
<div className="mt-4 flex items-center gap-3">
<p className="text-sm opacity-60">Listen to summary</p>
<audio controls src={summaryAudioUrl} className="h-8" />
</div>
)}
</div>
)}
{actionItemOpen && (
Expand Down
69 changes: 69 additions & 0 deletions convex/camb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
("use node");

import { internalAction, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";

const CAMB_API_BASE = "https://client.camb.ai/apis";

function getCambApiKey(): string {
const key = process.env.CAMB_API_KEY;
if (!key) {
throw new Error("CAMB_API_KEY environment variable is not set");
}
return key;
}

// Generate TTS audio of a note summary using CAMB AI
export const generateSpeech = internalAction({
args: {
id: v.id("notes"),
text: v.string(),
},
handler: async (ctx, args) => {
const apiKey = getCambApiKey();

const res = await fetch(`${CAMB_API_BASE}/tts-stream`, {
method: "POST",
headers: {
"x-api-key": apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: args.text,
voice_id: 147320,
language: "en-us",
speech_model: "mars-flash",
output_configuration: { format: "wav" },
}),
});

if (!res.ok) {
throw new Error(`CAMB TTS failed: ${await res.text()}`);
}

const audioBuffer = await res.arrayBuffer();
const blob = new Blob([audioBuffer], { type: "audio/wav" });

const storageId = await ctx.storage.store(blob);

await ctx.runMutation(internal.camb.saveSummaryAudio, {
id: args.id,
summaryAudioFileId: storageId,
});
},
});

export const saveSummaryAudio = internalMutation({
args: {
id: v.id("notes"),
summaryAudioFileId: v.id("_storage"),
},
handler: async (ctx, args) => {
const url = await ctx.storage.getUrl(args.summaryAudioFileId);
await ctx.db.patch(args.id, {
summaryAudioFileId: args.summaryAudioFileId,
summaryAudioUrl: url ?? undefined,
});
},
});
2 changes: 2 additions & 0 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export default defineSchema({
transcription: v.optional(v.string()),
summary: v.optional(v.string()),
embedding: v.optional(v.array(v.float64())),
summaryAudioFileId: v.optional(v.id('_storage')),
summaryAudioUrl: v.optional(v.string()),
generatingTranscript: v.boolean(),
generatingTitle: v.boolean(),
generatingActionItems: v.boolean(),
Expand Down
8 changes: 8 additions & 0 deletions convex/together.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ export const saveSummary = internalMutation({
await ctx.db.patch(id, {
generatingActionItems: false,
});

// Generate TTS audio of the summary using CAMB AI
if (summary && summary !== 'Summary failed to generate') {
await ctx.scheduler.runAfter(0, internal.camb.generateSpeech, {
id,
text: summary,
});
}
},
});

Expand Down