This commit is contained in:
Omer Sabic 2024-04-24 22:49:59 +02:00
parent adeb965f3e
commit 08fff603b5
17 changed files with 10351 additions and 682 deletions

9334
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -28,12 +28,14 @@
"xo": "^0.56.0" "xo": "^0.56.0"
}, },
"dependencies": { "dependencies": {
"@fastify/cookie": "^9.3.1",
"@fastify/cors": "^8.4.2", "@fastify/cors": "^8.4.2",
"@fastify/oauth2": "^7.8.0", "@fastify/oauth2": "^7.8.0",
"drizzle-orm": "^0.29.1", "drizzle-orm": "^0.29.1",
"fastify": "^4.25.0", "fastify": "^4.25.0",
"fastify-plugin": "^4.5.1", "fastify-plugin": "^4.5.1",
"googleapis": "^134.0.0", "googleapis": "^134.0.0",
"openai": "^4.38.2",
"pg": "^8.11.3", "pg": "^8.11.3",
"redis": "^4.6.11", "redis": "^4.6.11",
"simple-get": "^4.0.1", "simple-get": "^4.0.1",

4
src/fastify.d.ts vendored
View File

@ -10,8 +10,8 @@ declare module "fastify" {
session?: { session?: {
id: string, id: string,
user_id: string, user_id: string,
google_access_token: string, access_token: string,
google_refresh_token: string, refresh_token: string,
expires_at: Date expires_at: Date
} }
} }

View File

@ -1,10 +1,10 @@
import { initDb } from "./db/index.js"; import { initDb } from "./db/index.js";
import { channelRoutes, authRoutes } from "./routes/index.js"; import { channelRoutes, authRoutes, videoRoutes, meRoutes } from "./routes/index.js";
import { env, Logger, Redis } from "./utils/index.js"; import { env, Logger, Redis } from "./utils/index.js";
import fastify from "fastify"; import fastify from "fastify";
import { middleware } from "./modules/middleware.js"; import { middleware } from "./modules/middleware.js";
import oauth from '@fastify/oauth2'; import oauth from '@fastify/oauth2';
import { videoRoutes } from "./routes/videos.js"; import fastifyCookie from "@fastify/cookie";
const API_VERSION = "v1"; const API_VERSION = "v1";
@ -17,6 +17,12 @@ export const main = async () => {
await initDb(); await initDb();
// await Redis.initialize(); // await Redis.initialize();
server.register(fastifyCookie, {
secret: "my-secret", // for cookies signature
hook: 'preParsing', // set to false to disable cookie autoparsing or set autoparsing on any of the following hooks: 'onRequest', 'preParsing', 'preHandler', 'preValidation'. default: 'onRequest'
parseOptions: {}, // options for parsing cookies
});
server.register(middleware); server.register(middleware);
server.register(import("@fastify/cors"), { server.register(import("@fastify/cors"), {
maxAge: 600, maxAge: 600,
@ -40,14 +46,13 @@ export const main = async () => {
pkce: 'S256', pkce: 'S256',
// register a fastify url to start the redirect flow // register a fastify url to start the redirect flow
startRedirectPath: '/auth/google', startRedirectPath: '/auth/google',
// facebook redirect here after the user login // google redirect here after the user login
callbackUri: 'http://localhost:3000/auth/google/callback' callbackUri: `${env.PUBLIC_API_URL}/auth/google/callback`
}) });
// Routes // Routes
server.register(channelRoutes, { server.register(channelRoutes, {
prefix: `/`, prefix: `/channels`,
}); });
server.register(videoRoutes, { server.register(videoRoutes, {
@ -58,8 +63,12 @@ export const main = async () => {
prefix: `/auth`, prefix: `/auth`,
}); });
server.register(meRoutes, {
prefix: `/me`
});
server.get("/hello", (req, res) => { server.get("/hello", (req, res) => {
res.send("world"); res.send({message: "world", cookies: req.cookies});
}) })
server.listen({ host: env.HOST, port: env.PORT }, (error, address) => { server.listen({ host: env.HOST, port: env.PORT }, (error, address) => {

View File

@ -21,14 +21,17 @@ const authMiddleware = fp(
async (fastify, _options) => { async (fastify, _options) => {
fastify.addHook("preValidation", async (request, response) => { fastify.addHook("preValidation", async (request, response) => {
try { try {
if (!request.headers.authorization || !request.headers.authorization.startsWith("Bearer")) { // if (!request.headers.authorization || !request.headers.authorization.startsWith("Bearer")) {
response.status(401).send("Missing authentication token"); if(!request.cookies.token) {
response.status(401).send({ success: false, message: "Missing authentication token" });
return; return;
} }
const token = request.headers.authorization.split(" ")[1]; // const token = request.headers.authorization.split(" ")[1];
const token = request.cookies.token;
const session = await db.select().from(sessions).where(eq(sessions.id, token)); const session = await db.select().from(sessions).where(eq(sessions.id, token));
if(session.length == 0) { if(session.length == 0) {
response.status(401).send("Invalid authentication token"); response.status(401).send({ success: false, message: "Invalid authentication token" });
return; return;
} }
// console.log(token); // console.log(token);

View File

@ -1,11 +1,12 @@
import sget from 'simple-get'; import sget from 'simple-get';
import { createAuthToken as createSession } from '../utils/token.js'; import { createSession as createSession } from '../utils/token.js';
import { google } from 'googleapis'; import { google } from 'googleapis';
import { getChannelInfo, getUserInfo } from '../utils/youtube.js'; import { getChannelInfo, getUserInfo } from '../utils/youtube.js';
import { db } from '../db/index.js'; import { db } from '../db/index.js';
import { users as usersTable } from '../db/schemas.js'; import { users as usersTable } from '../db/schemas.js';
import { userInfo } from 'os'; import { userInfo } from 'os';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { env } from '../utils/env.js';
/** @typedef {import("fastify").FastifyInstance} FastifyInstance */ /** @typedef {import("fastify").FastifyInstance} FastifyInstance */
/** /**
@ -47,15 +48,22 @@ export const authRoutes = (fastify, _, done) => {
} }
} }
const session = await createSession(user.id, { const {session_id} = await createSession(user.id, {
access_token: token.access_token, access_token: token.access_token,
refresh_token: token.refresh_token, refresh_token: token.refresh_token,
expires_at: new Date(token.expires_at) expires_at: new Date(token.expires_at)
}); });
response.send({ response.setCookie("token", session_id, {
token: session.session_id httpOnly: true,
}) path: "/",
// sameSite: false,
maxAge: 1000 * 60 * 60 * 24 * 7,
domain: "api.omersabic.com"
}).redirect(env.FRONTEND_URL);
// response.send({
// token: session_id
// });
return; return;
} catch (e) { } catch (e) {
console.log(e); console.log(e);

33
src/routes/blog.js Normal file
View File

@ -0,0 +1,33 @@
import { authMiddleware } from "../modules/middleware.js";
import { getAccessToken, getChannelInfo } from "../utils/youtube.js";
/**
*
* @param {import("fastify").FastifyInstance} fastify
* @param {unknown} _
* @param {() => void} done
*/
export const channelRoutes = (fastify, _, done) => {
fastify.register(authMiddleware);
fastify.get("/", async (request, response) => {
try {
const mine = request.query.mine || true;
const blog_id = request.query
if(mine && request)
const access_token = await getAccessToken(fastify, request);
const channel = await getChannelInfo(access_token);
response.send({
success: true,
channel
});
} catch (e) {
console.log(e);
}
});
done();
};

View File

@ -1,7 +1,7 @@
/** @typedef {import("fastify").FastifyInstance} FastifyInstance */ /** @typedef {import("fastify").FastifyInstance} FastifyInstance */
import { authMiddleware } from "../modules/middleware.js"; import { authMiddleware } from "../modules/middleware.js";
import { getChannelInfo } from "../utils/youtube.js"; import { getAccessToken, getChannelInfo } from "../utils/youtube.js";
/** /**
* *
@ -14,14 +14,9 @@ export const channelRoutes = (fastify, _, done) => {
fastify.get("/", async (request, response) => { fastify.get("/", async (request, response) => {
try { try {
console.log(request.session); const access_token = await getAccessToken(fastify, request);
const { token } = await fastify.googleOAuth2.getNewAccessTokenUsingRefreshToken({ const channel = await getChannelInfo(access_token);
refresh_token: request.session.google_refresh_token,
expires_at: request.session.expires_at
});
console.log(token);
const channel = await getChannelInfo(token.access_token);
response.send({ response.send({
success: true, success: true,

View File

@ -1,2 +1,4 @@
export * from "./channels.js"; export * from "./channels.js";
export * from "./auth.js"; export * from "./auth.js";
export * from "./videos.js";
export * from "./me.js";

31
src/routes/me.js Normal file
View File

@ -0,0 +1,31 @@
/** @typedef {import("fastify").FastifyInstance} FastifyInstance */
import { eq } from "drizzle-orm";
import { db } from "../db/index.js";
import { users } from "../db/schemas.js";
import { authMiddleware } from "../modules/middleware.js";
/**
*
* @param {FastifyInstance} fastify
* @param {unknown} _
* @param {() => void} done
*/
export const meRoutes = (fastify, _, done) => {
fastify.register(authMiddleware);
fastify.get("/", async (request, response) => {
try {
const user = await db.select().from(users).where(eq(users.id, request.session.user_id));
response.send({
success: true,
user: user[0]
});
} catch (e) {
console.log(e);
}
});
done();
};

View File

@ -4,7 +4,8 @@ import { eq } from "drizzle-orm";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
import { users } from "../db/schemas.js"; import { users } from "../db/schemas.js";
import { authMiddleware } from "../modules/middleware.js"; import { authMiddleware } from "../modules/middleware.js";
import { getCaptionText, getNewToken, getVideoCaptions, getVideosFromPlaylist } from "../utils/youtube.js"; import { getAccessToken, getCaptionText, getVideoCaptions, getVideosFromPlaylist } from "../utils/youtube.js";
import { createBlogFromCaptions } from "../utils/ai.js";
/** /**
* *
@ -17,7 +18,7 @@ export const videoRoutes = (fastify, _, done) => {
fastify.get("/", async (request, response) => { fastify.get("/", async (request, response) => {
try { try {
const token = await getNewToken(fastify.googleOAuth2, request.session, {}); const token = await getAccessToken(fastify, request);
const [user] = await db.select().from(users).where(eq(users.id, request.session.user_id)); const [user] = await db.select().from(users).where(eq(users.id, request.session.user_id));
const videos = await getVideosFromPlaylist(token.access_token, user.uploads_playlist_id); const videos = await getVideosFromPlaylist(token.access_token, user.uploads_playlist_id);
@ -33,7 +34,7 @@ export const videoRoutes = (fastify, _, done) => {
fastify.get("/captions/:video_id", async (request, response) => { fastify.get("/captions/:video_id", async (request, response) => {
try { try {
const token = await getNewToken(fastify.googleOAuth2, request.session, {}); const token = await getAccessToken(fastify, request);
const captions_list = await getVideoCaptions(token.access_token, request.params.video_id); const captions_list = await getVideoCaptions(token.access_token, request.params.video_id);
const caption = captions_list.filter(x => x.snippet.language === "en"); const caption = captions_list.filter(x => x.snippet.language === "en");
@ -57,5 +58,28 @@ export const videoRoutes = (fastify, _, done) => {
} }
}) })
fastify.get("/blogify/:video_id", async (request, response) => {
try {
const token = await getAccessToken(fastify, request);
const captions_list = await getVideoCaptions(token.access_token, request.params.video_id);
const caption = captions_list.filter(x => x.snippet.language === "en");
if (caption.length === 0) {
response.send({
success: false,
message: "Couldn't find caption"
});
return;
}
const caption_text = await getCaptionText(token.access_token, caption[0].id);
const ai_response = await createBlogFromCaptions(caption_text);
response.send(ai_response);
} catch (e) {
console.log(e);
}
});
done(); done();
}; };

65
src/utils/ai.js Normal file
View File

@ -0,0 +1,65 @@
const defaultModel = '@hf/mistralai/mistral-7b-instruct-v0.2';
async function cf_prompt(prompt, model = defaultModel) {
const options = {
method: 'POST',
headers: { Authorization: 'Bearer oJh-qUnkPmsaaE7mfor617dasBMnH_t9QFkSc5L2' },
body: '{ "messages": [{ "role": "system", "content": "You are a friendly assistant" }, { "role": "user", "content": "Why is pizza so good" }]}'
};
const res = await fetch('https://api.cloudflare.com/client/v4/accounts/f79c2f6c3ee16c813cbc853bc7e16166/ai/run/@hf/mistralai/mistral-7b-instruct-v0.2', options)
.then(response => response.json())
.catch(err => console.error(err));
console.log(res);
return res;
}
async function promptGPT(prompt, model = "gpt-3-turbo") {
const options = {
method: 'POST',
headers: { Authorization: 'Bearer pk-hevZzgTruHUMITZDvBqcIaLXWYtkxuTLkQkYecEfszfNHNBT', "Content-Type": "application/json" },
body: JSON.stringify({
"model": "pai-001-light",
"max_tokens": 512,
"messages": [
{
"role": "user",
"content": prompt
}
]
})
};
console.log("prompting pai-001...")
const res = await fetch('https://api.pawan.krd/pai-001/v1/chat/completions', options)
.then(response => {
console.log("responded!")
return response.json();
})
.catch(err => console.log(err));
console.log(res);
return res.choices[0].message.content;
}
/**
*
* @param {string} captions
* @param {{length: number, language: string, format: "summary"|"listicle"|"product review"|"tutorial", tone: "professional"|"informal"|"informational"}} param1
*/
export async function createBlogFromCaptions(captions, {
length,
language,
format,
tone
} = {length: 500, language: "English", format: "summary", tone: "informal"}) {
const prompt = `"Please convert the following video transcript into a blog post. The approximate length should be around ${length || 500} characters, written in ${language || "English"}. The desired format of the blog post is ${format || "summary"}. Please ensure the blog post has a ${tone || "informal"} tone throughout. Use markdown to format the article. Here is the transcript: "`
const result = await promptGPT(prompt + captions);
return result;
}

View File

@ -6,6 +6,8 @@ const envSchema = z.object({
REDIS_URL: z.string().default("redis://127.0.0.1:6379/"), REDIS_URL: z.string().default("redis://127.0.0.1:6379/"),
PORT: z.coerce.number().default(8080), PORT: z.coerce.number().default(8080),
HOST: z.string().default("127.0.0.1"), HOST: z.string().default("127.0.0.1"),
PUBLIC_API_URL: z.string(),
FRONTEND_URL: z.string()
}); });
export const env = envSchema.parse(process.env); export const env = envSchema.parse(process.env);

View File

@ -1,3 +1,4 @@
import { eq } from "drizzle-orm";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
import { sessions } from "../db/schemas.js"; import { sessions } from "../db/schemas.js";
@ -8,15 +9,23 @@ import { sessions } from "../db/schemas.js";
* *
* @returns {Promise<{session_id: string}>} Auth token * @returns {Promise<{session_id: string}>} Auth token
*/ */
export async function createAuthToken(user_id, { export async function createSession(user_id, {
access_token, access_token,
refresh_token, refresh_token,
expires_at expires_at
}) { }) {
const [existing_session] = await db.select().from(sessions).where(eq(sessions.user_id, user_id));
if(existing_session) {
return {
session_id: existing_session.id
}
}
const token = await db.insert(sessions).values({ const token = await db.insert(sessions).values({
user_id, user_id,
google_access_token: access_token, access_token: access_token,
google_refresh_token: refresh_token, refresh_token: refresh_token,
expires_at expires_at
}).returning({ id: sessions.id }); }).returning({ id: sessions.id });
if(token.length == 0) { if(token.length == 0) {

View File

@ -1,3 +1,4 @@
import { eq } from 'drizzle-orm';
import { db } from '../db/index.js'; import { db } from '../db/index.js';
import { sessions } from '../db/schemas.js'; import { sessions } from '../db/schemas.js';
import { google } from 'googleapis'; import { google } from 'googleapis';
@ -38,7 +39,7 @@ export async function getCaptionText(access_token, caption_id) {
"Authorization": "Bearer " + access_token "Authorization": "Bearer " + access_token
} }
}).then(res=>res.data).then(x=>x.text()); }).then(res=>res.data).then(x=>x.text());
console.log(caption_text)
return caption_text; return caption_text;
} }
@ -100,13 +101,28 @@ export async function getUserInfo(access_token) {
/** /**
* *
* @param {import('@fastify/oauth2').OAuth2Namespace} oauth * @param {import('fastify').FastifyInstance} fastify
* @param {import('@fastify/oauth2').Token} session * @param {import('fastify').FastifyRequest} request
* *
* @returns {Promise<import('@fastify/oauth2').OAuth2Token>} * @returns {Promise<string>}
*/ */
export async function getNewToken(oauth, session) { export async function getAccessToken(fastify, request) {
const {token} = await oauth.getNewAccessTokenUsingRefreshToken(session, {}); // TODO: Move to cache instead of postgres
// const [cachedToken] = await db.select().from(sessions).where(eq(sessions.id, request.session.id));
let access_token = request.session.access_token;
return token; if((new Date().getTime() + 10) > request.session.expires_at) {
/** @type {import('@fastify/oauth2').Token} */
const {token} = await fastify.googleOAuth2.getNewAccessTokenUsingRefreshToken(fastify);
access_token = token.access_token;
await db.update(sessions).set({
expires_at: token.expires_at,
access_token: token.access_token
});
}
return access_token;
} }

26
test.md
View File

@ -239,18 +239,28 @@ make sure not to miss any of my future
videos try to click that subscribe videos try to click that subscribe
button bye guys and see in the next one button bye guys and see in the next one
Prompt 0: Split text : Pass 2k characters in to the context and it will return a json with the required text
```
You are a personal assistant of mine. I need you to take youtube captions and split them into 1500-2000 character blocks. The blocks should all be self-enclosing. Meaning no context from the 1st block is needed in the 2nd block etc. for it to make sense. Omit any sponsorships and mentions of "this video was brought by".
VERY IMPORTANT: Respond with a JSON compatible array of strings.
```
Prompt 1: Extract main points Prompt 1: Extract main points
``` ```
<system>
You are my personal assistant. I have a youtube video that needs to be broken down into the main points. Please take the captions I have given you and extract the main talking points along with quotes from the script. Send only a list of points with a quote attached to each point. Do not mention "The reviewer" or "You", only say full statements. Ignore any sponsorships or "this video was brought to you by" statements. Those should be excluded from the list of talking points. The talking points returned should look like this: You are my personal assistant. I have a youtube video that needs to be broken down into the main points. Please take the captions I have given you and extract the main talking points along with quotes from the script. Send only a list of points with a quote attached to each point. Do not mention "The reviewer" or "You", only say full statements. Ignore any sponsorships or "this video was brought to you by" statements. Those should be excluded from the list of talking points. The talking points returned should look like this:
* Main talking point (Quote: "Quote from the captions") * Main talking point (Quote: "Quote from the captions")
</system>
<captions> <captions>
a part of the captions
</captions> </captions>
<response> <response>
``` ```
Prompt 2: Combine into blog Prompt 2: Combine into blog mistral-7b-instruct-0.2
``` ```
You are a copywriter. I just got my personal assistant to take the main talking points and quotes from a youtube video of mine. Take these main talking points and write an engaging blog article. You are a copywriter. I just got my personal assistant to take the main talking points and quotes from a youtube video of mine. Take these main talking points and write an engaging blog article.
@ -260,6 +270,20 @@ all talking points from previous prompt
<response> <response>
``` ```
Thoughs: The response was generally pretty OK. Got like 60% on quillbot AI detector.
Prompt 2 v2: Caption directly into blog
```
You are a copywriter. Your job is to write blog articles for all of my youtube videos to help with SEO and channel growth. I provide you with SRT captions from my youtube videos and you return an *engaging* blog article I can post to my website. You are allowed to use markdown for formatting. Make sure you do not mention any sponsors or "video was brough to you by" in the blog article.
```
Thoughts: Very robotic, didn't like it. Also tended to imagine stuff.
Prompt 2 v3: Main talking points
```
Take the main points and quotes from my YouTube video, picked out by my assistant, and turn them into a fun and informative blog post. The topic can change depending on the video. Keep it easy to read and relatable, like chatting with a friend. You can use markdown if it helps. The aim is to make content that feels natural and connects well with my audience, while still getting across the main ideas from the video.
```
full response. Model used: mistral-7b-instruct-v0.2
Title: My Honest Take on the AMD Radeon RX 7900 XT: A Powerful Card with Room for Improvement Title: My Honest Take on the AMD Radeon RX 7900 XT: A Powerful Card with Room for Improvement

1390
yarn.lock

File diff suppressed because it is too large Load Diff