Skip to content

Commit

Permalink
works
Browse files Browse the repository at this point in the history
  • Loading branch information
TheRohit committed Sep 24, 2024
1 parent afb3c5d commit bf3da6b
Show file tree
Hide file tree
Showing 15 changed files with 838 additions and 9 deletions.
Binary file modified bun.lockb
Binary file not shown.
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,20 @@
"deploy": "wrangler deploy --minify src/index.ts"
},
"dependencies": {
"hono": "^4.5.10"
"@ai-sdk/openai": "^0.0.60",
"@deepgram/sdk": "^3.6.0",
"@distube/ytdl-core": "^4.14.4",
"@langchain/cohere": "^0.3.0",
"@pinecone-database/pinecone": "^3.0.3",
"ai": "^3.3.37",
"hono": "^4.5.10",
"langchain": "^0.3.2",
"langsmith": "^0.1.55",
"set-cookie-parser": "^2.7.0",
"youtubei.js": "^10.4.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240821.1",
"wrangler": "^3.57.2"
"wrangler": "3.78.2"
}
}
28 changes: 26 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import type { KVNamespace, R2Bucket } from "@cloudflare/workers-types";
import { Hono } from "hono";
import type { KVNamespace } from "@cloudflare/workers-types";
import { download } from "./tasks/download";

type Bindings = {
export type Bindings = {
KV: KVNamespace;
R2: R2Bucket;
PINECONE_API_KEY: string;
COHERE_API_KEY: string;
GROQ_API_KEY: string;
DEEPGRAM_API_KEY: string;
COOKIE: string;
};

const app = new Hono<{ Bindings: Bindings }>();
Expand All @@ -27,4 +34,21 @@ app.get("/get/:key", async (c) => {
return c.text(value);
});

app.get("/process-video/:id", async (c, env) => {
try {
const id = c.req.param("id");
if (!id) {
return c.json({ error: "Missing video ID" }, 400);
}
const result = await download(id, c);
return c.json(result);
} catch (error) {
console.error("Error processing video:", error);
return c.json(
{ error: "An error occurred while processing the video" },
500
);
}
});

export default app;
107 changes: 107 additions & 0 deletions src/tasks/download.ts
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"
}`,
};
}
}
45 changes: 45 additions & 0 deletions src/tasks/generate-chapters.ts
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;
}
72 changes: 72 additions & 0 deletions src/tasks/process-video.ts
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;
63 changes: 63 additions & 0 deletions src/tasks/transcribe.ts
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")}`;
}
13 changes: 13 additions & 0 deletions src/utils/cookies/cookie-manager.d.ts
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;
Loading

0 comments on commit bf3da6b

Please sign in to comment.