-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
838 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/* eslint-disable @typescript-eslint/no-empty-function */ | ||
|
||
import { | ||
createClient, | ||
DeepgramResponse, | ||
srt, | ||
SyncPrerecordedResponse, | ||
} from "@deepgram/sdk"; | ||
import { Context } from "hono"; | ||
import type { Bindings } from "../index"; | ||
import { cloneInnertube } from "../utils/innertube"; | ||
import { formatSRT } from "../utils/util"; | ||
|
||
interface Thumbnail { | ||
url: string; | ||
width: number; | ||
height: number; | ||
} | ||
|
||
export interface VideoInfo { | ||
title?: string; | ||
description?: string; | ||
duration?: string; | ||
author?: string; | ||
viewCount?: string; | ||
thumbnails?: Thumbnail[]; | ||
} | ||
|
||
export async function download(id: string, c: Context<{ Bindings: Bindings }>) { | ||
try { | ||
const deepgram = createClient(c.env.DEEPGRAM_API_KEY); | ||
console.log("Starting download for video ID:", id); | ||
|
||
const yt = await cloneInnertube(c); | ||
console.log("Innertube client created"); | ||
|
||
console.log("Fetching video info"); | ||
const video = await yt.getBasicInfo(id); | ||
|
||
console.log("Video info fetched successfully"); | ||
|
||
console.log("Choosing format"); | ||
const format = video.chooseFormat({ | ||
type: "audio", | ||
}); | ||
|
||
if (!format) { | ||
console.log("No suitable audio format found"); | ||
return { error: "No suitable audio format found" }; | ||
} | ||
|
||
console.log("Getting streaming data"); | ||
const stream = await yt.download(id, { | ||
type: "audio", | ||
}); | ||
|
||
let transcriptionResult: DeepgramResponse<SyncPrerecordedResponse>; | ||
try { | ||
transcriptionResult = await deepgram.listen.prerecorded.transcribeFile( | ||
stream as unknown as Buffer, | ||
{ | ||
model: "nova-2", | ||
smart_format: true, | ||
mimetype: format.mime_type, | ||
} | ||
); | ||
} catch (deepgramError: unknown) { | ||
console.error("Deepgram API error:", deepgramError); | ||
return { | ||
error: `Deepgram API error: ${ | ||
(deepgramError as Error).message || "Unknown error" | ||
}`, | ||
}; | ||
} | ||
|
||
if (transcriptionResult.error) { | ||
console.error("Transcription error:", transcriptionResult.error); | ||
return { error: `Transcription error: ${transcriptionResult.error}` }; | ||
} | ||
const subtitles = srt(transcriptionResult.result); | ||
const formattedSubtitles = formatSRT(subtitles); | ||
return { | ||
transcription: formattedSubtitles, | ||
videoInfo: { | ||
title: video?.basic_info?.title ?? "", | ||
description: video?.basic_info?.short_description ?? "", | ||
duration: video?.basic_info?.duration?.toString() ?? "", | ||
author: video?.basic_info?.author ?? "", | ||
viewCount: video?.basic_info?.view_count?.toString() ?? "", | ||
thumbnails: | ||
video?.basic_info?.thumbnail?.map((thumb) => ({ | ||
url: thumb?.url, | ||
width: thumb?.width, | ||
height: thumb?.height, | ||
})) ?? [], | ||
}, | ||
}; | ||
} catch (error: unknown) { | ||
console.error("Error in download task:", error); | ||
|
||
return { | ||
error: `An error occurred during download: ${ | ||
(error as Error).message || "Unknown error" | ||
}`, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { createOpenAI } from "@ai-sdk/openai"; | ||
import { generateObject } from "ai"; | ||
import { Context } from "hono"; | ||
|
||
import { z } from "zod"; | ||
import { Bindings } from ".."; | ||
|
||
const ChapterSchema = z.object({ | ||
title: z.string(), | ||
timestamp: z.string(), | ||
summary: z.string(), | ||
}); | ||
|
||
const ChaptersResponseSchema = z.object({ | ||
chapters: z.array(ChapterSchema), | ||
}); | ||
|
||
export type ChaptersResponse = z.infer<typeof ChaptersResponseSchema>; | ||
|
||
export async function generateChapters( | ||
c: Context<{ Bindings: Bindings }>, | ||
input: string | ||
): Promise<ChaptersResponse> { | ||
"use server"; | ||
const groq = createOpenAI({ | ||
baseURL: "https://api.groq.com/openai/v1", | ||
apiKey: c.env.GROQ_API_KEY, | ||
}); | ||
|
||
const { object } = await generateObject({ | ||
model: groq("llama-3.1-70b-versatile"), | ||
prompt: `You are an expert content analyzer. Your task is to create chapters and summaries for a YouTube video based on its transcription. Follow these guidelines: | ||
1. Analyze the transcription and create chapters. | ||
2. Each chapter should represent a distinct topic or section of the video. | ||
3. Chapter titles should be concise (3-7 words) and descriptive. | ||
4. Summaries should be upto 2-3 sentences long, capturing the main points of each chapter. | ||
5. make sure you cover all the topics in the video. | ||
Provide the output as a JSON array of chapter objects, each containing 'title', 'timestamp', and 'summary' fields. Here is the transcription: | ||
${input}`, | ||
schema: ChaptersResponseSchema, | ||
temperature: 0.8, | ||
}); | ||
|
||
return object; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { Pinecone } from "@pinecone-database/pinecone"; | ||
import { traceable } from "langsmith/traceable"; | ||
import { download, VideoInfo } from "./download"; | ||
import { Context } from "hono"; | ||
import { | ||
createPineconeIndex, | ||
updatePineconeWithTranscription, | ||
} from "../utils/rag-util"; | ||
import { generateChapters } from "./generate-chapters"; | ||
import { transcribe } from "./transcribe"; | ||
|
||
import type { Bindings } from "../index"; | ||
|
||
const processVideo = traceable( | ||
async (c: Context<{ Bindings: Bindings }>, id: string) => { | ||
const client = new Pinecone({ | ||
apiKey: c.env.PINECONE_API_KEY, | ||
}); | ||
|
||
try { | ||
// const cachedData = await c.env.KV.get<{ | ||
// transcription: string; | ||
// videoInfo: VideoInfo; | ||
// output: string; | ||
// }>(id); | ||
|
||
// if (cachedData) { | ||
// console.log("----- CACHED -----"); | ||
// const { transcription, videoInfo, output } = cachedData; | ||
// return { | ||
// transcription, | ||
// output, | ||
// status: "complete", | ||
// cached: true, | ||
// videoInfo, | ||
// }; | ||
// } | ||
|
||
// If not in cache, proceed with transcription | ||
const result = await download(id, c); | ||
// const { transcription, videoInfo } = result; | ||
|
||
// const output = await generateChapters(c, transcription); | ||
|
||
// await c.env.KV.put(id, JSON.stringify({ transcription, output })); | ||
// await createPineconeIndex(client, "video-transcriptions", 1024); | ||
// await updatePineconeWithTranscription( | ||
// client, | ||
// "video-transcriptions", | ||
// transcription, | ||
// id, | ||
// c | ||
// ); | ||
|
||
// return { | ||
// transcription, | ||
// output, | ||
// status: "complete", | ||
// cached: false, | ||
// videoInfo, | ||
|
||
// }; | ||
return result; | ||
} catch (error) { | ||
console.error("Error Processing Video", error); | ||
throw new Error("An error occurred during video processing"); | ||
} | ||
}, | ||
{ name: "process-video" } | ||
); | ||
|
||
export default processVideo; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { createClient, srt } from "@deepgram/sdk"; | ||
import fs from "fs"; | ||
import { Context } from "hono"; | ||
import { Bindings } from ".."; | ||
|
||
export async function transcribe( | ||
c: Context<{ Bindings: Bindings }>, | ||
audioPath: string | ||
) { | ||
const deepgram = createClient(c.env.DEEPGRAM_API_KEY); | ||
try { | ||
const fileData = fs.readFileSync(audioPath); | ||
const blob = new Blob([fileData], { type: "audio/webm" }); | ||
if (!(blob instanceof Blob)) throw new Error("No audio detected"); | ||
|
||
console.log("----- Transcribing audio -----"); | ||
const { result, error } = await deepgram.listen.prerecorded.transcribeFile( | ||
fileData, | ||
{ | ||
model: "nova-2", | ||
smart_format: true, | ||
} | ||
); | ||
if (error) console.error(error); | ||
|
||
fs.unlinkSync(audioPath); | ||
|
||
const subtitles = srt(result); | ||
const formattedSubtitles = formatSRT(subtitles); | ||
console.log("----- Subtitles generated -----"); | ||
return formattedSubtitles; | ||
} catch (error) { | ||
console.error("Error transcribing audio:", error); | ||
throw new Error("Error transcribing audio. Please try again later."); | ||
} | ||
} | ||
|
||
function formatSRT(srt: string): string { | ||
const lines = srt.split("\n"); | ||
let formatted = ""; | ||
let currentTime = ""; | ||
let isSubtitleText = false; | ||
|
||
for (const line of lines) { | ||
if (line.includes("-->")) { | ||
currentTime = formatTime(line?.split(" --> ")[0] ?? ""); | ||
isSubtitleText = true; | ||
} else if (line.trim() !== "" && isSubtitleText) { | ||
formatted += `${currentTime} ${line}\n`; | ||
isSubtitleText = false; | ||
} | ||
} | ||
|
||
return formatted.trim(); | ||
} | ||
|
||
function formatTime(time: string): string { | ||
const [hours, minutes, seconds] = time.split(/[:,.]/); | ||
return `${hours?.padStart(2, "0")}:${minutes?.padStart( | ||
2, | ||
"0" | ||
)}:${seconds?.padStart(2, "0")}`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Context } from "hono"; | ||
import { Cookie } from "tough-cookie"; | ||
import { Bindings } from "../.."; | ||
// biome-ignore lint/suspicious/noExplicitAny: <explanation> | ||
export function getCookie( | ||
name: string, | ||
c: Context<{ Bindings: Bindings }> | ||
): any; | ||
|
||
export function updateCookieValues( | ||
cookie: Cookie, | ||
values: Record<string, string | number | Date> | ||
): void; |
Oops, something went wrong.