From 0ccdef12e90cea6ca913c490830bf4612927a791 Mon Sep 17 00:00:00 2001 From: Khaled Alshibani <127689031+khaledsAlshibani@users.noreply.github.com> Date: Sat, 22 Mar 2025 14:58:19 +0300 Subject: [PATCH 01/22] prod: deploy to vercel (WIP) --- src/app.module.ts | 13 +++++-------- src/main.ts | 17 ++--------------- src/summarization/summarization.service.ts | 8 ++++---- test/http/test.http | 13 +++++++++---- vercel.json | 4 ++-- 5 files changed, 22 insertions(+), 33 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 3b9b10c..8c78d18 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,18 +1,15 @@ -import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { SummarizationModule } from './summarization/summarization.module'; import { ScheduleModule } from '@nestjs/schedule'; -import { ServeStaticModule } from '@nestjs/serve-static'; -import { join } from 'path'; -import { PUBLIC_DIR } from './utils/constants'; @Module({ imports: [ ScheduleModule.forRoot(), - ServeStaticModule.forRoot({ - rootPath: join(__dirname, '..', 'downloads'), - serveRoot: PUBLIC_DIR, - }), + // ServeStaticModule.forRoot({ + // rootPath: join(__dirname, '..', 'downloads'), + // serveRoot: PUBLIC_DIR, + // }), SummarizationModule ], controllers: [AppController], diff --git a/src/main.ts b/src/main.ts index 2dc8961..7e2ba5c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,6 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; -import { Express } from 'express'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -13,19 +12,7 @@ async function bootstrap() { credentials: true, }); - if (process.env.NODE_ENV === 'production') { - await app.init(); - const expressApp = app.getHttpAdapter().getInstance(); - return expressApp; - } else { - await app.listen(process.env.PORT ?? 3000); - } + await app.listen(process.env.PORT ?? 3000); } -// Run locally in development -if (process.env.NODE_ENV !== 'production') { - bootstrap(); -} - -// Export for Vercel in production -export default bootstrap; +bootstrap(); diff --git a/src/summarization/summarization.service.ts b/src/summarization/summarization.service.ts index 23b2650..a7a3f57 100644 --- a/src/summarization/summarization.service.ts +++ b/src/summarization/summarization.service.ts @@ -36,11 +36,11 @@ import { isValidYouTubeUrl, } from '../utils/video.util'; import { SummarizationOptions } from './interfaces/summarization-options.interface'; -import { getApiKey } from 'src/utils/api-key.util'; -import { convertTextToSpeech } from 'src/utils/tts.util'; -import { transcribeAudio } from 'src/utils/transcription.util'; +import { getApiKey } from '../utils/api-key.util'; +import { convertTextToSpeech } from '../utils/tts.util'; +import { transcribeAudio } from '../utils/transcription.util'; import { join } from 'path'; -import { uploadDownloadedAudioToS3 } from 'src/utils/s3.util'; +import { uploadDownloadedAudioToS3 } from '../utils/s3.util'; @Injectable() export class SummarizationService { diff --git a/test/http/test.http b/test/http/test.http index b0227b4..ce3121e 100644 --- a/test/http/test.http +++ b/test/http/test.http @@ -1,16 +1,20 @@ @openaiKey = {{$dotenv OPENAI_API_KEY}} @deepseekKey = {{$dotenv DEEPSEEK_API_KEY}} @allowedOrigin = {{$dotenv ALLOWED_ORIGIN}} -@baseUrl = http://localhost:3000 +@baseUrl = https://letssummarize-api.vercel.app/ @ytVideo = https://www.youtube.com/shorts/cKQC2-g6CRY @text = `I'm going to show you the best way to start practicing designing apps and websites in Figma. So in this video I'm going to give you step-by-step instructions. You can literally follow click by click. I'll only tell you the stuff that you need to get started designing interfaces. So let's get started. So we're going to be looking at a tool called Figma and it has a few advantages. One, most importantly for you, it's free to get started if you're working by yourself. We like using it at AGN Smart because it also has really good collaboration so we can have multiple people working on the same design file at the same time. It's also really fast. It works on any computer whether you have a Mac or a PC or Linux. Whatever you have it works right in the browser and it also has a mobile companion app so you can preview your designs on a mobile screen. So there are really no downsides to starting with a tool like Figma. As you're watching the video, if you have any questions about how to do a particular effect in Figma or any comment or something that you want to recommend, please put it in the comments below. And if you want to find out more tips about UI and UX, make sure to subscribe to our free newsletter. The link to that is in the description below and it's a great resource for anyone starting in UI and UX. So this is the website. You just go to Figma.com and I'm already signed in but you can sign up very quickly even with your Google account and get started. But before we jump right into Figma, I want to show you the way I would recommend to get started. So you just want to start practicing. Now for that, I'm not going to ask you to start designing something from scratch because I believe that would be very hard with someone, especially if you're a complete beginner in this space and you have no grounding in design principles and things like that. So the best way for you to get started is actually to copy other designs. And the reason this is so good is because you can see how this design was created so that when you get stuck on something, you can actually see how this person who created this file achieved particular effect or look inside of Figma. And this is totally fine in the beginning because you're not going to be selling these. You're not going to be saying that you designed something when you copied it from someone else. This is just for your own practice and it's a really good way to get started. So as you can see here, this is what Figma looks like after you log in and start a file. And I haven't even shown you how to start a file because I want you to use another file as your starting point as opposed to a blank file. And like I said, we're not going to cover everything that you can see here on the screen in terms of what all the various buttons do. We're just going to focus about how you can get started. Now to do that, I wanted to start off with a template. And what I literally did was I typed into Google Figma resources and I got a bunch of results` @fileUrl = ./media/example.pdf +GET {{baseUrl}} +Content-Type: application/json + ### Summarize YouTube Video POST {{baseUrl}}/summarize/video Content-Type: application/json +Authorization: Bearer {{deepseekKey}} Origin: {{allowedOrigin}} { @@ -20,9 +24,9 @@ Origin: {{allowedOrigin}} "options": { "length": "comprehensive", "format": "default", - "speed": "slow", + "speed": "fast", "model": "deepseek", - "listen": true, + "listen": false, "customInstructions": "use tables and add your opinion in the end", "lang": "english" } @@ -31,6 +35,7 @@ Origin: {{allowedOrigin}} ### Summarize Text POST {{baseUrl}}/summarize/text Content-Type: application/json +Authorization: Bearer {{openaiKey}} Origin: {{allowedOrigin}} { @@ -40,7 +45,7 @@ Origin: {{allowedOrigin}} "options": { "length": "brief", "format": "bullet-points", - "model": "deepseek", + "model": "openai", "listen": false } } diff --git a/vercel.json b/vercel.json index 81e4b21..0749d4a 100644 --- a/vercel.json +++ b/vercel.json @@ -2,14 +2,14 @@ "version": 2, "builds": [ { - "src": "dist/main.js", + "src": "src/main.ts", "use": "@vercel/node" } ], "routes": [ { "src": "/(.*)", - "dest": "dist/main.js", + "dest": "src/main.ts", "methods": ["GET", "POST"] } ] From c10a887d40bdc7dcbd9c581f5b4cbf54faed3bbd Mon Sep 17 00:00:00 2001 From: Khaled Alshibani <127689031+khaledsAlshibani@users.noreply.github.com> Date: Sun, 23 Mar 2025 20:49:25 +0300 Subject: [PATCH 02/22] prod(vercel): fix CORS issues --- src/main.ts | 6 +++++- src/utils/constants.ts | 4 +++- vercel.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main.ts b/src/main.ts index 7e2ba5c..0f193d1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,8 +8,12 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); app.enableCors({ - origin: true, + origin: ['http://localhost:3001', 'https://letssummarize.vercel.app'], credentials: true, + methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'], + allowedHeaders: ['Content-Type', 'Authorization'], + exposedHeaders: ['Authorization'], + optionsSuccessStatus: 204, }); await app.listen(process.env.PORT ?? 3000); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 2ed2ff8..359f0b8 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -15,4 +15,6 @@ export const USE_S3: boolean = process.env.USE_S3 === 'true'; export const DOWNLOAD_DIR = join(process.cwd(), 'downloads'); export const PUBLIC_DIR = '/public/audio'; export const AUDIO_FORMAT = 'mp3'; -export const MAX_FILE_AGE = 1000 * 60; // 1 day \ No newline at end of file +export const MAX_FILE_AGE = 1000 * 60; // 1 day + +export const CORS_ORIGINS: string[] = process.env.CORS ? process.env.CORS.split(',') : ['http://localhost:3001', 'https://letssummarize.vercel.app']; \ No newline at end of file diff --git a/vercel.json b/vercel.json index 0749d4a..cd272ac 100644 --- a/vercel.json +++ b/vercel.json @@ -10,7 +10,7 @@ { "src": "/(.*)", "dest": "src/main.ts", - "methods": ["GET", "POST"] + "methods": ["GET", "POST", "OPTIONS"] } ] } From 70e27308541206373e2100b8eee08c50e55cdb8c Mon Sep 17 00:00:00 2001 From: muneeb-almoliky Date: Mon, 24 Mar 2025 10:00:56 +0300 Subject: [PATCH 03/22] feat: add support for Fast-Whisper as an option and integrate with the Fast-Whisper Python API --- package.json | 3 ++ pnpm-lock.yaml | 22 ++++++++++ src/app.module.ts | 15 ++++--- .../transcribe_api.cpython-311.pyc | Bin 0 -> 9840 bytes src/python/transcribe_api/transcribe_api.py | 1 - .../dto/summarization-options.dto.ts | 6 ++- .../enums/summarization-options.enum.ts | 6 +++ .../summarization-options.interface.ts | 3 +- src/summarization/summarization.module.ts | 2 + src/summarization/summarization.service.ts | 14 +++++-- src/utils/constants.ts | 3 +- src/utils/summarization.util.ts | 2 + src/utils/transcription.util.ts | 39 ++++++++++++++++-- 13 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 src/python/__pycache__/transcribe_api.cpython-311.pyc diff --git a/package.json b/package.json index 8985306..067fd8f 100644 --- a/package.json +++ b/package.json @@ -21,16 +21,19 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.758.0", + "@nestjs/axios": "^4.0.0", "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "@nestjs/schedule": "^5.0.1", "@nestjs/serve-static": "^5.0.3", "aws-sdk": "^2.1692.0", + "axios": "^1.8.4", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.7", "fluent-ffmpeg": "^2.1.3", + "form-data": "^4.0.2", "mammoth": "^1.9.0", "multer": "1.4.5-lts.1", "openai": "^4.87.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 949714e..35c8c1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@aws-sdk/client-s3': specifier: ^3.758.0 version: 3.758.0 + '@nestjs/axios': + specifier: ^4.0.0 + version: 4.0.0(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.8.4)(rxjs@7.8.2) '@nestjs/common': specifier: ^11.0.1 version: 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -29,6 +32,9 @@ importers: aws-sdk: specifier: ^2.1692.0 version: 2.1692.0 + axios: + specifier: ^1.8.4 + version: 1.8.4 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -41,6 +47,9 @@ importers: fluent-ffmpeg: specifier: ^2.1.3 version: 2.1.3 + form-data: + specifier: ^4.0.2 + version: 4.0.2 mammoth: specifier: ^1.9.0 version: 1.9.0 @@ -982,6 +991,13 @@ packages: resolution: {integrity: sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==} engines: {node: '>= 10'} + '@nestjs/axios@4.0.0': + resolution: {integrity: sha512-1cB+Jyltu/uUPNQrpUimRHEQHrnQrpLzVj6dU3dgn6iDDDdahr10TgHFGTmw5VuJ9GzKZsCLDL78VSwJAs/9JQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + axios: ^1.3.1 + rxjs: ^7.0.0 + '@nestjs/cli@11.0.5': resolution: {integrity: sha512-ab/d8Ple+dMSQ4pC7RSNjhntpT8gFQQE8y/F/ilaplp7zPGpuxbayRtYbsA/wc1UkJHORDckrqFc8Jh8mrTq2A==} engines: {node: '>= 20.11'} @@ -5750,6 +5766,12 @@ snapshots: '@napi-rs/nice-win32-x64-msvc': 1.0.1 optional: true + '@nestjs/axios@4.0.0(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.8.4)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + axios: 1.8.4 + rxjs: 7.8.2 + '@nestjs/cli@11.0.5(@swc/cli@0.6.0(@swc/core@1.11.11)(chokidar@4.0.3))(@swc/core@1.11.11)(@types/node@22.13.10)': dependencies: '@angular-devkit/core': 19.1.8(chokidar@4.0.3) diff --git a/src/app.module.ts b/src/app.module.ts index 8c78d18..5a5781d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,15 +2,20 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { SummarizationModule } from './summarization/summarization.module'; import { ScheduleModule } from '@nestjs/schedule'; +import { ServeStaticModule } from '@nestjs/serve-static'; +import { join } from 'path'; +import { PUBLIC_DIR } from './utils/constants'; +import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ ScheduleModule.forRoot(), - // ServeStaticModule.forRoot({ - // rootPath: join(__dirname, '..', 'downloads'), - // serveRoot: PUBLIC_DIR, - // }), - SummarizationModule + ServeStaticModule.forRoot({ + rootPath: join(__dirname, '..', 'downloads'), + serveRoot: PUBLIC_DIR, + }), + SummarizationModule, + HttpModule ], controllers: [AppController], providers: [], diff --git a/src/python/__pycache__/transcribe_api.cpython-311.pyc b/src/python/__pycache__/transcribe_api.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f570b13bfe89301a1cb179f1187d8cbd17db839a GIT binary patch literal 9840 zcmcIJYit|GnX_CzmT&4!>gBZ_wrI-~70G(pdB}P=iY3|A!%i4qd9-#VQ|3e6UD}pP z6>RD@y1;i)S{G4L8&-kTr=TjJyK4ah2Nby`IpjwR>>3+{SU`Z0Lx2|d;}|5y1^#v4 z?2;5otF8OvhQryJ*>Ap?`R1GNwf}6lTM(q(%)^EMbR+a%xRFch8)$rW(}2+1XcjSu zAtESB1c<0Epo@|LGO7>g3EZ!X7^232QLU2^Q@{j$WP}P(>eC!Bt4~Y7qCTwwtNOGB zZ0ge#h_#lYzp zdyHTlB_nEKL|w@!C*vxHDp_g$K4Z$5^1nEyp=6ESCypmCz;d zHH^C$K5(!E!o$=SLu@wX=v(-_2aR9mJD6IHhJj|lR!XfproI?s$r@YO>dn?Xs)7pB zz`B2im`2tE{yyT2=Kb6hCj$Qz${H{Bb3c7{Ws0k!q&s6);nbB zY(Nd7m{Ng53>iAi9169;(;G&CBdn7>%DU!tp8U?)cINO&XfbU!P~aHgurB&?FWF;^ zcl$^&JvTdYZwsHr^TO>v%KDjh_UOExId(%I=wuIVK2r#vt24ghT`(OtP%*AR7mV%B z!52OmfALsYJIQnw_stW`Gpuf2$8_B=l;Azi_AuSq+xW_#J=x=cwNMW9Fuld}*zep` zS=Jlqg|WSheYrKWhnPO-DdpHFFc$BC>E9l>rw9Irzm|ZyIJT@9heytj4UH(|+)_fZ4V|4h zH#;*DoO$IO0EA;gpJJMa9SNO1O1U<6{OsUxaAs^`qU-T1Wfxu?n?5%(6+AUE zIH92?jw`NQ@A%;4>Dj^4BZ`iVDHXZinUM(?GB`6kHL^nbeO-!eaCUg?Y;g4K)WqP7 zLPnRMYh-fp)c8nnXmIF7yj(89D$a?)m$i=I7~q&z$WFgMVTEJ({N10^DoqFI6-zrk z%w7%8u~2ul(?jv-QbJ(qndK$uNmK)L=I%ee^FR0Bqz4mBI8KkMbc0te_0>=A(5Ki? zl%5W+U<|f)dOQ?cNC35=;E}n=n0Ku^##y68_fc0eVzME2cC>&cT9_RDQXg)DF2Qp$l5s570NgtEGXXaPTIfgtqxl%nE0zm~Uh|_qCw!Q)S zeL|tw>vO?yY(75fwJUaY-9ZcjtHN?4$|}Z4d|`p*u#f<$V}b>6m{&M_gE&l0#h?yT ztlDUnm$T_&Q#9xfC0)LE8M57@NF7L6>($bS%;fp*scX4T1xEzmN z%w0-NK>C)J6${>IP+c|G12ba{!v8lEDU>myDo<)6qqiFxcdIJJat?o6OMEPaPFFujKfs`=-D*U97r*X}Wf8T5v*6bqHkk7my_Iu-$;gkja&P)us z(eEslp)&G!m0d$N@fvCt)`VW|vE?gN%IVYo>65-20)xMIG{UT4A! zEH8Kot{X)s4QSbi>Akbr?V-7!aDX$2y(S{X)kb479MwcLjg6$UyH-@~ehi&T<2LLd0_s2h{ z!6>0Gv!RG^na1ko3ha|vii2YJDJCuvi@{ze!GJNs^%D}jV%xjwEW>#L3x~~7FUcLl z^?b+shxYN)-3xB=^3b-;9s;J*}H*C&`Ztt$=&~8mr#%!<|Ge|28 zMitYFtz+*dcK8%Mre+bJP=M|S@c$|R!sZMjFt0!}m$;*aMA@G|7^0YqKnG&Ml}(I} zA-DC#=H+XIfalF7uB&Jcd`wRy#_$*CFfyh>j95QCIcE}W8lON69mV|p1H@3@CC{U` z$sg%KzdH9uBunysWuC_`g2-7lu9}#N z%>sL?U{vJ|lr#>P@M!T}Sf!qZQb~hqF`A!(UR6mmQw3(J_SF}``=J(oeM8y5yJyKhpAOt5xzX1??HjgJ17k8wUf5u z6=7MxG;KGRN|+*@Vq{wW8jdIBKSQnBzGvI>F<|xo?=;GLF0BbXSI$44V&~ZKRct+h z*P{B$s=);D1zNM^G!G}IyH(fLr+PAbE^(Nrc_AE$s7?kpu6(}m|AO_j(l((vM5XWP z8E%;lErh^niGbuX;7U@Yi?_yEcdF zXAIDj2`n-*cYkt^9>V@NeK{Z6z=rUh| zAOpNxVHGNp4KOHHJV!RTpg69AC$bdcAPNLz;2#rokx7I_Q5XEb!a6}SiIV8=tgA$l zScD!efq_5}=q5z?u|Yfu5h%UDUKfC*I-g%L@$5nrOmiONqBV2&vH9czSk2d$xaU@y zb0>@aBYgh8lacsbD8fJI%LDx*3{|bYKcYW^#V69EkarGm*1T7*)eB42+EeXQ?6Bs@ zrt%GqeOgR@2D_`=Awa7Tyc%DD6T1T;9f6(0ZwsR_blW*DekpV*90?1{5O5)V-SdwL zZvq<@U;%HJOmU6_|CzluK>vvW0I#0A01s{oPi0-&L!>yNfxTxqPSp)fV3qp$JO7Sj zMjGc}zq%bRK{SpzBOYPEH2~{c;Ff&}Y!77azWtU89brQ;fJiJo$vzr;y}MPiNR5F9 zp!YB!TA@dp5&fs@6N#iTm|9%AmR?9Ck~% zCR|{(uTYqoab!`UF!SM-Qa%Z71_!!SejkNC4wF21tzaKYp>k}l?bx1Mtx7;HsDf}TzZ8%0tYXm) zGkAqv=5Xkn$0jSpANG>w_qcLca(9Ef3V?r)N0>p!YPaE$1yNNQq&KKh0>>K;v8G3& zdS$9tqHpasuPB8 zkz)6^K0X5l&WyBE_Q;4R zHM-#2Fn`+GC%SRYM<)8WTDFggmHm(8fxJv5i6sj@enZ#Xv~IMFIots>Q$_B7pYTytd10H8vt z#g`cVYh3L0?N`GgjD4UU*^t5d`kB|xh_2_hVo*Sq^v}!s=SBVV8GVOgoY*gPb7HG< zyFsixp=F6WDN`p!>f{%m#?L(72Oe+QR=Zn$c-Py1|NH~*kmw!yw5nCCYW<{g*L^fy ze|(1*>rP~>sP@R0$XE-f2*p1*)~NNCOgZw@-=*HAG%0xhWq3;Ts0^ihRCbT9*>`Ij zZ+qW4di&^>MXG&f&G|(xoHV`@7c0l}S)wLnYC@zYvd5FQv75qstD=pC{K1So@@=iE zm0yy;LvR4*EH@dE*{MaT&g?w`_oQGxS;a_z;aQ2JN z{@sRyzi9dN@PO2GLT)-CHJp?iPOe*aD-Um3AX2vLs(#!0W2;y{D!ER}uG6CH^h0OO zqjF@n-ZZcKB|;k*PDHn!kBehHTpP!n^>B%w9Bzgo#xr ze$%z{oqy^1bq`b_f570tSaxvk>Ye?V|D^Lju6}e?Y&i>gzl*2^%>Xg3JaYVvEBxjg z9!C&AJl-(eNPg6)A8s;z)Jj14v1#OKJ}-{K?ri`FUNV1xOkXihLCPh{gUMEhtEFnNeez1aoA(z zK%5~C+sLX`Q5^Us%LT7#pR%;@?;L&|g+ejm*f6XQV!X>d$YwRbja8DWFR<4Fal+*# z+=HVl9R3iYSTBY6@Z3;5HXnu<^VsC*S;amrfYOS-sC~`CK`I8mswifNwvDUNXHM04 zMrgCKtEKU05~5rj>>2GTB*mb9%u@AIR-HxS*T6PZH6o2+RgFkO+_)s-rRhtogA`{l z7>dQ>5HSN&jt>UaD6wLkQMaiWfNVfa!=+^iI;#3sAz^`PTnrN$!roX-;B&<~G#FG& z3HVeM=VA&85jk!Yui^zMxBPDfO2|mX!KSBbS)VX=F{QzXGs&5$%&fdJvTC!CuH{tsq&;;34|_7SJbaRbE_XT+)kxb(IZ#%0D;ogb?fZL*v6^bW8$F` z_h+U0A-R4?svefBhecEY>vwsym2Fi?&JNkxk+KzbR!dI5?DT);?0Mkqk(_<9v+w@s z1LrANb9vQz^Llvw(yg#ackUdQs{7^YeyMywE+0raA0p%H*4M0}?fk|OC?M|`wdW@` z?J1!PGP)q53+Wta*XBjh)&*HY-7@MHQFr!LS|1T>+C(TJ_l#yM7Z$Y&Pzj$C6kv@& z0Y_}o<{lCc_lve6ElX%vMzH7Mhems9;rrnm;S>q5W_!wzc2%Y9X-9d=nm*J9R9T%eXLKe43=$~5B$3XM>!`#)?$_LFK8f(i zgeUELW{cVBdOy4!&Y($x7$>ri&D!F2wuO<^!nll11HPm~dfVE{1JWasp0wVA&5<a ArvLx| literal 0 HcmV?d00001 diff --git a/src/python/transcribe_api/transcribe_api.py b/src/python/transcribe_api/transcribe_api.py index affad9c..15c22fc 100644 --- a/src/python/transcribe_api/transcribe_api.py +++ b/src/python/transcribe_api/transcribe_api.py @@ -134,7 +134,6 @@ async def transcribe_audio(file: UploadFile = File(...)): beam_size=WHISPER_BEAM_SIZE, language=WHISPER_LANGUAGE if WHISPER_LANGUAGE != "auto" else None, temperature=WHISPER_TEMPERATURE, - vad_filter=True, # Filter out non-speech parts vad_parameters={"min_silence_duration_ms": 500}, ) diff --git a/src/summarization/dto/summarization-options.dto.ts b/src/summarization/dto/summarization-options.dto.ts index 2af90b7..209333e 100644 --- a/src/summarization/dto/summarization-options.dto.ts +++ b/src/summarization/dto/summarization-options.dto.ts @@ -1,5 +1,5 @@ import { IsOptional, IsEnum, IsBoolean, IsString, Length, MaxLength } from "class-validator"; -import { SummarizationLanguage, SummarizationModel, SummarizationSpeed, SummaryFormat, SummaryLength } from "../enums/summarization-options.enum"; +import { STTModel, SummarizationLanguage, SummarizationModel, SummarizationSpeed, SummaryFormat, SummaryLength } from "../enums/summarization-options.enum"; export class SummarizationOptionsDto { @IsOptional() @@ -26,6 +26,10 @@ export class SummarizationOptionsDto { @IsEnum(SummarizationLanguage) lang?: SummarizationLanguage; + @IsOptional() + @IsEnum(STTModel) + sttModel?: STTModel; + @IsOptional() @IsString() @MaxLength(200) diff --git a/src/summarization/enums/summarization-options.enum.ts b/src/summarization/enums/summarization-options.enum.ts index f26a9f7..f84e0ab 100644 --- a/src/summarization/enums/summarization-options.enum.ts +++ b/src/summarization/enums/summarization-options.enum.ts @@ -27,4 +27,10 @@ export enum SummarizationLanguage { EN = 'english', AR = 'arabic', DEFAULT = 'english' +} + +export enum STTModel { + FAST_WHISPER = 'faster-whisper', + OPENAI_WHISPER = 'whisper-1', + DEFAULT = OPENAI_WHISPER } \ No newline at end of file diff --git a/src/summarization/interfaces/summarization-options.interface.ts b/src/summarization/interfaces/summarization-options.interface.ts index 4543c08..15cbcfb 100644 --- a/src/summarization/interfaces/summarization-options.interface.ts +++ b/src/summarization/interfaces/summarization-options.interface.ts @@ -1,4 +1,4 @@ -import { SummarizationLanguage, SummarizationModel, SummarizationSpeed, SummaryFormat, SummaryLength } from "../enums/summarization-options.enum"; +import { STTModel, SummarizationLanguage, SummarizationModel, SummarizationSpeed, SummaryFormat, SummaryLength } from "../enums/summarization-options.enum"; export interface SummarizationOptions { length?: SummaryLength, @@ -7,5 +7,6 @@ export interface SummarizationOptions { listen?: boolean, speed?: SummarizationSpeed, lang?: SummarizationLanguage, + sttModel?: STTModel, customInstructions?: string, } \ No newline at end of file diff --git a/src/summarization/summarization.module.ts b/src/summarization/summarization.module.ts index f00822a..7832a42 100644 --- a/src/summarization/summarization.module.ts +++ b/src/summarization/summarization.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; import { SummarizationController } from './summarization.controller'; import { SummarizationService } from './summarization.service'; +import { HttpModule } from '@nestjs/axios'; @Module({ + imports: [HttpModule], controllers: [SummarizationController], providers: [SummarizationService] }) diff --git a/src/summarization/summarization.service.ts b/src/summarization/summarization.service.ts index a7a3f57..dd4300b 100644 --- a/src/summarization/summarization.service.ts +++ b/src/summarization/summarization.service.ts @@ -24,6 +24,7 @@ import { USE_S3, } from '../utils/constants'; import { + STTModel, SummarizationModel, SummarizationSpeed, } from './enums/summarization-options.enum'; @@ -38,15 +39,16 @@ import { import { SummarizationOptions } from './interfaces/summarization-options.interface'; import { getApiKey } from '../utils/api-key.util'; import { convertTextToSpeech } from '../utils/tts.util'; -import { transcribeAudio } from '../utils/transcription.util'; +import { transcribeUsingOpenAIWhisper, transcribeUsingFastWhisper } from '../utils/transcription.util'; import { join } from 'path'; import { uploadDownloadedAudioToS3 } from '../utils/s3.util'; +import { HttpService } from '@nestjs/axios'; @Injectable() export class SummarizationService { private downloader: Downloader; - constructor() { + constructor(private readonly httpService: HttpService) { if (!USE_S3) { ensureDownloadDirectory(); } @@ -236,7 +238,13 @@ export class SummarizationService { console.log(`summarizing YOutube Video Using audio ${videoUrl} ... `); try { const audioPath = await this.downloadAudio(videoUrl); - const transcript = await transcribeAudio(audioPath, userApiKey); + let transcript: string; + if (options?.sttModel === STTModel.FAST_WHISPER) { + transcript = await transcribeUsingFastWhisper(audioPath, this.httpService); + } else { + transcript = await transcribeUsingOpenAIWhisper(audioPath, userApiKey); + } + const {summary, audioFilePath} = await this.summarizeText(transcript, options, userApiKey); const vidMetadata = await extractYouTubeVideoMetadata(videoUrl); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 359f0b8..bd9cebb 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -17,4 +17,5 @@ export const PUBLIC_DIR = '/public/audio'; export const AUDIO_FORMAT = 'mp3'; export const MAX_FILE_AGE = 1000 * 60; // 1 day -export const CORS_ORIGINS: string[] = process.env.CORS ? process.env.CORS.split(',') : ['http://localhost:3001', 'https://letssummarize.vercel.app']; \ No newline at end of file +export const CORS_ORIGINS: string[] = process.env.CORS ? process.env.CORS.split(',') : ['http://localhost:3001', 'https://letssummarize.vercel.app']; +export const FASTAPI_URL = "http://0.0.0.0:5566/transcribe/" \ No newline at end of file diff --git a/src/utils/summarization.util.ts b/src/utils/summarization.util.ts index 3a9cd74..4e5049d 100644 --- a/src/utils/summarization.util.ts +++ b/src/utils/summarization.util.ts @@ -6,6 +6,7 @@ import { SummarizationSpeed, SummarizationModel, SummarizationLanguage, + STTModel, } from '../summarization/enums/summarization-options.enum'; import { SummarizationOptions } from '../summarization/interfaces/summarization-options.interface'; import { DEEPSEEK_MAX_TOKENS, OPENAI_MAX_TOKENS } from './constants'; @@ -25,6 +26,7 @@ export function getSummarizationOptions( model: options?.model ?? SummarizationModel.DEFAULT, speed: options?.speed ?? SummarizationSpeed.DEFAULT, lang: options?.lang ?? SummarizationLanguage.DEFAULT, + sttModel: options?.sttModel ?? STTModel.DEFAULT, customInstructions: options?.customInstructions ?? undefined, }; } diff --git a/src/utils/transcription.util.ts b/src/utils/transcription.util.ts index 15c1f93..5f54f19 100644 --- a/src/utils/transcription.util.ts +++ b/src/utils/transcription.util.ts @@ -1,10 +1,14 @@ import OpenAI from 'openai'; import { existsSync } from 'fs'; -import { MAX_FILE_AGE } from './constants'; +import { FASTAPI_URL, MAX_FILE_AGE } from './constants'; import { getApiKey } from './api-key.util'; -import { createReadStream } from 'fs'; import { DEFAULT_OPENAI_API_KEY } from './constants'; import { downloadFileFromS3 } from './s3.util'; +import FormData from 'form-data'; +import { createReadStream } from 'fs'; +import axios from 'axios'; +import { HttpService } from '@nestjs/axios'; +import { firstValueFrom } from 'rxjs'; /** * Transcribes an audio file using OpenAI's Whisper model. @@ -12,7 +16,7 @@ import { downloadFileFromS3 } from './s3.util'; * @param userApiKey - Optional user-provided OpenAI API key (used when users integrate their own applications with our service). * @returns The transcribed text from the audio file. */ -export async function transcribeAudio( +export async function transcribeUsingOpenAIWhisper( audioPath: string, userApiKey?: string, ): Promise { @@ -27,7 +31,8 @@ export async function transcribeAudio( let fileStream; if (audioPath.startsWith('http')) { // If it's an S3 URL, download the file first - const { stream, cleanup: cleanupFn } = await downloadFileFromS3(audioPath); + const { stream, cleanup: cleanupFn } = + await downloadFileFromS3(audioPath); fileStream = stream; cleanup = cleanupFn; console.log('Downloaded file from S3 for transcription'); @@ -67,3 +72,29 @@ export async function transcribeAudio( throw new Error(`Failed to transcribe audio: ${error.message}`); } } + +export const transcribeUsingFastWhisper = async ( + audioFilePath: string, + httpService: HttpService, +) => { + try { + const formData = new FormData(); + formData.append('file', createReadStream(audioFilePath), 'audio.mp3'); + + const response = await firstValueFrom( + httpService.post(FASTAPI_URL, formData, { + headers: { + ...formData.getHeaders(), + }, + }), + ); + + return response.data.text; + } catch (error) { + console.error( + 'Error transcribing audio:', + error.response?.data || error.message, + ); + throw new Error('Transcription failed'); + } +}; From 5aae38e9fa7854d4ab33b61f898e04be847f6cf1 Mon Sep 17 00:00:00 2001 From: muneeb-almoliky Date: Mon, 24 Mar 2025 10:39:58 +0300 Subject: [PATCH 04/22] feat: add support for Gemini model for summarization --- .env.example | 5 +- package.json | 1 + pnpm-lock.yaml | 145 +++++++++++++++++- .../enums/summarization-options.enum.ts | 1 + src/summarization/summarization.service.ts | 6 + src/utils/constants.ts | 3 +- src/utils/summarization.util.ts | 20 +++ 7 files changed, 177 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index d6eabcc..cb2b3d7 100644 --- a/.env.example +++ b/.env.example @@ -22,4 +22,7 @@ AWS_S3_BUCKET=your-bucket-name USE_S3=false -MAX_TRANSCRIPT_TOKENS= \ No newline at end of file +MAX_TRANSCRIPT_TOKENS= + +FASTAPI_URL= +GEMENI_API_KEY= \ No newline at end of file diff --git a/package.json b/package.json index 067fd8f..a2237cb 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.758.0", + "@google/genai": "^0.6.0", "@nestjs/axios": "^4.0.0", "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35c8c1e..f994d1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@aws-sdk/client-s3': specifier: ^3.758.0 version: 3.758.0 + '@google/genai': + specifier: ^0.6.0 + version: 0.6.0 '@nestjs/axios': specifier: ^4.0.0 version: 4.0.0(@nestjs/common@11.0.12(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.8.4)(rxjs@7.8.2) @@ -58,7 +61,7 @@ importers: version: 1.4.5-lts.1 openai: specifier: ^4.87.3 - version: 4.88.0 + version: 4.88.0(ws@8.18.1) pdf-parse: specifier: ^1.1.1 version: 1.1.1 @@ -638,6 +641,10 @@ packages: resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@google/genai@0.6.0': + resolution: {integrity: sha512-wmLQM+K//DpcFjnHu10vBDbUua3W+CJjRF6nTblkNwzUEk4Tdb3WiMa53jl8J/X8h0jXOxXSrBuYrh1Rl3RxZQ==} + engines: {node: '>=18.0.0'} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1891,6 +1898,9 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + bin-version-check@5.1.0: resolution: {integrity: sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==} engines: {node: '>=12'} @@ -1941,6 +1951,9 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2321,6 +2334,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -2506,6 +2522,9 @@ packages: resolution: {integrity: sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==} engines: {node: '>=4'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -2675,6 +2694,14 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2744,6 +2771,14 @@ packages: resolution: {integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==} engines: {node: '>=18'} + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -2758,6 +2793,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -3177,6 +3216,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -3210,6 +3252,12 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + jwa@2.0.0: + resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + + jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -4399,6 +4447,18 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + ws@8.18.1: + resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml2js@0.6.2: resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} engines: {node: '>=4.0.0'} @@ -5345,6 +5405,16 @@ snapshots: '@eslint/core': 0.12.0 levn: 0.4.1 + '@google/genai@0.6.0': + dependencies: + google-auth-library: 9.15.1 + ws: 8.18.1 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -6911,6 +6981,8 @@ snapshots: base64-js@1.5.1: {} + bignumber.js@9.1.2: {} + bin-version-check@5.1.0: dependencies: bin-version: 6.0.0 @@ -6978,6 +7050,8 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@4.9.2: @@ -7324,6 +7398,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} ejs@3.1.10: @@ -7544,6 +7622,8 @@ snapshots: ext-list: 2.2.2 sort-keys-length: 1.0.1 + extend@3.0.2: {} + external-editor@3.1.0: dependencies: chardet: 0.7.0 @@ -7732,6 +7812,26 @@ snapshots: function-bind@1.1.2: {} + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -7807,6 +7907,20 @@ snapshots: globals@16.0.0: {} + google-auth-library@9.15.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.1 + gtoken: 7.1.0 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + google-logging-utils@0.0.2: {} + gopd@1.2.0: {} got@13.0.0: @@ -7827,6 +7941,14 @@ snapshots: graphemer@1.4.0: {} + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + has-flag@4.0.0: {} has-own-prop@2.0.0: {} @@ -8405,6 +8527,10 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.1.2 + json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} @@ -8434,6 +8560,17 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 + jwa@2.0.0: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.0: + dependencies: + jwa: 2.0.0 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -8685,7 +8822,7 @@ snapshots: dependencies: mimic-fn: 2.1.0 - openai@4.88.0: + openai@4.88.0(ws@8.18.1): dependencies: '@types/node': 18.19.80 '@types/node-fetch': 2.6.12 @@ -8694,6 +8831,8 @@ snapshots: form-data-encoder: 1.7.2 formdata-node: 4.4.1 node-fetch: 2.7.0 + optionalDependencies: + ws: 8.18.1 transitivePeerDependencies: - encoding @@ -9566,6 +9705,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 + ws@8.18.1: {} + xml2js@0.6.2: dependencies: sax: 1.2.1 diff --git a/src/summarization/enums/summarization-options.enum.ts b/src/summarization/enums/summarization-options.enum.ts index f84e0ab..393db89 100644 --- a/src/summarization/enums/summarization-options.enum.ts +++ b/src/summarization/enums/summarization-options.enum.ts @@ -14,6 +14,7 @@ export enum SummaryFormat { export enum SummarizationModel { OPENAI = 'openai', DEEPSEEK = 'deepseek', + GEMENI = 'gemini', DEFAULT = OPENAI, } diff --git a/src/summarization/summarization.service.ts b/src/summarization/summarization.service.ts index dd4300b..a3886dd 100644 --- a/src/summarization/summarization.service.ts +++ b/src/summarization/summarization.service.ts @@ -13,12 +13,14 @@ import { getSummarizationOptions, preparePrompt, summarizeWithDeepSeek, + summarizeWithGemini, summarizeWithOpenAi, } from '../utils/summarization.util'; import { AUDIO_FORMAT, DEFAULT_DEEPSEEK_API_KEY, DEFAULT_OPENAI_API_KEY, + DEFAULT_GEMENI_API_KEY, DOWNLOAD_DIR, MAX_FILE_AGE, USE_S3, @@ -43,6 +45,7 @@ import { transcribeUsingOpenAIWhisper, transcribeUsingFastWhisper } from '../uti import { join } from 'path'; import { uploadDownloadedAudioToS3 } from '../utils/s3.util'; import { HttpService } from '@nestjs/axios'; +import { GoogleGenAI } from "@google/genai"; @Injectable() export class SummarizationService { @@ -180,6 +183,9 @@ export class SummarizationService { if (options?.model === SummarizationModel.DEEPSEEK) { apiKey = getApiKey(userApiKey, DEFAULT_DEEPSEEK_API_KEY); summary = await summarizeWithDeepSeek(apiKey, prompt); + } else if (options?.model === SummarizationModel.GEMENI) { + apiKey = getApiKey(userApiKey, DEFAULT_GEMENI_API_KEY); + summary = await summarizeWithGemini(apiKey, prompt); } else { apiKey = getApiKey(userApiKey, DEFAULT_OPENAI_API_KEY); summary = await summarizeWithOpenAi(apiKey, prompt); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index bd9cebb..d87b278 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -4,6 +4,7 @@ config() export const DEFAULT_OPENAI_API_KEY = process.env.OPENAI_API_KEY; export const DEFAULT_DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY; +export const DEFAULT_GEMENI_API_KEY = process.env.GEMENI_API_KEY; export const OPENAI_MAX_TOKENS: number = Number(process.env.OPENAI_MAX_TOKENS) || 300; export const DEEPSEEK_MAX_TOKENS: number = Number(process.env.DEEPSEEK_MAX_TOKENS) || 1000; @@ -18,4 +19,4 @@ export const AUDIO_FORMAT = 'mp3'; export const MAX_FILE_AGE = 1000 * 60; // 1 day export const CORS_ORIGINS: string[] = process.env.CORS ? process.env.CORS.split(',') : ['http://localhost:3001', 'https://letssummarize.vercel.app']; -export const FASTAPI_URL = "http://0.0.0.0:5566/transcribe/" \ No newline at end of file +export const FASTAPI_URL = process.env.FASTAPI_URL || 'your-fastapi-url'; \ No newline at end of file diff --git a/src/utils/summarization.util.ts b/src/utils/summarization.util.ts index 4e5049d..f81fcf1 100644 --- a/src/utils/summarization.util.ts +++ b/src/utils/summarization.util.ts @@ -10,6 +10,7 @@ import { } from '../summarization/enums/summarization-options.enum'; import { SummarizationOptions } from '../summarization/interfaces/summarization-options.interface'; import { DEEPSEEK_MAX_TOKENS, OPENAI_MAX_TOKENS } from './constants'; +import { GoogleGenAI } from '@google/genai'; /** * Creates a complete SummarizationOptions object with default values for missing options @@ -203,3 +204,22 @@ export async function summarizeWithDeepSeek( throw new Error(`Failed to summarize text: ${error.message}`); } } + +export async function summarizeWithGemini( + apiKey: string, + prompt: string, +): Promise { + const googleAI = new GoogleGenAI({ apiKey: apiKey }); + console.log("Summarizing with Gemini ...") + + try { + const response = await googleAI.models.generateContent({ + model: "gemini-2.0-flash", + contents: prompt, + }); + console.log(response.text); + return response.text || "Could not generate a summary"; + } catch (error) { + throw new Error(`Failed to summarize text: ${error.message}`) + } +} From c14da723f3023d4adabb627b167d257e2d393b30 Mon Sep 17 00:00:00 2001 From: muneeb-almoliky Date: Mon, 24 Mar 2025 11:35:30 +0300 Subject: [PATCH 05/22] feat: enhance API key guard to allow API users of Gemini and Fast-Whisper without an API key --- src/summarization/guards/api-key.guard.ts | 17 +++++++++++++++++ src/summarization/summarization.service.ts | 3 +-- src/utils/summarization.util.ts | 9 ++++++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/summarization/guards/api-key.guard.ts b/src/summarization/guards/api-key.guard.ts index d600d58..763798a 100644 --- a/src/summarization/guards/api-key.guard.ts +++ b/src/summarization/guards/api-key.guard.ts @@ -1,5 +1,6 @@ import { CanActivate, ExecutionContext, UnauthorizedException } from "@nestjs/common"; import { Request } from "express"; +import { STTModel, SummarizationModel, SummarizationSpeed } from "../enums/summarization-options.enum"; export class ApiKeyGuard implements CanActivate { @@ -13,11 +14,27 @@ export class ApiKeyGuard implements CanActivate { return true; } + const { options } = request.body; + const { model, sttModel, speed, listen } = options; + + + // Special case for Gemini with fast speed and no listen + if (model === SummarizationModel.GEMENI && speed === SummarizationSpeed.FAST && !listen) { + return true; + } + + // Special case for Gemini with slow speed and Fast-Whisper STT model and no listen + if(model === SummarizationModel.GEMENI && speed === SummarizationSpeed.SLOW && sttModel === STTModel.FAST_WHISPER && !listen) { + return true; + } + const authHeader = request.headers.authorization; if(!authHeader || !authHeader.startsWith('Bearer ')) { throw new UnauthorizedException("Missing API key") } + + const apiKey = authHeader.split(' ')[1]?.trim(); if(!apiKey) { diff --git a/src/summarization/summarization.service.ts b/src/summarization/summarization.service.ts index a3886dd..7b5216d 100644 --- a/src/summarization/summarization.service.ts +++ b/src/summarization/summarization.service.ts @@ -184,8 +184,7 @@ export class SummarizationService { apiKey = getApiKey(userApiKey, DEFAULT_DEEPSEEK_API_KEY); summary = await summarizeWithDeepSeek(apiKey, prompt); } else if (options?.model === SummarizationModel.GEMENI) { - apiKey = getApiKey(userApiKey, DEFAULT_GEMENI_API_KEY); - summary = await summarizeWithGemini(apiKey, prompt); + summary = await summarizeWithGemini(prompt); } else { apiKey = getApiKey(userApiKey, DEFAULT_OPENAI_API_KEY); summary = await summarizeWithOpenAi(apiKey, prompt); diff --git a/src/utils/summarization.util.ts b/src/utils/summarization.util.ts index f81fcf1..b8a10f9 100644 --- a/src/utils/summarization.util.ts +++ b/src/utils/summarization.util.ts @@ -9,7 +9,7 @@ import { STTModel, } from '../summarization/enums/summarization-options.enum'; import { SummarizationOptions } from '../summarization/interfaces/summarization-options.interface'; -import { DEEPSEEK_MAX_TOKENS, OPENAI_MAX_TOKENS } from './constants'; +import { DEEPSEEK_MAX_TOKENS, DEFAULT_GEMENI_API_KEY, OPENAI_MAX_TOKENS } from './constants'; import { GoogleGenAI } from '@google/genai'; /** @@ -206,10 +206,13 @@ export async function summarizeWithDeepSeek( } export async function summarizeWithGemini( - apiKey: string, prompt: string, ): Promise { - const googleAI = new GoogleGenAI({ apiKey: apiKey }); + if (!DEFAULT_GEMENI_API_KEY) { + throw new Error("Gemint API key is not provided in the .env"); + } + + const googleAI = new GoogleGenAI({ apiKey: DEFAULT_GEMENI_API_KEY }); console.log("Summarizing with Gemini ...") try { From 4c9cd9af68c95acaab88a882add22428e415855a Mon Sep 17 00:00:00 2001 From: muneeb-almoliky Date: Tue, 25 Mar 2025 02:16:35 +0300 Subject: [PATCH 06/22] refactor: switch back to yt-dlp-exec from ytdl-mp3 --- package.json | 4 +- pnpm-lock.yaml | 49 ++++++++++++++++++++++ src/summarization/summarization.service.ts | 18 ++++---- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index a2237cb..b569455 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "youtube-transcript": "^1.2.1", + "yt-dlp-exec": "^1.0.2", "ytdl-mp3": "^5.2.2" }, "devDependencies": { @@ -96,7 +97,8 @@ "pnpm": { "onlyBuiltDependencies": [ "ffmpeg-static", - "ytdl-mp3" + "ytdl-mp3", + "yt-dlp-exec" ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f994d1b..b1a44ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: youtube-transcript: specifier: ^1.2.1 version: 1.2.1 + yt-dlp-exec: + specifier: ^1.0.2 + version: 1.0.2 ytdl-mp3: specifier: ^5.2.2 version: 5.2.2 @@ -2222,6 +2225,10 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + dargs@7.0.0: + resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} + engines: {node: '>=8'} + dargs@8.1.0: resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} engines: {node: '>=12'} @@ -3016,6 +3023,10 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-unix@2.0.10: + resolution: {integrity: sha512-CcasZSEOQUoE7JHy56se4wyRhdJfjohuMWYmceSTaDY4naKyd1fpLiY8rJsIT6AKfVstQAhHJOfPx7jcUxK61Q==} + engines: {node: '>= 12'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -3481,6 +3492,11 @@ packages: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -3518,6 +3534,15 @@ packages: node-ensure@0.0.0: resolution: {integrity: sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==} + node-fetch@2.6.13: + resolution: {integrity: sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -4514,6 +4539,10 @@ packages: resolution: {integrity: sha512-TvEGkBaajKw+B6y91ziLuBLsa5cawgowou+Bk0ciGpjELDfAzSzTGXaZmeSSkUeknCPpEr/WGApOHDwV7V+Y9Q==} engines: {node: '>=18.0.0'} + yt-dlp-exec@1.0.2: + resolution: {integrity: sha512-swKtruQmGBs+Xrxy0wCZ2FxCT167EpBYIWdj/klTzNB2HrHng/qFlKo/C0WVlopbww8/uMIGQR6grXQ2ObcrAw==} + engines: {node: '>= 12'} + ytdl-mp3@5.2.2: resolution: {integrity: sha512-CYrjnsynoUUOQlYT8ql8yJXMWyL7qmjY6LvEmo9B/FlEUwYfwqMv6/XYOgkNoRPEj8XJXnFUPPyhhccb7yIQ5Q==} engines: {node: '>=20.0.0'} @@ -7323,6 +7352,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + dargs@7.0.0: {} + dargs@8.1.0: {} debug@3.2.7: @@ -8137,6 +8168,8 @@ snapshots: is-unicode-supported@0.1.0: {} + is-unix@2.0.10: {} + isarray@1.0.0: {} isexe@2.0.0: {} @@ -8754,6 +8787,8 @@ snapshots: dependencies: minimist: 1.2.8 + mkdirp@1.0.4: {} + ms@2.1.2: {} ms@2.1.3: {} @@ -8786,6 +8821,10 @@ snapshots: node-ensure@0.0.0: {} + node-fetch@2.6.13: + dependencies: + whatwg-url: 5.0.0 + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -9749,6 +9788,16 @@ snapshots: youtube-transcript@1.2.1: {} + yt-dlp-exec@1.0.2: + dependencies: + dargs: 7.0.0 + execa: 5.1.1 + is-unix: 2.0.10 + mkdirp: 1.0.4 + node-fetch: 2.6.13 + transitivePeerDependencies: + - encoding + ytdl-mp3@5.2.2: dependencies: '@distube/ytdl-core': 4.16.5 diff --git a/src/summarization/summarization.service.ts b/src/summarization/summarization.service.ts index 7b5216d..59e3bf4 100644 --- a/src/summarization/summarization.service.ts +++ b/src/summarization/summarization.service.ts @@ -46,6 +46,7 @@ import { join } from 'path'; import { uploadDownloadedAudioToS3 } from '../utils/s3.util'; import { HttpService } from '@nestjs/axios'; import { GoogleGenAI } from "@google/genai"; +import ytDlpExec from 'yt-dlp-exec'; @Injectable() export class SummarizationService { @@ -55,10 +56,6 @@ export class SummarizationService { if (!USE_S3) { ensureDownloadDirectory(); } - this.downloader = new Downloader({ - getTags: false, - outputDir: DOWNLOAD_DIR, - }); } /** @@ -280,14 +277,21 @@ export class SummarizationService { const startTime = new Date(); try { - // Download the audio using ytdl-mp3 - const result = await this.downloader.downloadSong(videoUrl); + + await ytDlpExec(videoUrl, { + extractAudio: true, + audioFormat: AUDIO_FORMAT, + output: audioPath, + noCheckCertificate: true, + noWarnings: true, + preferFreeFormats: true, + }); const endTime = new Date(); const duration = (endTime.getTime() - startTime.getTime()) / 1000; // Rename the file because ytdl-mp3 uses the video tile as the file name by default - await fsPromises.rename(result.outputFile, audioPath); + // await fsPromises.rename(result.outputFile, audioPath); if (!existsSync(audioPath)) { throw new Error('Audio file was not created.'); From 7f4442fa232fd983fffd37dcb37e749e47f9a72a Mon Sep 17 00:00:00 2001 From: muneeb-almoliky Date: Tue, 25 Mar 2025 03:04:47 +0300 Subject: [PATCH 07/22] feat: use text language for summary if 'lang' is default or missing --- .../transcribe_api.cpython-311.pyc | Bin 9840 -> 9792 bytes src/python/transcribe_api/transcribe_api.py | 4 +- src/utils/summarization.util.ts | 35 ++++++------------ 3 files changed, 14 insertions(+), 25 deletions(-) rename src/python/{ => transcribe_api}/__pycache__/transcribe_api.cpython-311.pyc (72%) diff --git a/src/python/__pycache__/transcribe_api.cpython-311.pyc b/src/python/transcribe_api/__pycache__/transcribe_api.cpython-311.pyc similarity index 72% rename from src/python/__pycache__/transcribe_api.cpython-311.pyc rename to src/python/transcribe_api/__pycache__/transcribe_api.cpython-311.pyc index f570b13bfe89301a1cb179f1187d8cbd17db839a..975372b45caefd1307cbd56aac3ab9ce1e2cafa1 100644 GIT binary patch delta 521 zcmez1bHImpIWI340}zP6d6+(FBkvJ5M#IgQ*g{ztizjPvCvL9d-ptHswpou~m)RhN zqeKg&6arENQrI#X*9b0SWnfqh#1Ig}#K2I?R?D6uv_Nd~M-EX&;mJpZiaAAUI8#Jt zv!sYkb`&;M7O!E?5{K!S&5$Ai>_z;LM9tGgmo71#=`rIise^=3X%m#?80I7cjDg0qrUZoZKk6pHXqLqtqlG zX`sL@maP2DJfQFsso9KilN+QxxJ!Y;2|!%@lX>%H>1&M4H$){j?~%1(QsVVtV>rXc z<*USeM!=ERSC;v#EGv+6R*BhHmEo+av7f|bDTUQ+NkGj-Ih#)_s4y~?PX3^1$~a}S Invxd_061Q8$N&HU delta 547 zcmX@$^TCIAIWI340}$N(@F4xzM&2WAjHa6}v4yfQrcBo0PTV|$dkHgR)Mi_LU1qBi zEs#P8ND;_p2C<6Rkwxkih<_JO%hROG@YC$qM}q{1~Y_#A)5unE^;ohM2MAG1C^yK$}eMNU|0>r5WvV# z!&PDn<1sK~$xoINvt?GOVVQhDK!h!rL7Ab%VRC?|F|%Tc^W;;aax5uIYm_Ii5S3?E z0Sh?u;?zAkPmGgMb#sN72O}#>VrfbK=KbOe8QH>twiN|UPLtfvs5DtkYLXzx_eIh` z;ucF*er6s}?ugWE#`wtz(jMGpK;Z-+F6Ly}yjl7hqwrl#%N=2tG@UPKI$sr4y&$T3 zLS%D~tPPWrypIIK83`_56Xr8Yj=aA5%xCpkft<4@%)aIfXU&cMv?jljU(J>bG^{9h X^K1nbM#i$q2NX>ir*8hH=*0p63!Hg2 diff --git a/src/python/transcribe_api/transcribe_api.py b/src/python/transcribe_api/transcribe_api.py index 15c22fc..9e28d0f 100644 --- a/src/python/transcribe_api/transcribe_api.py +++ b/src/python/transcribe_api/transcribe_api.py @@ -26,7 +26,7 @@ ) # Max seconds to wait for model # πŸŽ™οΈ Whisper Transcription Configuration -WHISPER_BEAM_SIZE = int(os.getenv("WHISPER_BEAM_SIZE", "5")) +WHISPER_BEAM_SIZE = int(os.getenv("WHISPER_BEAM_SIZE", "1")) WHISPER_LANGUAGE = os.getenv("WHISPER_LANGUAGE", "en") WHISPER_TEMPERATURE = float(os.getenv("WHISPER_TEMPERATURE", "0.3")) @@ -132,7 +132,7 @@ async def transcribe_audio(file: UploadFile = File(...)): segments, info = model.transcribe( temp_file_path, beam_size=WHISPER_BEAM_SIZE, - language=WHISPER_LANGUAGE if WHISPER_LANGUAGE != "auto" else None, + language=None, temperature=WHISPER_TEMPERATURE, vad_parameters={"min_silence_duration_ms": 500}, ) diff --git a/src/utils/summarization.util.ts b/src/utils/summarization.util.ts index b8a10f9..23d9154 100644 --- a/src/utils/summarization.util.ts +++ b/src/utils/summarization.util.ts @@ -79,35 +79,24 @@ export function validateSummarizationOptions( export function preparePrompt(options: SummarizationOptions, text: string) { const { length, format, lang } = getSummarizationOptions(options); let prompt: string; + let language: string; + + if (!lang || lang === SummarizationLanguage.DEFAULT) { + language = "the same language as the text"; + } else { + language = lang; + } if ( - options?.customInstructions && - options?.lang === SummarizationLanguage.DEFAULT - ) { - prompt = `Summarize the following text based on these special requirements: ${options.customInstructions}`; - } else if ( - options?.customInstructions && - options?.lang !== SummarizationLanguage.DEFAULT - ) { - prompt = `Summarize the following text in ${lang} based on these special requirements: ${options.customInstructions}`; + options?.customInstructions) { + prompt = `Summarize the following text in ${language} based on these special requirements: ${options.customInstructions}`; } else { if ( - options?.format === SummaryFormat.DEFAULT && - options?.lang === SummarizationLanguage.DEFAULT - ) { - prompt = `Summarize the following text in a ${length} length. Focus on the key points, main arguments, and important details. Ensure the summary is coherent and complete`; - } else if ( - options?.format === SummaryFormat.DEFAULT && - options?.lang !== SummarizationLanguage.DEFAULT - ) { - prompt = `Summarize the following text in a ${length} length, in ${lang}. Focus on the key points, main arguments, and important details. Ensure the summary is coherent and complete`; - } else if ( - options?.format !== SummaryFormat.DEFAULT && - options?.lang === SummarizationLanguage.DEFAULT + options?.format === SummaryFormat.DEFAULT ) { - prompt = `Summarize the following text in a ${length} length, in ${format} style. Focus on the key points, main arguments, and important details. Ensure the summary is coherent and complete`; + prompt = `Summarize the following text in a ${length} length in ${language}. Focus on the key points, main arguments, and important details. Ensure the summary is coherent and complete`; } else { - prompt = `Summarize the following text in a ${length} length, in ${format} style in ${lang}. Focus on the key points, main arguments, and important details. Ensure the summary is coherent and complete`; + prompt = `Summarize the following text in a ${length} length, in ${format} style in ${language}. Focus on the key points, main arguments, and important details. Ensure the summary is coherent and complete`; } } From 0336c5f9beccd63ba16979461181912b812586b8 Mon Sep 17 00:00:00 2001 From: muneeb-almoliky Date: Tue, 25 Mar 2025 03:33:52 +0300 Subject: [PATCH 08/22] refactor: change default language from 'english' to 'default' --- src/summarization/enums/summarization-options.enum.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/summarization/enums/summarization-options.enum.ts b/src/summarization/enums/summarization-options.enum.ts index 393db89..5b64975 100644 --- a/src/summarization/enums/summarization-options.enum.ts +++ b/src/summarization/enums/summarization-options.enum.ts @@ -27,7 +27,7 @@ export enum SummarizationSpeed { export enum SummarizationLanguage { EN = 'english', AR = 'arabic', - DEFAULT = 'english' + DEFAULT = 'default' } export enum STTModel { From 0f9291cd08b3cc0d85f72440a877a76803f8d06b Mon Sep 17 00:00:00 2001 From: muneeb-almoliky Date: Wed, 26 Mar 2025 02:50:13 +0300 Subject: [PATCH 09/22] fix: added referer and user-agent headers to ytDlpExec to avoid request blocking --- src/summarization/summarization.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/summarization/summarization.service.ts b/src/summarization/summarization.service.ts index 59e3bf4..7979454 100644 --- a/src/summarization/summarization.service.ts +++ b/src/summarization/summarization.service.ts @@ -285,6 +285,8 @@ export class SummarizationService { noCheckCertificate: true, noWarnings: true, preferFreeFormats: true, + referer: 'youtube.com', + userAgent: 'googlebot' }); const endTime = new Date(); From a3aacba642f603e9958ad76c275ab6c148aae611 Mon Sep 17 00:00:00 2001 From: muneeb-almoliky Date: Wed, 26 Mar 2025 03:29:39 +0300 Subject: [PATCH 10/22] fix: improve error handling for summarization APIs --- src/utils/summarization.util.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/summarization.util.ts b/src/utils/summarization.util.ts index 23d9154..b2d5c39 100644 --- a/src/utils/summarization.util.ts +++ b/src/utils/summarization.util.ts @@ -149,7 +149,7 @@ export async function summarizeWithOpenAi( console.error( `Summarization failed at ${failTime.toISOString()}. Time taken: ${duration} seconds. Error: ${error.message}`, ); - throw new Error(`Failed to summarize text: ${error.message}`); + return `OpenAI summarization failed: ${error?.response?.data?.error || error.message}`; } } @@ -190,7 +190,7 @@ export async function summarizeWithDeepSeek( response.choices[0]?.message?.content || 'Could not generate a summary.' ); } catch (error) { - throw new Error(`Failed to summarize text: ${error.message}`); + return `DeepSeek summarization failed: ${error?.response?.data?.error || error.message}` } } @@ -212,6 +212,6 @@ export async function summarizeWithGemini( console.log(response.text); return response.text || "Could not generate a summary"; } catch (error) { - throw new Error(`Failed to summarize text: ${error.message}`) + return `Gemini summarization failed: ${error?.response?.data?.error || error.message}`; } } From e7a333bdfb22c7f8c9ceafa0c170999665af8ef9 Mon Sep 17 00:00:00 2001 From: Khaled Alshibani <127689031+khaledsAlshibani@users.noreply.github.com> Date: Wed, 26 Mar 2025 04:09:09 +0300 Subject: [PATCH 11/22] ci(deploy): add workflow for auto deploy on prod branch --- .github/workflows/deploy.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..1591b9b --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,31 @@ +name: Deploy API + +on: + push: + branches: + - prod + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.PROD_SSH_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H ${{ secrets.PROD_HOST }} >> ~/.ssh/known_hosts + + - name: Deploy via SSH + run: | + ssh ${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }} << 'EOF' + cd /path/to/your/app + git pull origin prod + pnpm install + pnpm build + pm2 restart letssummarize-api + EOF From df27c38a0f6864188fbd9de02e1a65b2c9018b70 Mon Sep 17 00:00:00 2001 From: muneeb-almoliky Date: Wed, 26 Mar 2025 08:20:56 +0300 Subject: [PATCH 12/22] chore: test auto deployment --- src/summarization/summarization.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/summarization/summarization.service.ts b/src/summarization/summarization.service.ts index 7979454..f008ff2 100644 --- a/src/summarization/summarization.service.ts +++ b/src/summarization/summarization.service.ts @@ -132,7 +132,7 @@ export class SummarizationService { } else { console.error('error ', error); throw new BadRequestException( - 'This video does not have a YouTube transcript. Please use SLOW mode instead. Or check your network connection', + 'This video does not have a YouTube transcript. Please use SLOW mode instead. Or check your network connection.', ); } } From 42cd129d8b3357d1b289f915303fb045bb3babae Mon Sep 17 00:00:00 2001 From: Khaled Alshibani <127689031+khaledsAlshibani@users.noreply.github.com> Date: Wed, 26 Mar 2025 12:49:44 +0300 Subject: [PATCH 13/22] ci(prod): update dir path --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1591b9b..937afc6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,7 +23,7 @@ jobs: - name: Deploy via SSH run: | ssh ${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }} << 'EOF' - cd /path/to/your/app + cd /api git pull origin prod pnpm install pnpm build From ed8d036d2ceea8bcfa4814c81b4d63799a228746 Mon Sep 17 00:00:00 2001 From: Khaled Alshibani <127689031+khaledsAlshibani@users.noreply.github.com> Date: Wed, 26 Mar 2025 12:54:39 +0300 Subject: [PATCH 14/22] ci(prod): update dir path --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 937afc6..c95b9a9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,7 +23,7 @@ jobs: - name: Deploy via SSH run: | ssh ${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }} << 'EOF' - cd /api + cd ~/api git pull origin prod pnpm install pnpm build From 76d95fc42a84fa45614cc8e559b5895092b49376 Mon Sep 17 00:00:00 2001 From: muneeb-almoliky Date: Thu, 27 Mar 2025 03:50:42 +0300 Subject: [PATCH 15/22] refactor(video): switch to youtubei.js for transcript fetching --- package.json | 1 + pnpm-lock.yaml | 39 +++++++++++++++++++++++++++++++++++++++ src/utils/video.util.ts | 26 ++++++++++++++++++++------ 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b569455..17283a4 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "youtube-transcript": "^1.2.1", + "youtubei.js": "^13.3.0", "yt-dlp-exec": "^1.0.2", "ytdl-mp3": "^5.2.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1a44ff..44e5133 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: youtube-transcript: specifier: ^1.2.1 version: 1.2.1 + youtubei.js: + specifier: ^13.3.0 + version: 13.3.0 yt-dlp-exec: specifier: ^1.0.2 version: 1.0.2 @@ -521,6 +524,9 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@bufbuild/protobuf@2.2.5': + resolution: {integrity: sha512-/g5EzJifw5GF8aren8wZ/G5oMuPoGeS6MQD3ca8ddcvdXR5UELUfdTZITCGNhNXynY/AYl3Z4plmxdj/tRl/hQ==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -644,6 +650,10 @@ packages: resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + '@google/genai@0.6.0': resolution: {integrity: sha512-wmLQM+K//DpcFjnHu10vBDbUua3W+CJjRF6nTblkNwzUEk4Tdb3WiMa53jl8J/X8h0jXOxXSrBuYrh1Rl3RxZQ==} engines: {node: '>=18.0.0'} @@ -3203,6 +3213,9 @@ packages: node-notifier: optional: true + jintr@3.3.0: + resolution: {integrity: sha512-ZsaajJ4Hr5XR0tSPhOZOTjFhxA0qscKNSOs41NRjx7ZOGwpfdp8NKIBEUtvUPbA37JXyv1sJlgeOOZHjr3h76Q==} + jiti@2.4.2: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true @@ -4337,6 +4350,10 @@ packages: undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + undici@7.5.0: resolution: {integrity: sha512-NFQG741e8mJ0fLQk90xKxFdaSM7z4+IQpAgsFI36bCDY9Z2+aXXZjVy2uUksMouWfMI9+w5ejOq5zYYTBCQJDQ==} engines: {node: '>=20.18.1'} @@ -4539,6 +4556,9 @@ packages: resolution: {integrity: sha512-TvEGkBaajKw+B6y91ziLuBLsa5cawgowou+Bk0ciGpjELDfAzSzTGXaZmeSSkUeknCPpEr/WGApOHDwV7V+Y9Q==} engines: {node: '>=18.0.0'} + youtubei.js@13.3.0: + resolution: {integrity: sha512-tbl7rxltpgKoSsmfGUe9JqWUAzv6HFLqrOn0N85EbTn5DLt24EXrjClnXdxyr3PBARMJ3LC4vbll100a0ABsYw==} + yt-dlp-exec@1.0.2: resolution: {integrity: sha512-swKtruQmGBs+Xrxy0wCZ2FxCT167EpBYIWdj/klTzNB2HrHng/qFlKo/C0WVlopbww8/uMIGQR6grXQ2ObcrAw==} engines: {node: '>= 12'} @@ -5254,6 +5274,8 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@bufbuild/protobuf@2.2.5': {} + '@colors/colors@1.5.0': optional: true @@ -5434,6 +5456,8 @@ snapshots: '@eslint/core': 0.12.0 levn: 0.4.1 + '@fastify/busboy@2.1.1': {} + '@google/genai@0.6.0': dependencies: google-auth-library: 9.15.1 @@ -8543,6 +8567,10 @@ snapshots: - supports-color - ts-node + jintr@3.3.0: + dependencies: + acorn: 8.14.1 + jiti@2.4.2: {} jmespath@0.16.0: {} @@ -9592,6 +9620,10 @@ snapshots: undici-types@6.20.0: {} + undici@5.29.0: + dependencies: + '@fastify/busboy': 2.1.1 + undici@7.5.0: {} unicorn-magic@0.1.0: {} @@ -9788,6 +9820,13 @@ snapshots: youtube-transcript@1.2.1: {} + youtubei.js@13.3.0: + dependencies: + '@bufbuild/protobuf': 2.2.5 + jintr: 3.3.0 + tslib: 2.8.1 + undici: 5.29.0 + yt-dlp-exec@1.0.2: dependencies: dargs: 7.0.0 diff --git a/src/utils/video.util.ts b/src/utils/video.util.ts index 5bd3e8a..754a886 100644 --- a/src/utils/video.util.ts +++ b/src/utils/video.util.ts @@ -98,13 +98,27 @@ export async function extractYouTubeVideoMetadata( */ export async function fetchYouTubeTranscript(videoId: string): Promise { try { - const transcriptItems = await YoutubeTranscript.fetchTranscript(videoId); - const fullTranscript = transcriptItems - .map((item) => item.text.trim()) - .filter((text) => text.length > 0) - .join(' '); + // Replaced with youtubei.js + // const transcriptItems = await YoutubeTranscript.fetchTranscript(videoId); + // const fullTranscript = transcriptItems + // .map((item) => item.text.trim()) + // .filter((text) => text.length > 0) + // .join(' '); + const youtubei = await import ('youtubei.js'); + const Innertube = youtubei.Innertube; + const youtube = await Innertube.create({ + lang: 'en', + location: 'US', + retrieve_player: false, + }); - const words = fullTranscript.split(' '); + const info = await youtube.getInfo(videoId); + const transcriptData = await info.getTranscript(); + + const segments = transcriptData?.transcript?.content?.body?.initial_segments || []; + const fullTranscript = segments.map(segment => segment.snippet.text).join(' '); + // const words = fullTranscript.split(' '); + const words = fullTranscript.split(/\s+/); const safeLength = Math.floor(MAX_TRANSCRIPT_TOKENS / 4); return words.slice(0, safeLength).join(' '); } catch (error) { From 1361d6b5ce6cc0c7bdc2f0126f3f311b3ad61c59 Mon Sep 17 00:00:00 2001 From: muneeb-almoliky Date: Thu, 27 Mar 2025 06:26:00 +0300 Subject: [PATCH 16/22] chore: test auto deployment --- src/summarization/summarization.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/summarization/summarization.controller.ts b/src/summarization/summarization.controller.ts index c1d2159..3ee5837 100644 --- a/src/summarization/summarization.controller.ts +++ b/src/summarization/summarization.controller.ts @@ -65,6 +65,6 @@ export class SummarizationController { @Get() getMessage() { - return 'Hello there.'; + return 'Hello World.'; } } From 09c2e53a369470b31cb6b258148164b496888acd Mon Sep 17 00:00:00 2001 From: muneeb-almoliky Date: Fri, 28 Mar 2025 01:14:53 +0300 Subject: [PATCH 17/22] fix(download): force generic extractor to bypass YouTube restrictions --- src/summarization/summarization.service.ts | 5 +++-- src/utils/video.util.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/summarization/summarization.service.ts b/src/summarization/summarization.service.ts index f008ff2..891ef38 100644 --- a/src/summarization/summarization.service.ts +++ b/src/summarization/summarization.service.ts @@ -286,7 +286,8 @@ export class SummarizationService { noWarnings: true, preferFreeFormats: true, referer: 'youtube.com', - userAgent: 'googlebot' + userAgent: 'googlebot', + forceGenericExtractor: true, }); const endTime = new Date(); @@ -321,7 +322,7 @@ export class SummarizationService { const failTime = new Date(); const duration = (failTime.getTime() - startTime.getTime()) / 1000; console.error( - `Download failed at ${failTime.toISOString()}. Time taken: ${duration} seconds. Error: ${error.message}`, + `Download failed at ${failTime.toISOString()} Time taken: ${duration} seconds. Error: ${error.message}`, ); throw new Error('Failed to download audio'); } diff --git a/src/utils/video.util.ts b/src/utils/video.util.ts index 754a886..b60514f 100644 --- a/src/utils/video.util.ts +++ b/src/utils/video.util.ts @@ -122,7 +122,7 @@ export async function fetchYouTubeTranscript(videoId: string): Promise { const safeLength = Math.floor(MAX_TRANSCRIPT_TOKENS / 4); return words.slice(0, safeLength).join(' '); } catch (error) { - throw new Error( + throw new Error( `Could not fetch transcript from YouTube: ${error.message}`, ); } From f95f77ecd35bd34bc0c7fb01f24f14e1856dd951 Mon Sep 17 00:00:00 2001 From: muneeb-almoliky Date: Fri, 28 Mar 2025 02:16:58 +0300 Subject: [PATCH 18/22] fix: authenticate yt-dlp with GVS using PO token --- src/summarization/interfaces/custom-yt-flags.ts | 5 +++++ src/summarization/summarization.service.ts | 10 ++++++---- src/utils/constants.ts | 4 +++- 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 src/summarization/interfaces/custom-yt-flags.ts diff --git a/src/summarization/interfaces/custom-yt-flags.ts b/src/summarization/interfaces/custom-yt-flags.ts new file mode 100644 index 0000000..5fb150d --- /dev/null +++ b/src/summarization/interfaces/custom-yt-flags.ts @@ -0,0 +1,5 @@ +import { YtFlags } from 'yt-dlp-exec'; + +export type CustomYtFlags = YtFlags & { + extractorArgs?: string; +}; diff --git a/src/summarization/summarization.service.ts b/src/summarization/summarization.service.ts index 891ef38..3db2d6b 100644 --- a/src/summarization/summarization.service.ts +++ b/src/summarization/summarization.service.ts @@ -24,6 +24,7 @@ import { DOWNLOAD_DIR, MAX_FILE_AGE, USE_S3, + PO_TOKEN, } from '../utils/constants'; import { STTModel, @@ -46,7 +47,8 @@ import { join } from 'path'; import { uploadDownloadedAudioToS3 } from '../utils/s3.util'; import { HttpService } from '@nestjs/axios'; import { GoogleGenAI } from "@google/genai"; -import ytDlpExec from 'yt-dlp-exec'; +import ytDlpExec, { YtFlags } from 'yt-dlp-exec'; +import { CustomYtFlags } from './interfaces/custom-yt-flags'; @Injectable() export class SummarizationService { @@ -277,7 +279,7 @@ export class SummarizationService { const startTime = new Date(); try { - + await ytDlpExec(videoUrl, { extractAudio: true, audioFormat: AUDIO_FORMAT, @@ -287,8 +289,8 @@ export class SummarizationService { preferFreeFormats: true, referer: 'youtube.com', userAgent: 'googlebot', - forceGenericExtractor: true, - }); + extractorArgs: `youtube:po_token=web.gvs+${PO_TOKEN}` + } as CustomYtFlags); const endTime = new Date(); const duration = (endTime.getTime() - startTime.getTime()) / 1000; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index d87b278..3792c19 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -19,4 +19,6 @@ export const AUDIO_FORMAT = 'mp3'; export const MAX_FILE_AGE = 1000 * 60; // 1 day export const CORS_ORIGINS: string[] = process.env.CORS ? process.env.CORS.split(',') : ['http://localhost:3001', 'https://letssummarize.vercel.app']; -export const FASTAPI_URL = process.env.FASTAPI_URL || 'your-fastapi-url'; \ No newline at end of file +export const FASTAPI_URL = process.env.FASTAPI_URL || 'your-fastapi-url'; + +export const PO_TOKEN = process.env.PO_TOKEN; \ No newline at end of file From 89f8d39208cdfbd34cd88d4cddaef99fa682b021 Mon Sep 17 00:00:00 2001 From: muneeb-almoliky Date: Fri, 28 Mar 2025 03:28:23 +0300 Subject: [PATCH 19/22] fix: add YouTube session cookies for authentication --- src/summarization/summarization.service.ts | 4 +++- src/utils/constants.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/summarization/summarization.service.ts b/src/summarization/summarization.service.ts index 3db2d6b..fab563c 100644 --- a/src/summarization/summarization.service.ts +++ b/src/summarization/summarization.service.ts @@ -25,6 +25,7 @@ import { MAX_FILE_AGE, USE_S3, PO_TOKEN, + COOKIES_PATH, } from '../utils/constants'; import { STTModel, @@ -289,7 +290,8 @@ export class SummarizationService { preferFreeFormats: true, referer: 'youtube.com', userAgent: 'googlebot', - extractorArgs: `youtube:po_token=web.gvs+${PO_TOKEN}` + extractorArgs: `youtube:po_token=web.gvs+${PO_TOKEN}`, + cookies: COOKIES_PATH } as CustomYtFlags); const endTime = new Date(); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 3792c19..ff2008c 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,4 +1,4 @@ -import { join } from 'path'; +import path, { join } from 'path'; import { config } from 'dotenv'; config() @@ -18,6 +18,8 @@ export const PUBLIC_DIR = '/public/audio'; export const AUDIO_FORMAT = 'mp3'; export const MAX_FILE_AGE = 1000 * 60; // 1 day +export const COOKIES_PATH = path.resolve(process.cwd(), 'cookies.txt'); + export const CORS_ORIGINS: string[] = process.env.CORS ? process.env.CORS.split(',') : ['http://localhost:3001', 'https://letssummarize.vercel.app']; export const FASTAPI_URL = process.env.FASTAPI_URL || 'your-fastapi-url'; From 172a497d9435e8d388d3957d6295e76aa5eef114 Mon Sep 17 00:00:00 2001 From: Khaled Alshibani <127689031+khaledsAlshibani@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:04:05 +0300 Subject: [PATCH 20/22] feat: add abillity to add multible allowed origins --- .env.example | 6 ++-- docs/authentication.md | 18 +++++----- docs/getting-started.md | 12 +++---- src/summarization/guards/api-key.guard.ts | 6 ++-- src/utils/constants.ts | 4 ++- test/http/test.http | 43 ++++++++++++----------- 6 files changed, 46 insertions(+), 43 deletions(-) diff --git a/.env.example b/.env.example index cb2b3d7..3bebfa5 100644 --- a/.env.example +++ b/.env.example @@ -6,9 +6,9 @@ OPENAI_API_KEY=your_openai_api_key_here # DeepSeek API Key for summarization DEEPSEEK_API_KEY= -# Origin that is allowed to use the `OPENAI_API_KEY` & `DEEPSEEK_API_KEY` provided by the API -# instead of needing to provide them as `Authorization` in each request from client -ALLOWED_ORIGIN=http://localhost:3001 +# Origins that are allowed to use the AI models keys provided by the API, seperate them using a comma +# instead of need to provide them as Authorization in each request from client +ALLOWED_ORIGINS=http://localhost:3001 # Max Tokens (Limit the response length) OPENAI_MAX_TOKENS=500 diff --git a/docs/authentication.md b/docs/authentication.md index 5808b07..c4c19d3 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -3,7 +3,7 @@ The Summarization API uses **API Key authentication** to control access. There are two ways to provide an API key: -1. **Environment Variables (`.env`)** – The API owner can set `OPENAI_API_KEY` and `DEEPSEEK_API_KEY` to allow authorized access without requiring clients to provide API keys. However this only works if the **origin** is same as the value of `ALLOWED_ORIGIN`. +1. **Environment Variables (`.env`)** – The API owner can set `OPENAI_API_KEY` and `DEEPSEEK_API_KEY` to allow authorized access without requiring clients to provide API keys. However this only works if the **origin** is a value in `ALLOWED_ORIGINS`. 2. **Request Headers** – Clients can include an API key in the `Authorization` header for each request. > When there is both a `.env` key and a request header key, the request header key will be used. @@ -45,15 +45,15 @@ Authorization: Bearer YOUR_API_KEY ## 2. Allowed Origin Configuration -The API restricts access to the default keys to specific **frontend applications** by defining `ALLOWED_ORIGIN`. +The API restricts access to the default keys to specific **frontend applications** by defining `ALLOWED_ORIGINS`. If an origin is **not** allowed, then the api key must be provided in the request headers. ```ini -ALLOWED_ORIGIN=http://localhost:3001 +ALLOWED_ORIGINS=http://localhost:3001,http://localhost:3000 ``` -- If a frontend **matches `ALLOWED_ORIGIN`**, it **does not** need to send API keys. -- If a frontend **is not listed in `ALLOWED_ORIGIN`**, it must include API keys in request headers. +- If a frontend **is listed in `ALLOWED_ORIGINS`**, it **does not** need to send API keys. +- If a frontend **is not listed in `ALLOWED_ORIGINS`**, it must include API keys in request headers. --- @@ -65,9 +65,9 @@ The API uses a **security guard (`ApiKeyGuard`)** to verify API keys before proc | **Scenario** | **What Happens?** | **Notes** | | ---------------------------------------------- | -------------------------------------------- | -------------------------------------------- | -| **Valid API Key in Request Header** | βœ… Request is allowed | Does not require origin to be provided in `ALLOWED_ORIGIN` | -| **Valid API Key in `.env` but not in request** | βœ… Request is allowed (uses `.env` key) | Requires origin to be provided in `ALLOWED_ORIGIN` | -| **Valid API Key in `.env` and in request** | βœ… Request is allowed (uses request api key) | βœ…Even if the origin is same as the valud of `ALLOWED_ORIGIN`, api key in the request will be used | +| **Valid API Key in Request Header** | βœ… Request is allowed | Does not require origin to be provided in `ALLOWED_ORIGINS` | +| **Valid API Key in `.env` but not in request** | βœ… Request is allowed (uses `.env` key) | Requires origin to be provided in `ALLOWED_ORIGINS` | +| **Valid API Key in `.env` and in request** | βœ… Request is allowed (uses request api key) | βœ…Even if the origin is provided in `ALLOWED_ORIGINS`, api key in the request will be used | | **No API Key provided in request or `.env`** | ❌ Request is rejected | | --- @@ -81,7 +81,7 @@ curl -X POST http://localhost:3000/summarize/text \ -d '{ "content": { "text": "This is a test." }, "options": {} }' ``` -This will be valid only if the `ALLOWED_ORIGIN` is `http://localhost:3001`. +This will be valid only if the `ALLOWED_ORIGINS` contains `http://localhost:3001`. --- diff --git a/docs/getting-started.md b/docs/getting-started.md index 112ed1a..3752658 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -60,9 +60,9 @@ OPENAI_API_KEY= # DeepSeek API Key for summarization (Optional if provided in request headers) DEEPSEEK_API_KEY= -# Origin that is allowed to use the `OPENAI_API_KEY` & `DEEPSEEK_API_KEY` provided by the API -# instead of needing to provide them as `Authorization` in each request from client -ALLOWED_ORIGIN=http://localhost:3001 +# Origins that are allowed to use the AI models keys provided by the API, seperate them using a comma +# instead of need to provide them as Authorization in each request from client +ALLOWED_ORIGINS=http://localhost:3001,http://localhost:3000 # Max Tokens (Limit the response length) OPENAI_MAX_TOKENS=500 @@ -87,9 +87,9 @@ Below is a breakdown of the `.env` variables and their functions: | **Variable** | **Description** | **Required?** | **Default Value** | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ----------------------- | | `NODE_ENV` | Node.js environment (development, production, etc.) | ❌ No | None | -| `OPENAI_API_KEY` | API key for using OpenAI GPT-4o, Whisper, and TTS-1 models. Only works if the origin matches the value of `ALLOWED_ORIGIN`. _(can be provided in request headers instead)_ | ❌ No | None | -| `DEEPSEEK_API_KEY` | API key for using DeepSeek Chat (DeepSeek-V3). Only works if the origin matches the value of `ALLOWED_ORIGIN`. _(can be provided in request headers instead)_ | ❌ No | None | -| `ALLOWED_ORIGIN` | Specifies the allowed frontend origin that can access API-provided keys | ❌ No | `http://localhost:3001` | +| `OPENAI_API_KEY` | API key for using OpenAI GPT-4o, Whisper, and TTS-1 models. Only works if the origin is listed in the `ALLOWED_ORIGINS`. _(can be provided in request headers instead)_ | ❌ No | None | +| `DEEPSEEK_API_KEY` | API key for using DeepSeek Chat (DeepSeek-V3). Only works if the origin is listed in the `ALLOWED_ORIGINS`. _(can be provided in request headers instead)_ | ❌ No | None | +| `ALLOWED_ORIGINS` | Specifies the allowed frontend origins that can access API-provided keys | ❌ No | `http://localhost:3001` | | `OPENAI_MAX_TOKENS` | Maximum token limit for OpenAI-generated responses | ❌ No | `500` | | `DEEPSEEK_MAX_TOKENS` | Maximum token limit for DeepSeek-generated responses | ❌ No | `1000` | | `AWS_ACCESS_KEY_ID` | AWS Access Key for S3 storage (for text-to-speech audio files) | ⚠️ Required only if `USE_S3` is `true` | None | diff --git a/src/summarization/guards/api-key.guard.ts b/src/summarization/guards/api-key.guard.ts index 763798a..9779545 100644 --- a/src/summarization/guards/api-key.guard.ts +++ b/src/summarization/guards/api-key.guard.ts @@ -1,16 +1,16 @@ import { CanActivate, ExecutionContext, UnauthorizedException } from "@nestjs/common"; import { Request } from "express"; import { STTModel, SummarizationModel, SummarizationSpeed } from "../enums/summarization-options.enum"; - +import { ALLOWED_ORIGINS } from 'src/utils/constants'; export class ApiKeyGuard implements CanActivate { - private readonly allowedOrigin = process.env.ALLOWED_ORIGIN || 'http://localhost:3000'; + private readonly allowedOrigins: string[] = ALLOWED_ORIGINS; canActivate(context: ExecutionContext): boolean { const request:Request = context.switchToHttp().getRequest(); const origin = request.headers.origin; - if(origin && origin === this.allowedOrigin) { + if(origin && this.allowedOrigins.includes(origin)) { return true; } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index ff2008c..fa3236f 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -23,4 +23,6 @@ export const COOKIES_PATH = path.resolve(process.cwd(), 'cookies.txt'); export const CORS_ORIGINS: string[] = process.env.CORS ? process.env.CORS.split(',') : ['http://localhost:3001', 'https://letssummarize.vercel.app']; export const FASTAPI_URL = process.env.FASTAPI_URL || 'your-fastapi-url'; -export const PO_TOKEN = process.env.PO_TOKEN; \ No newline at end of file +export const PO_TOKEN = process.env.PO_TOKEN; + +export const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(',') : ['http://localhost:3001']; \ No newline at end of file diff --git a/test/http/test.http b/test/http/test.http index ce3121e..05a82d2 100644 --- a/test/http/test.http +++ b/test/http/test.http @@ -1,8 +1,9 @@ @openaiKey = {{$dotenv OPENAI_API_KEY}} @deepseekKey = {{$dotenv DEEPSEEK_API_KEY}} -@allowedOrigin = {{$dotenv ALLOWED_ORIGIN}} -@baseUrl = https://letssummarize-api.vercel.app/ -@ytVideo = https://www.youtube.com/shorts/cKQC2-g6CRY +@allowedOrigin = http://localhost:3001 +@baseUrl = http://localhost:3002 +@baseProdUrl = https://letssummarize.technway.biz +@ytVideo = https://www.youtube.com/watch?v=R03DjtCPkGE&pp=ygUGdGVkIGVk @text = `I'm going to show you the best way to start practicing designing apps and websites in Figma. So in this video I'm going to give you step-by-step instructions. You can literally follow click by click. I'll only tell you the stuff that you need to get started designing interfaces. So let's get started. So we're going to be looking at a tool called Figma and it has a few advantages. One, most importantly for you, it's free to get started if you're working by yourself. We like using it at AGN Smart because it also has really good collaboration so we can have multiple people working on the same design file at the same time. It's also really fast. It works on any computer whether you have a Mac or a PC or Linux. Whatever you have it works right in the browser and it also has a mobile companion app so you can preview your designs on a mobile screen. So there are really no downsides to starting with a tool like Figma. As you're watching the video, if you have any questions about how to do a particular effect in Figma or any comment or something that you want to recommend, please put it in the comments below. And if you want to find out more tips about UI and UX, make sure to subscribe to our free newsletter. The link to that is in the description below and it's a great resource for anyone starting in UI and UX. So this is the website. You just go to Figma.com and I'm already signed in but you can sign up very quickly even with your Google account and get started. But before we jump right into Figma, I want to show you the way I would recommend to get started. So you just want to start practicing. Now for that, I'm not going to ask you to start designing something from scratch because I believe that would be very hard with someone, especially if you're a complete beginner in this space and you have no grounding in design principles and things like that. So the best way for you to get started is actually to copy other designs. And the reason this is so good is because you can see how this design was created so that when you get stuck on something, you can actually see how this person who created this file achieved particular effect or look inside of Figma. And this is totally fine in the beginning because you're not going to be selling these. You're not going to be saying that you designed something when you copied it from someone else. This is just for your own practice and it's a really good way to get started. So as you can see here, this is what Figma looks like after you log in and start a file. And I haven't even shown you how to start a file because I want you to use another file as your starting point as opposed to a blank file. And like I said, we're not going to cover everything that you can see here on the screen in terms of what all the various buttons do. We're just going to focus about how you can get started. Now to do that, I wanted to start off with a template. And what I literally did was I typed into Google Figma resources and I got a bunch of results` @@ -12,9 +13,8 @@ GET {{baseUrl}} Content-Type: application/json ### Summarize YouTube Video -POST {{baseUrl}}/summarize/video +POST {{baseProdUrl}}/summarize/video Content-Type: application/json -Authorization: Bearer {{deepseekKey}} Origin: {{allowedOrigin}} { @@ -24,10 +24,11 @@ Origin: {{allowedOrigin}} "options": { "length": "comprehensive", "format": "default", - "speed": "fast", - "model": "deepseek", + "speed": "slow", + "model": "gemini", + "sttModel": "whisper-1", "listen": false, - "customInstructions": "use tables and add your opinion in the end", + "customInstructions": "", "lang": "english" } } @@ -35,7 +36,6 @@ Origin: {{allowedOrigin}} ### Summarize Text POST {{baseUrl}}/summarize/text Content-Type: application/json -Authorization: Bearer {{openaiKey}} Origin: {{allowedOrigin}} { @@ -45,7 +45,7 @@ Origin: {{allowedOrigin}} "options": { "length": "brief", "format": "bullet-points", - "model": "openai", + "model": "gemini", "listen": false } } @@ -53,20 +53,21 @@ Origin: {{allowedOrigin}} ### Summarize File POST {{baseUrl}}/summarize/file Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW -Authorization: Bearer {{openaiKey}} - -------WebKitFormBoundary7MA4YWxkTrZu0gW -Content-Disposition: form-data; name="file"; filename="example.pdf" -Content-Type: application/pdf Origin: {{allowedOrigin}} -< {{fileUrl}} ------WebKitFormBoundary7MA4YWxkTrZu0gW -Content-Disposition: form-data; name="length" +Content-Disposition: form-data; name="options" +Content-Type: application/json -standard +{ + "format": "bullet-points", + "model": "gemini", + "speed": "fast", + "listen": false +} ------WebKitFormBoundary7MA4YWxkTrZu0gW -Content-Disposition: form-data; name="format" +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/pdf -narrative -------WebKitFormBoundary7MA4YWxkTrZu0gW--> \ No newline at end of file +< {{fileUrl}} +------WebKitFormBoundary7MA4YWxkTrZu0gW----> \ No newline at end of file From 2dccf494b77be6e37fde177d49b09c7e15c360ce Mon Sep 17 00:00:00 2001 From: Khaled Alshibani <127689031+khaledsAlshibani@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:13:48 +0300 Subject: [PATCH 21/22] refactor: reuse CORS_ORIGINS constans for CORS config --- src/main.ts | 3 ++- src/summarization/guards/api-key.guard.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 0f193d1..efaa60f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,7 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; +import { CORS_ORIGINS } from './utils/constants'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -8,7 +9,7 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); app.enableCors({ - origin: ['http://localhost:3001', 'https://letssummarize.vercel.app'], + origin: CORS_ORIGINS, credentials: true, methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], diff --git a/src/summarization/guards/api-key.guard.ts b/src/summarization/guards/api-key.guard.ts index 9779545..2998533 100644 --- a/src/summarization/guards/api-key.guard.ts +++ b/src/summarization/guards/api-key.guard.ts @@ -7,6 +7,7 @@ export class ApiKeyGuard implements CanActivate { private readonly allowedOrigins: string[] = ALLOWED_ORIGINS; canActivate(context: ExecutionContext): boolean { + console.log('allowedOrigins ', this.allowedOrigins) const request:Request = context.switchToHttp().getRequest(); const origin = request.headers.origin; From b18041aa97efc34963516ee3c2d2bed198eb0bb8 Mon Sep 17 00:00:00 2001 From: Khaled Alshibani <127689031+khaledsAlshibani@users.noreply.github.com> Date: Fri, 28 Mar 2025 16:12:23 +0300 Subject: [PATCH 22/22] chore(package.json): add prestart script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 17283a4..7a5ecb5 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "prestart": "pnpm run build", "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch",