From 8a0d1c132f4e67eb031041738fe91a850026ed2a Mon Sep 17 00:00:00 2001 From: Anurag Maurya Date: Sun, 31 Aug 2025 20:43:15 +0530 Subject: [PATCH] added youtube summary ai app and fixed issue #202 --- backend/.env.example | 26 ++++- backend/bun.lock | 50 ++++++++++ backend/package.json | 2 + .../migration.sql | 2 + backend/prisma/schema.prisma | 1 + backend/routes/apps/app.ts | 4 + backend/routes/apps/index.ts | 62 +++++++++++- backend/routes/apps/youtube-summarizer.ts | 95 ++++++++++++++++++ .../(app)/apps/youtube-summarizer/page.tsx | 99 +++++++++++++++++++ frontend/bun.lock | 3 + frontend/components/ui/ui-structure.tsx | 7 ++ 11 files changed, 344 insertions(+), 7 deletions(-) create mode 100644 backend/prisma/migrations/20250831114125_added_youtube_summarization_execution_type/migration.sql create mode 100644 frontend/app/(app)/apps/youtube-summarizer/page.tsx diff --git a/backend/.env.example b/backend/.env.example index b6998f3a..92450b62 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,5 +1,21 @@ -OPENROUTER_KEY= -FROM_EMAIL= -POSTMARK_SERVER_TOKEN= -DB_URL= -RZP_WEBHOOK_SECRET= \ No newline at end of file +# Application / Prisma +DATABASE_URL="" +JWT_SECRET="" + +YOUTUBE_API_KEY="" + +# OpenRouter (LLM provider) +OPENROUTER_KEY="" +GEMINI_API_KEY="" + +# Email / Postmark +FROM_EMAIL="" +POSTMARK_SERVER_TOKEN="" +# Razorpay +RZP_KEY="" +RZP_SECRET="" +RZP_ENVIRONMENT="test" +RZP_WEBHOOK_SECRET="" + +# Frontend +FRONTEND_URL="http://localhost:3002" \ No newline at end of file diff --git a/backend/bun.lock b/backend/bun.lock index 948a2496..7efb489a 100644 --- a/backend/bun.lock +++ b/backend/bun.lock @@ -4,6 +4,7 @@ "": { "name": "backend", "dependencies": { + "@google/genai": "^1.16.0", "@prisma/client": "6.14.0", "@types/cors": "^2.8.19", "@types/express": "^5.0.3", @@ -16,6 +17,7 @@ "prisma": "^6.14.0", "razorpay": "^2.9.6", "totp-generator": "^1.0.0", + "uuid": "^11.1.0", "zod": "^4.0.17", }, "devDependencies": { @@ -27,6 +29,8 @@ }, }, "packages": { + "@google/genai": ["@google/genai@1.16.0", "", { "dependencies": { "google-auth-library": "^9.14.2", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.11.4" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-hdTYu39QgDFxv+FB6BK2zi4UIJGWhx2iPc0pHQ0C5Q/RCi+m+4gsryIzTGO+riqWcUA8/WGYp6hpqckdOBNysw=="], + "@prisma/client": ["@prisma/client@6.14.0", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-8E/Nk3eL5g7RQIg/LUj1ICyDmhD053STjxrPxUtCRybs2s/2sOEcx9NpITuAOPn07HEpWBfhAVe1T/HYWXUPOw=="], "@prisma/config": ["@prisma/config@6.14.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.16.12", "empathic": "2.0.0" } }, "sha512-IwC7o5KNNGhmblLs23swnfBjADkacBb7wvyDXUWLwuvUQciKJZqyecU0jw0d7JRkswrj+XTL8fdr0y2/VerKQQ=="], @@ -77,10 +81,16 @@ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "axios": ["axios@1.11.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], @@ -161,6 +171,8 @@ "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], @@ -175,14 +187,24 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="], + + "gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + "google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="], + + "google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], @@ -193,6 +215,8 @@ "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], @@ -203,8 +227,12 @@ "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], "jssha": ["jssha@3.3.1", "", {}, "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ=="], @@ -241,6 +269,8 @@ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "nypm": ["nypm@0.6.1", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.2.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w=="], @@ -315,6 +345,8 @@ "totp-generator": ["totp-generator@1.0.0", "", { "dependencies": { "jssha": "^3.3.1" } }, "sha512-Iu/1Lk60/MH8FE+5cDWPiGbwKK1hxzSq+KT9oSqhZ1BEczGIKGcN50bP0WMLiIZKRg7t29iWLxw6f81TICQdoA=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], @@ -323,16 +355,34 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "zod": ["zod@4.0.17", "", {}, "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ=="], "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + + "google-auth-library/jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="], + + "gtoken/jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="], + "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "google-auth-library/jws/jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "gtoken/jws/jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], } } diff --git a/backend/package.json b/backend/package.json index 74478bbe..f0afa3ec 100644 --- a/backend/package.json +++ b/backend/package.json @@ -16,6 +16,7 @@ "typescript": "^5.0.0" }, "dependencies": { + "@google/genai": "^1.16.0", "@prisma/client": "6.14.0", "@types/cors": "^2.8.19", "@types/express": "^5.0.3", @@ -28,6 +29,7 @@ "prisma": "^6.14.0", "razorpay": "^2.9.6", "totp-generator": "^1.0.0", + "uuid": "^11.1.0", "zod": "^4.0.17" } } diff --git a/backend/prisma/migrations/20250831114125_added_youtube_summarization_execution_type/migration.sql b/backend/prisma/migrations/20250831114125_added_youtube_summarization_execution_type/migration.sql new file mode 100644 index 00000000..68b332a1 --- /dev/null +++ b/backend/prisma/migrations/20250831114125_added_youtube_summarization_execution_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "public"."ExecutionType" ADD VALUE 'YOUTUBE_SUMMARIZER'; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 43dcffd2..f2e67db9 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -40,6 +40,7 @@ model Execution { enum ExecutionType { CONVERSATION ARTICLE_SUMMARIZER + YOUTUBE_SUMMARIZER } model Conversation { diff --git a/backend/routes/apps/app.ts b/backend/routes/apps/app.ts index 41228a14..0cc7fcfd 100644 --- a/backend/routes/apps/app.ts +++ b/backend/routes/apps/app.ts @@ -32,6 +32,10 @@ export abstract class App { } } + public getPerExecutionCredit(): number { + return this.per_execution_credit; + } + initStreamableRoute() { return (req: Request, res: Response) => { const result = this.zodSchema.safeParse(req.body); diff --git a/backend/routes/apps/index.ts b/backend/routes/apps/index.ts index c971bc81..24011112 100644 --- a/backend/routes/apps/index.ts +++ b/backend/routes/apps/index.ts @@ -1,12 +1,13 @@ import { Router } from "express"; import { ArticleSummarizer } from "./article-summarizer"; +import { YoutubeSummarizer } from "./youtube-summarizer"; import { authMiddleware } from "../../auth-middleware"; import { PrismaClient } from "../../generated/prisma"; const router = Router(); router.get("/", (req, res) => { - res.json(["article-summarizer"]); + res.json(["article-summarizer", "youtube-summarizer"]); }); const prismaClient = new PrismaClient(); @@ -44,7 +45,7 @@ router.get("/article-summarizer/:executionId", authMiddleware, async (req, res) }); }); -router.post("/article-summarizer", authMiddleware, (req, res) => { +router.post("/article-summarizer", authMiddleware, async (req, res) => { // Set SSE headers res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); @@ -65,6 +66,20 @@ router.post("/article-summarizer", authMiddleware, (req, res) => { return; } + // Check and deduct credits before starting stream + try { + const user = await prismaClient.user.findUnique({ where: { id: req.userId }, select: { credits: true } }); + const cost = articleSummarizer.getPerExecutionCredit(); + if (!user || user.credits < cost) { + res.status(403).json({ error: "Insufficient credits" }); + return; + } + await prismaClient.user.update({ where: { id: req.userId }, data: { credits: { decrement: cost } } }); + } catch (e) { + res.status(500).json({ error: "Failed to deduct credits" }); + return; + } + articleSummarizer.runStreamable(result.data as any, (chunk: string) => { // Send data in SSE format res.write(`data: ${chunk}\n\n`); @@ -78,4 +93,47 @@ router.post("/article-summarizer", authMiddleware, (req, res) => { }); }); +router.post("/youtube-summarizer", authMiddleware, async (req, res) => { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Headers', 'Cache-Control'); + + const youtubeSummarizer = new YoutubeSummarizer(); + + const result = youtubeSummarizer.zodSchema.safeParse({ + ...req.body, + userId: req.userId + }); + + if (!result.success) { + res.status(400).json({ error: result.error.message }); + return; + } + + // Check and deduct credits before starting stream + try { + const user = await prismaClient.user.findUnique({ where: { id: req.userId }, select: { credits: true } }); + const cost = youtubeSummarizer.getPerExecutionCredit(); + if (!user || user.credits < cost) { + res.status(403).json({ error: "Insufficient credits" }); + return; + } + await prismaClient.user.update({ where: { id: req.userId }, data: { credits: { decrement: cost } } }); + } catch (e) { + res.status(500).json({ error: "Failed to deduct credits" }); + return; + } + + youtubeSummarizer.runStreamable(result.data as any, (chunk: string) => { + res.write(`data: ${chunk}\n\n`); + }).then(() => { + res.end(); + }).catch((error) => { + res.write(`data: {"error": "${error.message}"}\n\n`); + res.end(); + }); +}); + export default router; \ No newline at end of file diff --git a/backend/routes/apps/youtube-summarizer.ts b/backend/routes/apps/youtube-summarizer.ts index e69de29b..0e42ecc9 100644 --- a/backend/routes/apps/youtube-summarizer.ts +++ b/backend/routes/apps/youtube-summarizer.ts @@ -0,0 +1,95 @@ +import { z } from "zod"; +import { App, AppType } from "./app"; +import { PrismaClient } from "../../generated/prisma"; +import { GoogleGenAI } from "@google/genai"; + +const YoutubeSummarizerSchema = z.object({ + url: z.string().url(), + userId: z.string() +}); + +const MODEL = "gemini-2.5-flash"; +const prismaClient = new PrismaClient(); + +const SYSTEM_PROMPT = ` + You are a helpful assistant that summarizes YouTube videos. + You will be given a YouTube URL. Extract the video's transcript (if available) and summarize the key points in a concise, easy-to-understand way with bullet points. If transcript is unavailable, ask the user to provide a brief description. +`; + +export class YoutubeSummarizer extends App { + constructor() { + super({ + name: "YouTube Summarizer", + route: "/youtube-summarizer", + description: "Summarize a YouTube video", + icon: "https://static.vecteezy.com/system/resources/thumbnails/018/930/575/small_2x/youtube-logo-youtube-icon-transparent-free-png.png", + per_execution_credit: 4, + zodSchema: YoutubeSummarizerSchema, + appType: AppType.StreamableLLM + }); + } + + async runStreamable(data: z.infer, callback: (chunk: string) => void) { + const { url } = data; + + let response = ""; + + console.log("Starting AI") + const ai = new GoogleGenAI({ + apiKey: process.env.GEMINI_API_KEY, + }); + + const config = { + thinkingConfig: { + thinkingBudget: -1, + }, + } as any; + + const contents = [ + { + role: "user", + parts: [ + { + fileData: { + fileUri: url, + mimeType: "video/*", + } + }, + { + text: "Summarize the video keep the summary small yet informative , do not miss any key points in plain english without the jargons respond in plain text with out any formatting variables or new line variables or ** or * please", + }, + ], + }, + ]; + + console.log("Starting AI Stream") + const stream = await ai.models.generateContentStream({ + model: MODEL, + config, + contents, + }); + + for await (const chunk of stream as any) { + if (chunk?.text) { + callback(chunk.text); + response += chunk.text; + } + } + console.log("AI stream end") + + try { + await prismaClient.execution.create({ + data: { + title: "YouTube Summarizer", + type: "YOUTUBE_SUMMARIZER", + user: { connect: { id: data.userId } } + } + }); + } catch (error) { + console.error("Error saving youtube summary execution:", error); + } + + return response; + } +} + diff --git a/frontend/app/(app)/apps/youtube-summarizer/page.tsx b/frontend/app/(app)/apps/youtube-summarizer/page.tsx new file mode 100644 index 00000000..39c5f5fa --- /dev/null +++ b/frontend/app/(app)/apps/youtube-summarizer/page.tsx @@ -0,0 +1,99 @@ +"use client" +import React from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Send } from "lucide-react"; + +const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:3000"; + +export default function YoutubeVideoSummarizer(){ + const [url, setUrl] = React.useState(""); + const [isLoading, setIsLoading] = React.useState(false); + const [response, setResponse] = React.useState(""); + const [error, setError] = React.useState(null); + + const handleClick = async()=>{ + if (!url.trim()) return; + setIsLoading(true); + setResponse(""); + setError(null); + + try{ + const token = localStorage.getItem("token"); + if(!token){ + setError("Authentication token not found. Please login again."); + setIsLoading(false); + return; + } + + const res = await fetch(`${BACKEND_URL}/apps/youtube-summarizer`,{ + method:"POST", + headers:{ + "Content-Type":"application/json", + "Authorization": `Bearer ${token}` + }, + body: JSON.stringify({ url }) + }); + + if(!res.body){ + setError("No response body received"); + setIsLoading(false); + return; + } + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + while(true){ + const {done, value} = await reader.read(); + if(done) break; + const chunk = decoder.decode(value); + buffer += chunk; + + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + for(const line of lines){ + if(line.startsWith("data: ")){ + const data = line.slice(6); + if(data && data !== "[DONE]"){ + setResponse(prev => prev + data); + } + } + } + } + }catch(e){ + setError("Failed to summarize the video"); + }finally{ + setIsLoading(false); + } + } + + return( +
+
+

Youtube Video Summarizer

+

+ Summarize long and boring youtube videos into a small engaging summary +

+
+
+ setUrl(e.target.value)} type="text" placeholder="Enter Youtube Video link"/> + +
+
+ {error && ( +
{error}
+ )} + {response && ( +
{response}
+ )} + {isLoading && !response && ( +
Processing...
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/frontend/bun.lock b/frontend/bun.lock index 6c2ff42a..bdbbcc32 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -39,6 +39,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "framer-motion": "^12.16.0", + "input-otp": "^1.4.2", "lucide-react": "^0.514.0", "next": "^15.2.3", "next-auth": "5.0.0-beta.25", @@ -1012,6 +1013,8 @@ "inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="], + "input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="], + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], diff --git a/frontend/components/ui/ui-structure.tsx b/frontend/components/ui/ui-structure.tsx index 21b5d500..3b059b0f 100644 --- a/frontend/components/ui/ui-structure.tsx +++ b/frontend/components/ui/ui-structure.tsx @@ -94,6 +94,13 @@ export function UIStructure() { icon: "📄", credits: 2, }, + { + id: "youtube-summarizer", + name: "YouTube Summarizer", + description: "Summarize YouTube videos into key bullet points", + icon: "📺", + credits: 4, + }, ]; const handleAppNavigation = (appId: string) => {