Yashwanth commited on
Commit
b4a9f77
·
0 Parent(s):

Fresh start without large files

Browse files
.github/workflows/deploy.yml ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to Vercel
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ branches:
9
+ - main
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: '3.9'
21
+
22
+ - name: Install dependencies
23
+ run: |
24
+ python -m pip install --upgrade pip
25
+ pip install -r requirements.txt
26
+
27
+ - name: Run tests
28
+ run: |
29
+ # Add tests here when ready
30
+ python -c "import api.index; print('Import successful')"
31
+
32
+ deploy:
33
+ needs: test
34
+ runs-on: ubuntu-latest
35
+ if: github.ref == 'refs/heads/main'
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+
39
+ - name: Deploy to Vercel
40
+ uses: vercel/action-deploy@v1
41
+ with:
42
+ vercel-token: ${{ secrets.VERCEL_TOKEN }}
43
+ vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
44
+ vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ venv/
5
+ .env
6
+
7
+ # Large Binaries (Not needed on repo, Docker installs them)
8
+ ffmpeg/
9
+ yt-dlp.exe
10
+
11
+ # Editor
12
+ .vscode/
13
+ .idea/
DEPLOYMENT.md ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 Deployment Guide
2
+
3
+ Complete guide to deploy the Viral Clip Extractor API to Vercel and GitHub.
4
+
5
+ ## Quick Deploy (Recommended)
6
+
7
+ ### Option 1: Vercel Dashboard (Easiest)
8
+
9
+ 1. **Push to GitHub**
10
+ ```bash
11
+ # Create a new repository on GitHub first
12
+ git init
13
+ git add .
14
+ git commit -m "Initial commit"
15
+ git branch -M main
16
+ git remote add origin https://github.com/YOUR_USERNAME/viral-clip-extractor-api.git
17
+ git push -u origin main
18
+ ```
19
+
20
+ 2. **Deploy on Vercel**
21
+ - Go to [vercel.com](https://vercel.com)
22
+ - Sign up/login with GitHub
23
+ - Click "New Project"
24
+ - Import your repository
25
+ - Vercel auto-detects Python
26
+ - Click "Deploy"
27
+
28
+ 3. **Done!** Your API is live at `https://your-project.vercel.app`
29
+
30
+ ---
31
+
32
+ ### Option 2: Vercel CLI
33
+
34
+ 1. **Install Vercel CLI**
35
+ ```bash
36
+ npm i -g vercel
37
+ ```
38
+
39
+ 2. **Login**
40
+ ```bash
41
+ vercel login
42
+ ```
43
+
44
+ 3. **Deploy**
45
+ ```bash
46
+ cd viral-clip-extractor-api
47
+ vercel --prod
48
+ ```
49
+
50
+ 4. **Follow prompts** - Vercel will detect Python automatically
51
+
52
+ ---
53
+
54
+ ## GitHub + Vercel Integration (Auto-Deploy)
55
+
56
+ ### Setup GitHub Repository
57
+
58
+ 1. **Create repository on GitHub**
59
+ - Go to github.com/new
60
+ - Name: `viral-clip-extractor-api`
61
+ - Make it Public or Private
62
+ - Don't initialize with README
63
+
64
+ 2. **Push local code**
65
+ ```bash
66
+ cd viral-okcomputer/output/viral-clip-api
67
+ git init
68
+ git add .
69
+ git commit -m "Initial commit"
70
+ git branch -M main
71
+ git remote add origin https://github.com/YOUR_USERNAME/viral-clip-extractor-api.git
72
+ git push -u origin main
73
+ ```
74
+
75
+ ### Setup Vercel Project
76
+
77
+ 1. **Import from GitHub**
78
+ - Go to [vercel.com/new](https://vercel.com/new)
79
+ - Select your repository
80
+ - Framework Preset: `Other`
81
+ - Root Directory: `./`
82
+ - Build Command: (leave empty for Python)
83
+ - Output Directory: (leave empty)
84
+
85
+ 2. **Environment Variables** (if needed)
86
+ - Click "Environment Variables"
87
+ - Add any required variables
88
+ - Click "Deploy"
89
+
90
+ 3. **Auto-Deploy Enabled**
91
+ - Every push to `main` branch auto-deploys
92
+ - Pull requests get preview deployments
93
+
94
+ ---
95
+
96
+ ## GitHub Actions CI/CD
97
+
98
+ The repository includes a GitHub Actions workflow for automated deployment.
99
+
100
+ ### Setup Secrets
101
+
102
+ 1. **Get Vercel Tokens**
103
+ ```bash
104
+ vercel login
105
+ vercel tokens create
106
+ ```
107
+
108
+ 2. **Get Project ID**
109
+ ```bash
110
+ cd your-project
111
+ vercel
112
+ # Check .vercel/project.json
113
+ ```
114
+
115
+ 3. **Add to GitHub Secrets**
116
+ - Go to Repository → Settings → Secrets → Actions
117
+ - Add:
118
+ - `VERCEL_TOKEN` - Your token
119
+ - `VERCEL_ORG_ID` - From `.vercel/project.json`
120
+ - `VERCEL_PROJECT_ID` - From `.vercel/project.json`
121
+
122
+ ### Workflow Features
123
+
124
+ - ✅ Runs tests on every PR
125
+ - ✅ Auto-deploys on merge to main
126
+ - ✅ Shows deployment status in PRs
127
+
128
+ ---
129
+
130
+ ## Configuration
131
+
132
+ ### Vercel Settings
133
+
134
+ Edit `vercel.json` to customize:
135
+
136
+ ```json
137
+ {
138
+ "version": 2,
139
+ "name": "viral-clip-extractor-api",
140
+ "functions": {
141
+ "api/index.py": {
142
+ "maxDuration": 60 // Max 60 seconds (Vercel limit)
143
+ }
144
+ }
145
+ }
146
+ ```
147
+
148
+ ### Python Runtime
149
+
150
+ Vercel uses Python 3.9 by default. To change:
151
+
152
+ ```json
153
+ {
154
+ "builds": [{
155
+ "src": "api/index.py",
156
+ "use": "@vercel/python",
157
+ "config": {
158
+ "runtime": "python3.9"
159
+ }
160
+ }]
161
+ }
162
+ ```
163
+
164
+ ### Custom Domain
165
+
166
+ 1. Go to Vercel Dashboard → Your Project → Settings → Domains
167
+ 2. Add your domain
168
+ 3. Follow DNS configuration instructions
169
+
170
+ ---
171
+
172
+ ## Testing Your Deployment
173
+
174
+ ### 1. Health Check
175
+ ```bash
176
+ curl https://your-project.vercel.app/health
177
+ ```
178
+
179
+ Expected:
180
+ ```json
181
+ {"status": "healthy"}
182
+ ```
183
+
184
+ ### 2. API Info
185
+ ```bash
186
+ curl https://your-project.vercel.app/
187
+ ```
188
+
189
+ ### 3. Test Analysis
190
+ ```bash
191
+ curl -X POST https://your-project.vercel.app/analyze \
192
+ -H "Content-Type: application/json" \
193
+ -d '{"url": "https://youtube.com/watch?v=dQw4w9WgXcQ", "num_clips": 3}'
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Troubleshooting
199
+
200
+ ### Build Failures
201
+
202
+ **Problem**: Build fails with dependency errors
203
+
204
+ **Solution**:
205
+ ```bash
206
+ # Update requirements.txt
207
+ pip freeze > requirements.txt
208
+
209
+ # Or manually specify versions
210
+ flask==3.0.3
211
+ yt-dlp==2024.8.6
212
+ ```
213
+
214
+ ### Timeout Issues
215
+
216
+ **Problem**: 504 Gateway Timeout
217
+
218
+ **Solution**:
219
+ - Reduce `clip_length` or `num_clips` in requests
220
+ - Videos > 2 hours may timeout
221
+ - Consider splitting long videos
222
+
223
+ ### Import Errors
224
+
225
+ **Problem**: `ModuleNotFoundError`
226
+
227
+ **Solution**:
228
+ - Ensure all dependencies in `requirements.txt`
229
+ - Check Python version compatibility
230
+ - Verify file structure matches `vercel.json`
231
+
232
+ ### CORS Errors
233
+
234
+ **Problem**: Frontend can't access API
235
+
236
+ **Solution**:
237
+ - CORS is enabled by default in `api/index.py`
238
+ - Check `flask-cors` is in requirements.txt
239
+
240
+ ---
241
+
242
+ ## Monitoring
243
+
244
+ ### Vercel Analytics
245
+
246
+ 1. Go to Vercel Dashboard → Your Project → Analytics
247
+ 2. View:
248
+ - Request count
249
+ - Response times
250
+ - Error rates
251
+ - Bandwidth usage
252
+
253
+ ### Logs
254
+
255
+ ```bash
256
+ # View live logs
257
+ vercel logs your-project --tail
258
+ ```
259
+
260
+ Or in Dashboard → Project → Logs
261
+
262
+ ---
263
+
264
+ ## Updating Your API
265
+
266
+ ### Method 1: Git Push (Auto-Deploy)
267
+
268
+ ```bash
269
+ # Make changes
270
+ git add .
271
+ git commit -m "Update feature"
272
+ git push origin main
273
+ # Vercel auto-deploys!
274
+ ```
275
+
276
+ ### Method 2: Vercel CLI
277
+
278
+ ```bash
279
+ vercel --prod
280
+ ```
281
+
282
+ ---
283
+
284
+ ## Environment Variables
285
+
286
+ ### Add to Vercel
287
+
288
+ 1. Dashboard → Project → Settings → Environment Variables
289
+ 2. Or via CLI:
290
+ ```bash
291
+ vercel env add VARIABLE_NAME
292
+ ```
293
+
294
+ ### Use in Code
295
+
296
+ ```python
297
+ import os
298
+
299
+ api_key = os.environ.get('API_KEY')
300
+ ```
301
+
302
+ ---
303
+
304
+ ## Security Best Practices
305
+
306
+ 1. **Never commit `.env` files**
307
+ - Already in `.gitignore`
308
+
309
+ 2. **Use environment variables for secrets**
310
+ - API keys
311
+ - Database URLs
312
+ - Private tokens
313
+
314
+ 3. **Enable Vercel Authentication**
315
+ - Dashboard → Project → Settings → Security
316
+
317
+ 4. **Rate Limiting**
318
+ - Consider adding Flask-Limiter for production
319
+
320
+ ---
321
+
322
+ ## Next Steps
323
+
324
+ - [ ] Add custom domain
325
+ - [ ] Set up monitoring/alerts
326
+ - [ ] Add rate limiting
327
+ - [ ] Implement caching
328
+ - [ ] Add more languages support
329
+ - [ ] Create mobile app
330
+
331
+ ---
332
+
333
+ ## Support
334
+
335
+ - **Vercel Docs**: [vercel.com/docs](https://vercel.com/docs)
336
+ - **Python Runtime**: [vercel.com/docs/functions/serverless-functions/runtimes/python](https://vercel.com/docs/functions/serverless-functions/runtimes/python)
337
+ - **yt-dlp Docs**: [github.com/yt-dlp/yt-dlp](https://github.com/yt-dlp/yt-dlp)
338
+
339
+ ---
340
+
341
+ **Happy Deploying! 🚀**
Dockerfile ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.9 Slim image
2
+ FROM python:3.9-slim
3
+
4
+ # Install system dependencies (FFmpeg)
5
+ RUN apt-get update && \
6
+ apt-get install -y ffmpeg && \
7
+ apt-get clean && \
8
+ rm -rf /var/lib/apt/lists/*
9
+
10
+ # Set working directory
11
+ WORKDIR /app
12
+
13
+ # Copy requirements first (for caching)
14
+ COPY requirements.txt .
15
+
16
+ # Install Python dependencies
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy application code
20
+ COPY api/ api/
21
+
22
+ # Set environment variables
23
+ ENV PYTHONUNBUFFERED=1
24
+
25
+ # Expose port (Render sets PORT env var)
26
+ EXPOSE 5000
27
+
28
+ # Run with Gunicorn
29
+ CMD gunicorn -w 4 -b 0.0.0.0:$PORT api.index:app
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Viral Clip Extractor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🎬 Viral Clip Extractor API
2
+
3
+ Turn long-form YouTube videos into engaging, viral-ready clips for TikTok, Shorts, and Reels using AI.
4
+
5
+ **Live Demo / Docs:** `https://your-app-url.onrender.com/docs`
6
+
7
+ ## ✨ Features
8
+
9
+ - **🧠 Three Powerful Modes:**
10
+ 1. **Heuristic (Free/Fast):** Uses keyword analysis and audio/visual cues. Great for high-energy content.
11
+ 2. **Gemini AI (Balanced):** Uses Google's Gemini 2.0 Flash for context-aware clipping.
12
+ 3. **Nvidia/DeepSeek AI (Advanced):** Uses DeepSeek V3/R1 via Nvidia API for deep reasoning and technical content.
13
+ - **🚀 High Performance:** Optimized `yt-dlp` and `ffmpeg` integration for fast processing.
14
+ - **☁️ Cloud Ready:** Dockerized and configured for [Render](https://render.com) deployment.
15
+ - **🔒 Secure:** Environment variable support for API keys.
16
+ - **📄 Auto-Documentation:** Built-in interactive API docs at `/docs`.
17
+
18
+ ## 🛠️ Installation
19
+
20
+ ### Option 1: Docker (Recommended)
21
+ ```bash
22
+ docker build -t viral-clips .
23
+ docker run -p 5000:5000 --env-file .env viral-clips
24
+ ```
25
+
26
+ ### Option 2: Local Python
27
+ 1. **Install FFmpeg:** Ensure `ffmpeg` is in your system PATH.
28
+ 2. **Install Requirements:**
29
+ ```bash
30
+ pip install -r requirements.txt
31
+ ```
32
+ 3. **Run API:**
33
+ ```bash
34
+ python api/index.py
35
+ ```
36
+
37
+ ## 🔑 Configuration
38
+
39
+ Create a `.env` file or set these environment variables in your cloud dashboard:
40
+
41
+ | Variable | Description | Required For |
42
+ | :--- | :--- | :--- |
43
+ | `GEMINI_API_KEY` | Google Gemini API Key | `mode=ai` |
44
+ | `NVIDIA_API_KEY` | Nvidia/DeepSeek API Key | `mode=nvidia` |
45
+ | `PORT` | Server Port (Default: 5000) | Deployment |
46
+
47
+ ## 📚 API Usage
48
+
49
+ ### Get Clips
50
+ **Endpoint:** `GET /clips`
51
+
52
+ | Parameter | Type | Default | Description |
53
+ | :--- | :--- | :--- | :--- |
54
+ | `url` | string | **Required** | YouTube Video URL |
55
+ | `mode` | string | `heuristic` | `heuristic`, `ai`, or `nvidia` |
56
+ | `num` | int | `5` | Number of clips to generate |
57
+
58
+ **Example:**
59
+ ```bash
60
+ curl "http://localhost:5000/clips?url=https://youtu.be/VIDEO_ID&mode=ai"
61
+ ```
62
+
63
+ ### Download Clip
64
+ **Endpoint:** `GET /download_file`
65
+
66
+ | Parameter | Type | Description |
67
+ | :--- | :--- | :--- |
68
+ | `url` | string | Original YouTube URL |
69
+ | `start` | float | Start time (seconds) |
70
+ | `end` | float | End time (seconds) |
71
+
72
+ ## 📝 Credits
73
+ Developed with ❤️ by **yash@dev**
74
+
75
+ Powered by:
76
+ - [yt-dlp](https://github.com/yt-dlp/yt-dlp)
77
+ - [FFmpeg](https://ffmpeg.org/)
78
+ - [Google Gemini](https://ai.google.dev/)
79
+ - [Nvidia DeepSeek](https://build.nvidia.com/)
api/index.py ADDED
@@ -0,0 +1,1001 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Viral Clip Extractor API
3
+ Deploy to Vercel for YouTube viral clip analysis
4
+ """
5
+
6
+ from flask import Flask, request, jsonify, send_file, after_this_request
7
+ from flask_cors import CORS
8
+ import yt_dlp
9
+ import requests
10
+ import google.generativeai as genai
11
+ from openai import OpenAI
12
+ import json
13
+ import re
14
+ from io import StringIO
15
+ import os
16
+ import re
17
+ from typing import Dict, List, Optional, Any
18
+
19
+ app = Flask(__name__)
20
+ CORS(app)
21
+
22
+ # ============== CONFIGURATION ==============
23
+ DEFAULT_CLIP_LENGTH = 40
24
+ DEFAULT_WINDOW_STEP = 10
25
+ DEFAULT_MIN_CLIP_LENGTH = 25
26
+ DEFAULT_MAX_CLIP_LENGTH = 60
27
+
28
+ HOOK_WORDS = [
29
+ "wait", "shocking", "unbelievable", "secret", "crazy", "exposed", "omg", "wtf",
30
+ "plot twist", "insane", "cheating", "stunning", "mind blowing", "truth", "lie",
31
+ "revealed", "hidden", "discover", "amazing", "incredible", "must see", "urgent"
32
+ ]
33
+
34
+ EMOTION_WORDS = [
35
+ "laugh", "cry", "angry", "shocked", "surprised", "scream", "love", "hate",
36
+ "disgusted", "horrified", "amazed", "excited", "sad", "happy", "furious"
37
+ ]
38
+
39
+ # ============== UTILITY FUNCTIONS ==============
40
+
41
+ def extract_video_id(url: str) -> Optional[str]:
42
+ """Extract YouTube video ID from various URL formats"""
43
+ patterns = [
44
+ r'(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/v\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})',
45
+ r'^([a-zA-Z0-9_-]{11})$'
46
+ ]
47
+ for pattern in patterns:
48
+ match = re.search(pattern, url)
49
+ if match:
50
+ return match.group(1)
51
+ return None
52
+
53
+ def time_to_seconds(t) -> float:
54
+ """Convert time string to seconds"""
55
+ if isinstance(t, (int, float)):
56
+ return float(t)
57
+ if ':' in str(t):
58
+ parts = list(map(int, str(t).split(':')))
59
+ if len(parts) == 3:
60
+ return parts[0] * 3600 + parts[1] * 60 + parts[2]
61
+ elif len(parts) == 2:
62
+ return parts[0] * 60 + parts[1]
63
+ elif len(parts) == 1:
64
+ return parts[0]
65
+ return float(t)
66
+
67
+ def seconds_to_time(s: float) -> str:
68
+ """Convert seconds to MM:SS format"""
69
+ minutes = int(s // 60)
70
+ seconds = int(s % 60)
71
+ return f"{minutes}:{seconds:02d}"
72
+
73
+ def parse_vtt_content(vtt_text: str) -> List[Dict]:
74
+ """Parse VTT subtitle content"""
75
+ segments = []
76
+ lines = vtt_text.strip().split('\n')
77
+
78
+ i = 0
79
+ while i < len(lines) and not '-->' in lines[i]:
80
+ i += 1
81
+
82
+ current_text = []
83
+ current_start = None
84
+ current_end = None
85
+
86
+ while i < len(lines):
87
+ line = lines[i].strip()
88
+
89
+ if '-->' in line:
90
+ if current_text and current_start is not None:
91
+ segments.append({
92
+ 'start': current_start,
93
+ 'end': current_end,
94
+ 'text': ' '.join(current_text)
95
+ })
96
+
97
+ times = line.split('-->')
98
+ if len(times) == 2:
99
+ start_str = times[0].strip().split('.')[0]
100
+ end_str = times[1].strip().split()[0].split('.')[0]
101
+
102
+ current_start = time_to_seconds(start_str)
103
+ current_end = time_to_seconds(end_str)
104
+ current_text = []
105
+ elif line and not line.startswith('NOTE') and not line.startswith('STYLE'):
106
+ clean_line = re.sub(r'<[^>]+>', '', line)
107
+ if clean_line:
108
+ current_text.append(clean_line)
109
+
110
+ i += 1
111
+
112
+ if current_text and current_start is not None:
113
+ segments.append({
114
+ 'start': current_start,
115
+ 'end': current_end,
116
+ 'text': ' '.join(current_text)
117
+ })
118
+
119
+ return segments
120
+
121
+ import sys
122
+ import logging
123
+
124
+ # Configure logging
125
+ logging.basicConfig(level=logging.INFO)
126
+ logger = logging.getLogger(__name__)
127
+
128
+ # ============== CORE CLASS ==============
129
+
130
+ class ViralClipExtractor:
131
+ def __init__(self):
132
+ self.headers = {
133
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
134
+ }
135
+ # Robust FFmpeg path resolution
136
+ import shutil
137
+ project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
138
+
139
+ # 1. Check local ffmpeg folder (Windows/Dev)
140
+ ffmpeg_path_local = os.path.join(project_root, "ffmpeg", "bin", "ffmpeg.exe")
141
+
142
+ # 2. Check system PATH (Linux/Render/Docker)
143
+ ffmpeg_path_system = shutil.which("ffmpeg")
144
+
145
+ ffmpeg_location = None
146
+ if os.path.exists(ffmpeg_path_local):
147
+ ffmpeg_location = ffmpeg_path_local
148
+ ffmpeg_dir = os.path.dirname(ffmpeg_location)
149
+ if ffmpeg_dir not in os.environ["PATH"]:
150
+ os.environ["PATH"] += os.pathsep + ffmpeg_dir
151
+ logger.info(f"Using Local FFmpeg: {ffmpeg_location}")
152
+ elif ffmpeg_path_system:
153
+ # If on system path, we don't need to specify location for yt-dlp usually,
154
+ # but getting the dir is good practice
155
+ ffmpeg_location = ffmpeg_path_system
156
+ logger.info(f"Using System FFmpeg: {ffmpeg_location}")
157
+ else:
158
+ logger.warning("FFmpeg NOT FOUND! Clip cutting will fail.")
159
+
160
+
161
+ self.base_ydl_opts = {
162
+ 'quiet': False,
163
+ 'no_warnings': False,
164
+ # 'extractor_args': {'youtube': {'player_client': ['web']}},
165
+ 'http_headers': self.headers,
166
+ # 'ffmpeg_location': ffmpeg_dir # Removed, relying on PATH
167
+ }
168
+
169
+ if ffmpeg_dir:
170
+ logger.info(f"Added FFmpeg to PATH: {ffmpeg_dir}")
171
+ else:
172
+ logger.warning("FFmpeg NOT FOUND! Clip cutting will fail.")
173
+
174
+ def download_clip(self, url: str, start: float, end: float, quality: str = "720") -> Optional[str]:
175
+ """Download and cut a clip"""
176
+ import uuid
177
+ output_dir = "downloads"
178
+ os.makedirs(output_dir, exist_ok=True)
179
+ clip_id = f"clip_{uuid.uuid4().hex[:8]}"
180
+ output_template = os.path.join(output_dir, f"{clip_id}.%(ext)s")
181
+
182
+ ydl_opts = {
183
+ **self.base_ydl_opts,
184
+ 'format': f'bestvideo[height<={quality}]+bestaudio/best[height<={quality}]/best',
185
+ 'outtmpl': output_template,
186
+ 'download_ranges': lambda info, ydl: [{
187
+ 'start_time': start,
188
+ 'end_time': end
189
+ }],
190
+ 'force_keyframes_at_cuts': True,
191
+ }
192
+
193
+ try:
194
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
195
+ ydl.download([url])
196
+
197
+ # Find the downloaded file
198
+ for file in os.listdir(output_dir):
199
+ if file.startswith(clip_id):
200
+ return os.path.abspath(os.path.join(output_dir, file))
201
+ return None
202
+ except Exception as e:
203
+ with open("error.log", "a") as f:
204
+ f.write(f"Download Error: {e}\n")
205
+ logger.error(f"Error downloading clip: {e}")
206
+ return None
207
+
208
+ def analyze_with_gemini(self, transcript_segments: List[Dict], api_key: str) -> List[Dict]:
209
+ """Analyze full transcript using Gemini AI"""
210
+ try:
211
+ genai.configure(api_key=api_key)
212
+ # Using 2.0 Flash as 1.5 is unavailable for this key/region
213
+ model = genai.GenerativeModel('gemini-2.0-flash')
214
+
215
+ # Helper to check if API key is likely valid (heuristic)
216
+ if "YOUR_GEMINI" in api_key:
217
+ raise ValueError("Invalid API Key placeholder used")
218
+
219
+ # Construct prompt
220
+ formatted_text = ""
221
+ for seg in transcript_segments:
222
+ start = int(seg['start'])
223
+ text = seg['text']
224
+ formatted_text += f"[{start}s] {text} "
225
+
226
+ # Truncate
227
+ full_context = formatted_text[:30000] # Safe limit
228
+
229
+ prompt = f"""
230
+ You are a viral content expert. Analyze the video transcript (with timestamps) and identify the top 3-5 most engaging clips for Shorts/TikTok.
231
+
232
+ Transcript:
233
+ {full_context}
234
+
235
+ Return ONLY a raw JSON array (no markdown) of objects:
236
+ [
237
+ {{
238
+ "start": <number_seconds>,
239
+ "end": <number_seconds>,
240
+ "viral_score": <0-100>,
241
+ "reason": "<short explanation>"
242
+ }}
243
+ ]
244
+ """
245
+
246
+ logger.info("Sending request to Gemini AI...")
247
+ response = model.generate_content(prompt)
248
+ text = response.text
249
+ # Clean markdown
250
+ text = re.sub(r"```json\s*", "", text)
251
+ text = re.sub(r"```\s*", "", text)
252
+ return json.loads(text)
253
+ except Exception as e:
254
+ logger.error(f"Gemini API Error: {e}")
255
+ raise e # Create visibility for the error
256
+
257
+ def analyze_with_nvidia(self, transcript_segments: List[Dict], api_key: str) -> List[Dict]:
258
+ """Analyze full transcript using Nvidia/DeepSeek AI"""
259
+ try:
260
+ client = OpenAI(
261
+ base_url = "https://integrate.api.nvidia.com/v1",
262
+ api_key = api_key
263
+ )
264
+
265
+ # Construct prompt
266
+ formatted_text = ""
267
+ for seg in transcript_segments:
268
+ start = int(seg['start'])
269
+ text = seg['text']
270
+ formatted_text += f"[{start}s] {text} "
271
+
272
+ full_context = formatted_text[:30000]
273
+
274
+ prompt = f"""
275
+ You are a viral content expert. Analyze the video transcript (with timestamps) and identify the top 3-5 most engaging clips for Shorts/TikTok.
276
+
277
+ Transcript:
278
+ {full_context}
279
+
280
+ Return ONLY a raw JSON array (no markdown) of objects:
281
+ [
282
+ {{
283
+ "start": <number_seconds>,
284
+ "end": <number_seconds>,
285
+ "viral_score": <0-100>,
286
+ "reason": "<short explanation>"
287
+ }}
288
+ ]
289
+ """
290
+
291
+ logger.info("Sending request to Nvidia/DeepSeek AI...")
292
+ completion = client.chat.completions.create(
293
+ model="deepseek-ai/deepseek-v3.1-terminus",
294
+ messages=[{"role":"user","content":prompt}],
295
+ temperature=0.2,
296
+ top_p=0.7,
297
+ max_tokens=8192,
298
+ extra_body={"chat_template_kwargs": {"thinking":True}},
299
+ stream=False
300
+ )
301
+
302
+ text = completion.choices[0].message.content
303
+ # Clean markdown
304
+ text = re.sub(r"```json\s*", "", text)
305
+ text = re.sub(r"```\s*", "", text)
306
+ # Remove thinking content
307
+ text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
308
+
309
+ return json.loads(text)
310
+ except Exception as e:
311
+ logger.error(f"Nvidia API Error: {e}")
312
+ raise e
313
+
314
+ def extract_video_info(self, url: str) -> Dict:
315
+ """Get video metadata"""
316
+ logger.info(f"Extracting video info for: {url}")
317
+ ydl_opts = {
318
+ **self.base_ydl_opts,
319
+ 'simulate': True,
320
+ }
321
+
322
+ try:
323
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
324
+ info = ydl.extract_info(url, download=False)
325
+
326
+ chapters = []
327
+ if 'chapters' in info and info['chapters']:
328
+ for ch in info['chapters']:
329
+ chapters.append({
330
+ "start": ch['start_time'],
331
+ "end": ch.get('end_time') or ch['start_time'] + DEFAULT_CLIP_LENGTH,
332
+ "title": ch.get('title', 'Untitled')
333
+ })
334
+
335
+ logger.info(f"Video info extracted: {info.get('title')}")
336
+ return {
337
+ "id": info.get("id"),
338
+ "title": info.get("title"),
339
+ "uploader": info.get("uploader"),
340
+ "duration": info.get("duration"),
341
+ "description": info.get("description", "")[:500],
342
+ "view_count": info.get("view_count"),
343
+ "like_count": info.get("like_count"),
344
+ "chapters": chapters
345
+ }
346
+ except Exception as e:
347
+ logger.error(f"Error extracting video info: {e}")
348
+ raise
349
+
350
+ def fetch_full_transcript(self, url: str) -> List[Dict]:
351
+ """Fetch full transcript once"""
352
+ logger.info(f"Fetching full transcript for: {url}")
353
+ ydl_opts = {
354
+ **self.base_ydl_opts,
355
+ 'skip_download': True,
356
+ 'writesubtitles': True,
357
+ 'writeautomaticsub': True,
358
+ 'subtitleslangs': ['en'],
359
+ }
360
+
361
+ try:
362
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
363
+ info = ydl.extract_info(url, download=False)
364
+
365
+ sub_url = None
366
+ if 'en' in info.get('subtitles', {}):
367
+ sub_url = info['subtitles']['en'][-1].get('url')
368
+ elif 'en' in info.get('automatic_captions', {}):
369
+ sub_url = info['automatic_captions']['en'][-1].get('url')
370
+
371
+ if not sub_url:
372
+ logger.warning("No transcript found")
373
+ return []
374
+
375
+ response = requests.get(sub_url, headers=self.headers, timeout=15)
376
+ response.raise_for_status()
377
+ sub_text = response.text
378
+
379
+ segments = []
380
+ if sub_url.endswith('.vtt') or 'vtt' in sub_url:
381
+ segments = parse_vtt_content(sub_text)
382
+ elif sub_url.endswith('.srt') or 'srt' in sub_url:
383
+ # Reuse vtt logic as simple fallback
384
+ segments = parse_vtt_content(sub_text)
385
+
386
+ # If we have segments, return them
387
+ if segments:
388
+ return segments
389
+
390
+ # Fallback to json3 if available/needed
391
+ try:
392
+ import json
393
+ data = json.loads(sub_text)
394
+ if 'events' in data:
395
+ for event in data['events']:
396
+ if 'segs' in event:
397
+ start_time = event.get('tStartMs', 0) / 1000
398
+ duration = event.get('dDurationMs', 0) / 1000
399
+ text = ''.join(seg.get('utf8', '') for seg in event['segs'])
400
+ segments.append({
401
+ 'start': start_time,
402
+ 'end': start_time + duration,
403
+ 'text': text
404
+ })
405
+ except:
406
+ pass
407
+
408
+ return segments
409
+
410
+ except Exception as e:
411
+ logger.error(f"Transcript error: {e}")
412
+ return []
413
+
414
+ def get_transcript_text(self, segments: List[Dict], start: float, end: float) -> str:
415
+ """Filter transcript from pre-fetched segments"""
416
+ relevant = [
417
+ seg["text"] for seg in segments
418
+ if seg["end"] >= start and seg["start"] <= end and seg["text"].strip()
419
+ ]
420
+ return " ".join(relevant)
421
+
422
+ def get_transcript(self, url: str, start: float, end: float) -> str:
423
+ """Legacy method for backward compatibility"""
424
+ segments = self.fetch_full_transcript(url)
425
+ return self.get_transcript_text(segments, start, end)
426
+
427
+ def score_clip(self, url: str, start: float, end: float, transcript_segments: List[Dict] = None) -> Dict:
428
+ """Calculate viral score for clip"""
429
+ if transcript_segments:
430
+ transcript = self.get_transcript_text(transcript_segments, start, end).lower()
431
+ else:
432
+ # Fallback
433
+ transcript = self.get_transcript(url, start, end).lower()
434
+
435
+ score = 0
436
+ reasons = []
437
+
438
+ # Hook words (up to 50 points)
439
+ hooks = [w for w in HOOK_WORDS if w in transcript]
440
+ hook_score = min(len(hooks) * 10, 50)
441
+ score += hook_score
442
+ if hooks:
443
+ reasons.append(f"Hooks: {', '.join(hooks[:3])}")
444
+
445
+ # Emotion words (up to 30 points)
446
+ emotions = [w for w in EMOTION_WORDS if w in transcript]
447
+ emotion_score = min(len(emotions) * 6, 30)
448
+ score += emotion_score
449
+ if emotions:
450
+ reasons.append(f"Emotion: {', '.join(emotions[:2])}")
451
+
452
+ # Conflict indicators (20 points)
453
+ conflict_words = ["but", "however", "argument", "fight", "wrong", "disagree"]
454
+ if any(w in transcript for w in conflict_words):
455
+ score += 20
456
+ reasons.append("Conflict detected")
457
+
458
+ # Duration bonus
459
+ duration = end - start
460
+ if DEFAULT_MIN_CLIP_LENGTH <= duration <= DEFAULT_MAX_CLIP_LENGTH:
461
+ score += 10
462
+ reasons.append("Optimal duration")
463
+ elif duration < DEFAULT_MIN_CLIP_LENGTH:
464
+ score -= 10
465
+ reasons.append("Short clip")
466
+ elif duration > DEFAULT_MAX_CLIP_LENGTH:
467
+ score -= 5
468
+ reasons.append("Long clip")
469
+
470
+ # Transcript quality bonus
471
+ word_count = len(transcript.split())
472
+ if word_count > 10:
473
+ score += 5
474
+ reasons.append("Good content density")
475
+
476
+ return {
477
+ "start": start,
478
+ "end": end,
479
+ "duration": round(duration, 1),
480
+ "viral_score": max(0, min(score, 100)),
481
+ "reasons": reasons,
482
+ "transcript_preview": transcript[:150] + "..." if len(transcript) > 150 else transcript,
483
+ "word_count": word_count
484
+ }
485
+
486
+ def generate_candidates(self, video_info: Dict, clip_length: int = None, window_step: int = None) -> List[Dict]:
487
+ """Generate candidate clips"""
488
+ duration = video_info.get("duration", 0)
489
+ clip_length = clip_length or DEFAULT_CLIP_LENGTH
490
+ window_step = window_step or DEFAULT_WINDOW_STEP
491
+ candidates = []
492
+
493
+ # Use chapters if available
494
+ if video_info.get("chapters"):
495
+ logger.info(f"Using {len(video_info['chapters'])} chapters for candidates")
496
+ for ch in video_info["chapters"]:
497
+ clip_start = ch["start"]
498
+ clip_end = min(ch["end"], clip_start + clip_length)
499
+ if clip_end - clip_start >= DEFAULT_MIN_CLIP_LENGTH:
500
+ candidates.append({
501
+ "start": clip_start,
502
+ "end": clip_end,
503
+ "title": ch.get("title", "Untitled")
504
+ })
505
+ else:
506
+ # Sliding windows
507
+ logger.info("Using sliding windows for candidates")
508
+ for start in range(0, int(duration) - clip_length, window_step):
509
+ candidates.append({
510
+ "start": start,
511
+ "end": min(start + clip_length, duration)
512
+ })
513
+
514
+ return candidates
515
+
516
+ # ============== API ROUTES ==============
517
+
518
+ @app.route('/', methods=['GET'])
519
+ def index():
520
+ """Root endpoint - API info"""
521
+ return jsonify({
522
+ "name": "Viral Clip Extractor API",
523
+ "version": "1.3.0 (AI Enabled)",
524
+ "description": "Extract and analyze viral clips from YouTube videos",
525
+ "quick_start": {
526
+ "endpoint": "/clips",
527
+ "method": "GET",
528
+ "usage": "/clips?url=YOUTUBE_URL&quality=720&seconds=40&num=5&mode=ai&gemini_key=YOUR_KEY",
529
+ "example": "/clips?url=https://youtube.com/watch?v=dQw4w9WgXcQ&seconds=30&num=3"
530
+ },
531
+ "endpoints": {
532
+ "/clips": "GET - Extract clips (Heuristic or AI)",
533
+ "/health": "Health check",
534
+ "/analyze": "POST - Full analysis",
535
+ "/extract": "POST - Extract video info only",
536
+ "/download_file": "GET - Download clip file"
537
+ }
538
+ })
539
+
540
+ @app.route('/health', methods=['GET'])
541
+ def health():
542
+ """Health check endpoint"""
543
+ return jsonify({
544
+ "status": "healthy",
545
+ "service": "viral-clip-extractor"
546
+ })
547
+
548
+ @app.route('/clips', methods=['GET'])
549
+ def get_clips():
550
+ """
551
+ Get viral clips (Heuristic, Gemini AI, or Nvidia AI)
552
+ """
553
+ try:
554
+ url = request.args.get('url')
555
+ if not url:
556
+ return jsonify({"error": "Missing 'url' parameter"}), 400
557
+
558
+ quality = request.args.get('quality', '720')
559
+ seconds = int(request.args.get('seconds', DEFAULT_CLIP_LENGTH))
560
+ num_clips = int(request.args.get('num', 5))
561
+
562
+ mode = request.args.get('mode', 'heuristic') # 'heuristic', 'ai' (defaults to gemini), 'nvidia'
563
+ ai_provider = request.args.get('ai_provider', 'gemini') # 'gemini' or 'nvidia'
564
+
565
+ # Handle API Keys
566
+ nvidia_key = request.args.get('nvidia_key') or os.environ.get('NVIDIA_API_KEY')
567
+ gemini_key = request.args.get('gemini_key') or os.environ.get('GEMINI_API_KEY')
568
+
569
+ # Unified key handling if user passes 'api_key' generic param
570
+ generic_key = request.args.get('api_key')
571
+ if generic_key:
572
+ if mode == 'nvidia' or ai_provider == 'nvidia':
573
+ nvidia_key = generic_key
574
+ else:
575
+ gemini_key = generic_key
576
+
577
+ extractor = ViralClipExtractor()
578
+
579
+ # 1. Get video info
580
+ video_info = extractor.extract_video_info(url)
581
+ video_id = extract_video_id(url)
582
+
583
+ if not video_id:
584
+ return jsonify({"error": "Invalid YouTube URL"}), 400
585
+
586
+ # 2. Fetch transcript ONCE
587
+ transcript_segments = extractor.fetch_full_transcript(url)
588
+
589
+ scored_clips = []
590
+
591
+ if mode == 'ai' or mode == 'nvidia' or request.args.get('ai_provider'):
592
+ # Determine provider
593
+ provider = 'nvidia' if (mode == 'nvidia' or ai_provider == 'nvidia') else 'gemini'
594
+
595
+ try:
596
+ if provider == 'nvidia':
597
+ if not nvidia_key:
598
+ return jsonify({"error": "Nvidia API Key required"}), 400
599
+ logger.info("Using Nvidia DeepSeek for analysis...")
600
+ scored_clips = extractor.analyze_with_nvidia(transcript_segments, nvidia_key)
601
+ else:
602
+ if not gemini_key:
603
+ return jsonify({"error": "Gemini API Key required"}), 400
604
+ logger.info("Using Gemini AI for analysis...")
605
+ scored_clips = extractor.analyze_with_gemini(transcript_segments, gemini_key)
606
+
607
+ # Normalize response
608
+ for clip in scored_clips:
609
+ clip['title'] = clip.get('reason', f'{provider.title()} Selected Clip')
610
+ clip.setdefault('start', 0)
611
+ clip.setdefault('end', clip['start'] + seconds)
612
+ clip.setdefault('viral_score', 80)
613
+
614
+ except Exception as e:
615
+ return jsonify({"error": f"AI Analysis Failed ({provider}): {str(e)}"}), 500
616
+ else:
617
+ # Heuristic
618
+ candidates = extractor.generate_candidates(video_info, seconds, DEFAULT_WINDOW_STEP)
619
+
620
+ logger.info(f"Scoring {len(candidates)} candidates...")
621
+ for i, cand in enumerate(candidates):
622
+ score_data = extractor.score_clip(url, cand['start'], cand['end'], transcript_segments)
623
+ score_data['title'] = cand.get('title', f'Clip {i+1}')
624
+ scored_clips.append(score_data)
625
+
626
+ scored_clips.sort(key=lambda x: x['viral_score'], reverse=True)
627
+
628
+ # Select top non-overlapping clips
629
+ final_clips = []
630
+ used_ranges = []
631
+
632
+ for clip in scored_clips:
633
+ overlaps = False
634
+ for used in used_ranges:
635
+ if not (clip['end'] <= used['start'] or clip['start'] >= used['end']):
636
+ overlaps = True
637
+ break
638
+
639
+ if not overlaps:
640
+ final_clips.append(clip)
641
+ used_ranges.append(clip)
642
+
643
+ if len(final_clips) >= num_clips:
644
+ break
645
+
646
+ import urllib.parse
647
+ encoded_url = urllib.parse.quote(url)
648
+
649
+ for clip in final_clips:
650
+ # Ensure start/end are floats
651
+ clip['start'] = float(clip['start'])
652
+ clip['end'] = float(clip['end'])
653
+ clip['start_formatted'] = seconds_to_time(clip['start'])
654
+ clip['end_formatted'] = seconds_to_time(clip['end'])
655
+ clip['youtube_url'] = f"https://youtube.com/watch?v={video_id}&t={int(clip['start'])}"
656
+ clip['download_link'] = f"/download_file?url={encoded_url}&start={clip['start']}&end={clip['end']}&quality={quality}"
657
+
658
+ return jsonify({
659
+ "success": True,
660
+ "video_id": video_id,
661
+ "video_title": video_info.get('title'),
662
+ "video_duration": video_info.get('duration'),
663
+ "mode": mode,
664
+ "clips": final_clips,
665
+ "clips_count": len(final_clips)
666
+ })
667
+
668
+ except Exception as e:
669
+ logger.error(f"Error in /clips: {e}")
670
+ return jsonify({
671
+ "success": False,
672
+ "error": str(e)
673
+ }), 500
674
+
675
+ @app.route('/extract', methods=['POST'])
676
+ def extract_video():
677
+ """Extract basic video information"""
678
+ try:
679
+ data = request.get_json()
680
+ if not data or 'url' not in data:
681
+ return jsonify({"error": "Missing 'url' parameter"}), 400
682
+
683
+ url = data['url']
684
+ video_id = extract_video_id(url)
685
+
686
+ if not video_id:
687
+ return jsonify({"error": "Invalid YouTube URL"}), 400
688
+
689
+ extractor = ViralClipExtractor()
690
+ info = extractor.extract_video_info(url)
691
+
692
+ return jsonify({
693
+ "success": True,
694
+ "video_id": video_id,
695
+ "data": info
696
+ })
697
+
698
+ except Exception as e:
699
+ return jsonify({
700
+ "success": False,
701
+ "error": str(e)
702
+ }), 500
703
+
704
+ @app.route('/analyze', methods=['POST'])
705
+ def analyze_video():
706
+ """Full viral clip analysis"""
707
+ try:
708
+ data = request.get_json()
709
+ if not data or 'url' not in data:
710
+ return jsonify({"error": "Missing 'url' parameter"}), 400
711
+
712
+ url = data['url']
713
+ video_id = extract_video_id(url)
714
+
715
+ if not video_id:
716
+ return jsonify({"error": "Invalid YouTube URL"}), 400
717
+
718
+ num_clips = data.get('num_clips', 5)
719
+ clip_length = data.get('clip_length', DEFAULT_CLIP_LENGTH)
720
+ window_step = data.get('window_step', DEFAULT_WINDOW_STEP)
721
+ quality = data.get('quality', '720')
722
+ mode = data.get('mode', 'heuristic')
723
+ gemini_key = data.get('gemini_key') or os.environ.get('GEMINI_API_KEY')
724
+
725
+ extractor = ViralClipExtractor()
726
+
727
+ # 1. Get video info
728
+ video_info = extractor.extract_video_info(url)
729
+
730
+ # 2. Fetch transcript ONCE
731
+ transcript_segments = extractor.fetch_full_transcript(url)
732
+
733
+ if mode == 'ai':
734
+ if not gemini_key:
735
+ return jsonify({"error": "Gemini API Key required for AI mode"}), 400
736
+
737
+ logger.info("Using Gemini AI for analysis")
738
+ scored_clips = extractor.analyze_with_gemini(transcript_segments, gemini_key)
739
+
740
+ # Normalize AI response
741
+ for clip in scored_clips:
742
+ clip['title'] = clip.get('reason', 'AI Selected Clip')
743
+ clip.setdefault('start', 0)
744
+ clip.setdefault('end', clip['start'] + clip_length)
745
+ clip.setdefault('viral_score', 80)
746
+ else:
747
+ # 3. Generate candidates
748
+ candidates = extractor.generate_candidates(video_info, clip_length, window_step)
749
+
750
+ # 4. Score candidates
751
+ scored_clips = []
752
+ logger.info(f"Scoring {len(candidates)} candidates...")
753
+ for i, cand in enumerate(candidates):
754
+ score_data = extractor.score_clip(url, cand['start'], cand['end'], transcript_segments)
755
+ score_data['title'] = cand.get('title', f'Clip {i+1}')
756
+ scored_clips.append(score_data)
757
+
758
+ # Sort by viral score
759
+ scored_clips.sort(key=lambda x: x['viral_score'], reverse=True)
760
+
761
+ # Select top non-overlapping clips
762
+ final_clips = []
763
+ used_ranges = []
764
+
765
+ for clip in scored_clips:
766
+ overlaps = False
767
+ for used in used_ranges:
768
+ if not (clip['end'] <= used['start'] or clip['start'] >= used['end']):
769
+ overlaps = True
770
+ break
771
+
772
+ if not overlaps:
773
+ final_clips.append(clip)
774
+ used_ranges.append(clip)
775
+
776
+ if len(final_clips) >= num_clips:
777
+ break
778
+
779
+ # Add formatted times and YouTube URLs and Download Links
780
+ import urllib.parse
781
+ encoded_url = urllib.parse.quote(url)
782
+
783
+ for clip in final_clips:
784
+ clip['start'] = float(clip['start'])
785
+ clip['end'] = float(clip['end'])
786
+ clip['start_formatted'] = seconds_to_time(clip['start'])
787
+ clip['end_formatted'] = seconds_to_time(clip['end'])
788
+ clip['youtube_url'] = f"https://youtube.com/watch?v={video_id}&t={int(clip['start'])}"
789
+ clip['download_link'] = f"/download_file?url={encoded_url}&start={clip['start']}&end={clip['end']}&quality={quality}"
790
+
791
+ return jsonify({
792
+ "success": True,
793
+ "video_id": video_id,
794
+ "video_title": video_info.get('title'),
795
+ "video_duration": video_info.get('duration'),
796
+ "clips": final_clips
797
+ })
798
+
799
+ except Exception as e:
800
+ logger.error(f"Error in /analyze: {e}")
801
+ return jsonify({
802
+ "success": False,
803
+ "error": str(e)
804
+ }), 500
805
+
806
+ @app.route('/download_file', methods=['GET'])
807
+ def download_file_endpoint():
808
+ """Download the actual clip file"""
809
+ try:
810
+ url = request.args.get('url')
811
+ if not url:
812
+ return jsonify({"error": "Missing 'url' parameter"}), 400
813
+
814
+ start = float(request.args.get('start', 0))
815
+ end = float(request.args.get('end', 0))
816
+ quality = request.args.get('quality', '720')
817
+
818
+ if end <= start:
819
+ return jsonify({"error": "Invalid time range"}), 400
820
+
821
+ extractor = ViralClipExtractor()
822
+ file_path = extractor.download_clip(url, start, end, quality)
823
+
824
+ if file_path and os.path.exists(file_path):
825
+ @after_this_request
826
+ def remove_file(response):
827
+ try:
828
+ pass # windows file locking might prevent removal if not closed, but send_file should handle it
829
+ # os.remove(file_path) # Defer removal or use a temp dir cleaner properly
830
+ except Exception as error:
831
+ app.logger.error("Error removing file", error)
832
+ return response
833
+
834
+ return send_file(file_path, as_attachment=True, download_name=os.path.basename(file_path))
835
+ else:
836
+ return jsonify({"error": "Failed to download clip"}), 500
837
+
838
+ except Exception as e:
839
+ logger.error(f"Error in /download: {e}")
840
+ return jsonify({
841
+ "success": False,
842
+ "error": str(e)
843
+ }), 500
844
+
845
+ @app.route('/docs', methods=['GET'])
846
+ def docs():
847
+ """API Documentation & Credits"""
848
+ html = """
849
+ <!DOCTYPE html>
850
+ <html lang="en">
851
+ <head>
852
+ <meta charset="UTF-8">
853
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
854
+ <title>Viral Clip Extractor API | Documentation</title>
855
+ <style>
856
+ :root { --primary: #3b82f6; --secondary: #8b5cf6; --bg: #0f172a; --text: #f8fafc; --card-bg: #1e293b; }
857
+ body {
858
+ font-family: 'Segoe UI', system-ui, sans-serif;
859
+ line-height: 1.6;
860
+ color: var(--text);
861
+ background: var(--bg);
862
+ max-width: 900px;
863
+ margin: 0 auto;
864
+ padding: 40px 20px;
865
+ opacity: 0;
866
+ animation: fadeIn 0.8s ease-out forwards;
867
+ }
868
+
869
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
870
+ @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } }
871
+ @keyframes gradient { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
872
+
873
+ h1 { font-size: 3rem; margin-bottom: 0.5rem; background: linear-gradient(to right, #60a5fa, #c084fc); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
874
+ h2 { margin-top: 3rem; border-bottom: 2px solid #334155; padding-bottom: 0.5rem; color: #94a3b8; }
875
+ h3 { color: #60a5fa; margin-top: 0; }
876
+
877
+ .badge { background: linear-gradient(135deg, var(--primary), var(--secondary)); color: white; padding: 4px 12px; border-radius: 20px; font-size: 0.9rem; vertical-align: middle; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); }
878
+
879
+ .card {
880
+ background: var(--card-bg);
881
+ padding: 25px;
882
+ border-radius: 16px;
883
+ box-shadow: 0 10px 15px -3px rgba(0,0,0,0.3);
884
+ margin-bottom: 25px;
885
+ border: 1px solid #334155;
886
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
887
+ }
888
+ .card:hover { transform: translateY(-5px); box-shadow: 0 20px 25px -5px rgba(0,0,0,0.4); border-color: #60a5fa; }
889
+
890
+ code { font-family: 'Consolas', monospace; background: #0f172a; padding: 4px 8px; border-radius: 6px; color: #f472b6; border: 1px solid #334155; }
891
+ pre { background: #020617; color: #e2e8f0; padding: 20px; border-radius: 12px; overflow-x: auto; font-size: 0.9rem; border: 1px solid #334155; }
892
+
893
+ table { width: 100%; border-collapse: collapse; margin: 15px 0; }
894
+ th, td { text-align: left; padding: 12px; border-bottom: 1px solid #334155; }
895
+ th { color: #94a3b8; font-weight: 600; }
896
+
897
+ .method { font-weight: bold; color: #34d399; }
898
+ .url { color: #cbd5e1; }
899
+
900
+ .warning { background: rgba(249, 115, 22, 0.1); border-left: 4px solid #f97316; padding: 15px; margin: 20px 0; border-radius: 0 8px 8px 0; }
901
+ .tip { background: rgba(34, 197, 94, 0.1); border-left: 4px solid #22c55e; padding: 15px; margin: 20px 0; border-radius: 0 8px 8px 0; }
902
+
903
+ .creator-highlight {
904
+ background: linear-gradient(270deg, #ff00cc, #333399, #60a5fa);
905
+ background-size: 600% 600%;
906
+ -webkit-background-clip: text;
907
+ -webkit-text-fill-color: transparent;
908
+ font-weight: 900;
909
+ font-size: 1.5em;
910
+ animation: gradient 3s ease infinite;
911
+ display: inline-block;
912
+ padding: 0 5px;
913
+ }
914
+
915
+ footer { margin-top: 80px; text-align: center; color: #64748b; font-size: 1.1rem; border-top: 1px solid #334155; padding-top: 40px; }
916
+ </style>
917
+ </head>
918
+ <body>
919
+ <h1>Viral Clip Extractor API <span class="badge">v1.3</span></h1>
920
+ <p class="lead">Transform long-form YouTube videos into engaging viral clips using sophisticated AI analysis.</p>
921
+
922
+ <div class="warning">
923
+ <strong>🔐 Security Best Practice:</strong> Do not pass API keys in URLs for public applications.
924
+ Configure <code>GEMINI_API_KEY</code> and <code>NVIDIA_API_KEY</code> as Environment Variables on your server.
925
+ </div>
926
+
927
+ <h2>🧠 Analysis Modes & Usage</h2>
928
+
929
+ <div class="card">
930
+ <h3>1. Heuristic Mode (Non-AI)</h3>
931
+ <p><strong>Best for:</strong> High-energy content, gaming, reactions. Fast and free.</p>
932
+ <div class="tip">No API Key required.</div>
933
+ <pre>GET /clips?url={youtube_url}&mode=heuristic&num=5</pre>
934
+ </div>
935
+
936
+ <div class="card">
937
+ <h3>2. Gemini AI Mode</h3>
938
+ <p><strong>Best for:</strong> Podcasts, storytelling, general dialogue. Balanced performance.</p>
939
+ <div class="warning">Requires <code>GEMINI_API_KEY</code> environment variable (or param).</div>
940
+ <pre>GET /clips?url={youtube_url}&mode=ai&num=5</pre>
941
+ </div>
942
+
943
+ <div class="card">
944
+ <h3>3. Nvidia/DeepSeek AI Mode</h3>
945
+ <p><strong>Best for:</strong> Technical content, debates, complex reasoning.</p>
946
+ <div class="warning">Requires <code>NVIDIA_API_KEY</code> environment variable (or param).</div>
947
+ <pre>GET /clips?url={youtube_url}&mode=nvidia&num=5</pre>
948
+ </div>
949
+
950
+ <h2>📚 Endpoints</h2>
951
+
952
+ <div class="card">
953
+ <h3>GET /clips</h3>
954
+ <p>Main endpoint to analyze and get clip suggestions.</p>
955
+
956
+ <h4>Parameters</h4>
957
+ <table>
958
+ <tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr>
959
+ <tr><td><code>url</code></td><td>string</td><td>Yes</td><td>YouTube Video URL</td></tr>
960
+ <tr><td><code>mode</code></td><td>string</td><td>No</td><td><code>heuristic</code> (default), <code>ai</code>, <code>nvidia</code></td></tr>
961
+ <tr><td><code>num</code></td><td>int</td><td>No</td><td>Number of clips (default: 5)</td></tr>
962
+ <tr><td><code>quality</code></td><td>string</td><td>No</td><td>Video height (e.g., <code>720</code>, <code>1080</code>)</td></tr>
963
+ </table>
964
+
965
+ <h4>Example Response</h4>
966
+ <pre>{
967
+ "success": true,
968
+ "video_title": "Podcast Episode 1",
969
+ "mode": "ai",
970
+ "clips": [
971
+ {
972
+ "title": "Shocking Reveal",
973
+ "start": 120.5,
974
+ "end": 150.0,
975
+ "viral_score": 95,
976
+ "download_link": "/download_file?url=...&start=120.5&end=150"
977
+ }
978
+ ]
979
+ }</pre>
980
+ </div>
981
+
982
+ <div class="card">
983
+ <h3>GET /download_file</h3>
984
+ <p>Download a specific clip directly.</p>
985
+ <pre>GET /download_file?url={url}&start={start}&end={end}</pre>
986
+ </div>
987
+
988
+ <footer>
989
+ <p>Developed with ❤️ by <span class="creator-highlight">yash@dev</span></p>
990
+ <p>&copy; 2026 Viral Clip Extractor API</p>
991
+ </footer>
992
+ </body>
993
+ </html>
994
+ """
995
+ return html
996
+
997
+ # Local development and Production
998
+ if __name__ == '__main__':
999
+ port = int(os.environ.get('PORT', 5000))
1000
+ app.run(debug=True, host='0.0.0.0', port=port)
1001
+
debug_ffmpeg.log ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
2
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
3
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
4
+ Selected: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
5
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
6
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
7
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
8
+ Selected: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
9
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
10
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
11
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
12
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
13
+ Using Dir: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
14
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
15
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
16
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
17
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
18
+ Using Dir: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
19
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
20
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
21
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
22
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
23
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
24
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
25
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
26
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
27
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
28
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
29
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
30
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
31
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
32
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
33
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
34
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
35
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
36
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
37
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
38
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
39
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
40
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
41
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
42
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
43
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
44
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
45
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
46
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
47
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
48
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
49
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
50
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
51
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
52
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
53
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
54
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
55
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
56
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
57
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
58
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
59
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
60
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
61
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
62
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
63
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
64
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
65
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
66
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
67
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
68
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
69
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
70
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
71
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
72
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
73
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
74
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
75
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
76
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
77
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
78
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
79
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
80
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
81
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
82
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
83
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
84
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
85
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
86
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
87
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
88
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
89
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
90
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
91
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
92
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
93
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
94
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
95
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
96
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
97
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
98
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
99
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
100
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
101
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
102
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
103
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
104
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
105
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
106
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
107
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
108
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
109
+ Project Root: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor
110
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
111
+ Checking: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe -> True
112
+ Selected File: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin\ffmpeg.exe
113
+ Adding to PATH: C:\Users\YASHWANTH\Downloads\Kimi_Agent_Viral Clip Extractor\ffmpeg\bin
diagnose_api.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import requests
3
+ import time
4
+ import sys
5
+
6
+ BASE_URL = "http://127.0.0.1:5000"
7
+
8
+ def test_health():
9
+ print("Checking /health...")
10
+ try:
11
+ t0 = time.time()
12
+ resp = requests.get(f"{BASE_URL}/health", timeout=5)
13
+ print(f"Health Status: {resp.status_code}")
14
+ print(f"Response: {resp.text}")
15
+ print(f"Time: {time.time() - t0:.2f}s")
16
+ return True
17
+ except Exception as e:
18
+ print(f"Health Check Failed: {e}")
19
+ return False
20
+
21
+ def test_clips():
22
+ print("\nChecking /clips (simplified)...")
23
+ url = "https://youtu.be/uVkFrqugXFQ" # The user's URL
24
+ try:
25
+ t0 = time.time()
26
+ print(f"Requesting clips for {url}...")
27
+ resp = requests.get(f"{BASE_URL}/clips", params={
28
+ "url": url,
29
+ "seconds": 30,
30
+ "num": 2
31
+ }, timeout=60)
32
+ print(f"Clips Status: {resp.status_code}")
33
+ print(f"Time: {time.time() - t0:.2f}s")
34
+ if resp.status_code == 200:
35
+ data = resp.json()
36
+ print(f"Success! Got {len(data.get('clips', []))} clips")
37
+ print(f"Video: {data.get('video_title')}")
38
+ else:
39
+ print(f"Error: {resp.text}")
40
+ except requests.exceptions.Timeout:
41
+ print("❌ Request timed out after 60s")
42
+ except Exception as e:
43
+ print(f"Error: {e}")
44
+
45
+ if __name__ == "__main__":
46
+ if test_health():
47
+ test_clips()
error.log ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Download Error: ERROR: You have requested downloading the video partially, but ffmpeg is not installed. Aborting
2
+ Download Error: ERROR: You have requested downloading the video partially, but ffmpeg is not installed. Aborting
3
+ Download Error: ERROR: You have requested downloading the video partially, but ffmpeg is not installed. Aborting
examples/frontend-example.html ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Viral Clip Extractor - Frontend Demo</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
17
+ min-height: 100vh;
18
+ color: #fff;
19
+ padding: 20px;
20
+ }
21
+
22
+ .container {
23
+ max-width: 900px;
24
+ margin: 0 auto;
25
+ }
26
+
27
+ h1 {
28
+ text-align: center;
29
+ margin-bottom: 10px;
30
+ font-size: 2.5rem;
31
+ background: linear-gradient(90deg, #ff6b6b, #feca57);
32
+ -webkit-background-clip: text;
33
+ -webkit-text-fill-color: transparent;
34
+ }
35
+
36
+ .subtitle {
37
+ text-align: center;
38
+ color: #888;
39
+ margin-bottom: 30px;
40
+ }
41
+
42
+ .input-section {
43
+ background: rgba(255,255,255,0.05);
44
+ border-radius: 16px;
45
+ padding: 25px;
46
+ margin-bottom: 20px;
47
+ border: 1px solid rgba(255,255,255,0.1);
48
+ }
49
+
50
+ .form-group {
51
+ margin-bottom: 15px;
52
+ }
53
+
54
+ label {
55
+ display: block;
56
+ margin-bottom: 8px;
57
+ color: #aaa;
58
+ font-size: 0.9rem;
59
+ }
60
+
61
+ input, select {
62
+ width: 100%;
63
+ padding: 12px 16px;
64
+ border: 1px solid rgba(255,255,255,0.2);
65
+ border-radius: 8px;
66
+ background: rgba(0,0,0,0.3);
67
+ color: #fff;
68
+ font-size: 1rem;
69
+ }
70
+
71
+ input:focus, select:focus {
72
+ outline: none;
73
+ border-color: #ff6b6b;
74
+ }
75
+
76
+ .row {
77
+ display: grid;
78
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
79
+ gap: 15px;
80
+ }
81
+
82
+ button {
83
+ width: 100%;
84
+ padding: 14px 24px;
85
+ background: linear-gradient(90deg, #ff6b6b, #feca57);
86
+ border: none;
87
+ border-radius: 8px;
88
+ color: #1a1a2e;
89
+ font-size: 1rem;
90
+ font-weight: 600;
91
+ cursor: pointer;
92
+ transition: transform 0.2s, box-shadow 0.2s;
93
+ }
94
+
95
+ button:hover {
96
+ transform: translateY(-2px);
97
+ box-shadow: 0 8px 25px rgba(255,107,107,0.3);
98
+ }
99
+
100
+ button:disabled {
101
+ opacity: 0.6;
102
+ cursor: not-allowed;
103
+ transform: none;
104
+ }
105
+
106
+ .loading {
107
+ text-align: center;
108
+ padding: 40px;
109
+ }
110
+
111
+ .spinner {
112
+ width: 50px;
113
+ height: 50px;
114
+ border: 3px solid rgba(255,255,255,0.1);
115
+ border-top-color: #ff6b6b;
116
+ border-radius: 50%;
117
+ animation: spin 1s linear infinite;
118
+ margin: 0 auto 20px;
119
+ }
120
+
121
+ @keyframes spin {
122
+ to { transform: rotate(360deg); }
123
+ }
124
+
125
+ .results {
126
+ display: none;
127
+ }
128
+
129
+ .video-info {
130
+ background: rgba(255,255,255,0.05);
131
+ border-radius: 12px;
132
+ padding: 20px;
133
+ margin-bottom: 20px;
134
+ }
135
+
136
+ .video-info h3 {
137
+ margin-bottom: 10px;
138
+ color: #feca57;
139
+ }
140
+
141
+ .clip-card {
142
+ background: rgba(255,255,255,0.05);
143
+ border-radius: 12px;
144
+ padding: 20px;
145
+ margin-bottom: 15px;
146
+ border-left: 4px solid #ff6b6b;
147
+ transition: transform 0.2s;
148
+ }
149
+
150
+ .clip-card:hover {
151
+ transform: translateX(5px);
152
+ }
153
+
154
+ .clip-header {
155
+ display: flex;
156
+ justify-content: space-between;
157
+ align-items: center;
158
+ margin-bottom: 10px;
159
+ }
160
+
161
+ .clip-rank {
162
+ font-size: 1.5rem;
163
+ font-weight: bold;
164
+ color: #ff6b6b;
165
+ }
166
+
167
+ .clip-score {
168
+ background: linear-gradient(90deg, #ff6b6b, #feca57);
169
+ padding: 5px 15px;
170
+ border-radius: 20px;
171
+ font-weight: bold;
172
+ color: #1a1a2e;
173
+ }
174
+
175
+ .clip-time {
176
+ color: #888;
177
+ font-size: 0.9rem;
178
+ margin-bottom: 10px;
179
+ }
180
+
181
+ .clip-reasons {
182
+ display: flex;
183
+ flex-wrap: wrap;
184
+ gap: 8px;
185
+ margin-bottom: 10px;
186
+ }
187
+
188
+ .reason-tag {
189
+ background: rgba(255,255,255,0.1);
190
+ padding: 4px 10px;
191
+ border-radius: 12px;
192
+ font-size: 0.8rem;
193
+ color: #aaa;
194
+ }
195
+
196
+ .transcript-preview {
197
+ color: #888;
198
+ font-style: italic;
199
+ font-size: 0.9rem;
200
+ margin-top: 10px;
201
+ padding-top: 10px;
202
+ border-top: 1px solid rgba(255,255,255,0.1);
203
+ }
204
+
205
+ .clip-actions {
206
+ margin-top: 15px;
207
+ display: flex;
208
+ gap: 10px;
209
+ }
210
+
211
+ .btn-small {
212
+ padding: 8px 16px;
213
+ font-size: 0.85rem;
214
+ }
215
+
216
+ .btn-secondary {
217
+ background: rgba(255,255,255,0.1);
218
+ color: #fff;
219
+ }
220
+
221
+ .error {
222
+ background: rgba(255,107,107,0.1);
223
+ border: 1px solid #ff6b6b;
224
+ border-radius: 8px;
225
+ padding: 15px;
226
+ color: #ff6b6b;
227
+ text-align: center;
228
+ }
229
+
230
+ .api-config {
231
+ margin-bottom: 20px;
232
+ }
233
+
234
+ .endpoint-selector {
235
+ display: flex;
236
+ gap: 10px;
237
+ margin-bottom: 15px;
238
+ flex-wrap: wrap;
239
+ }
240
+
241
+ .endpoint-btn {
242
+ padding: 8px 16px;
243
+ background: rgba(255,255,255,0.1);
244
+ border: 1px solid rgba(255,255,255,0.2);
245
+ border-radius: 6px;
246
+ color: #fff;
247
+ cursor: pointer;
248
+ transition: all 0.2s;
249
+ }
250
+
251
+ .endpoint-btn.active {
252
+ background: #ff6b6b;
253
+ border-color: #ff6b6b;
254
+ }
255
+ </style>
256
+ </head>
257
+ <body>
258
+ <div class="container">
259
+ <h1>🔥 Viral Clip Extractor</h1>
260
+ <p class="subtitle">Extract the most viral moments from any YouTube video</p>
261
+
262
+ <div class="input-section">
263
+ <div class="api-config">
264
+ <label>API Base URL</label>
265
+ <input type="text" id="apiUrl" placeholder="https://your-api.vercel.app" value="">
266
+ </div>
267
+
268
+ <div class="endpoint-selector">
269
+ <button class="endpoint-btn active" data-endpoint="analyze">Analyze</button>
270
+ <button class="endpoint-btn" data-endpoint="extract">Extract Info</button>
271
+ <button class="endpoint-btn" data-endpoint="transcript">Transcript</button>
272
+ </div>
273
+
274
+ <div class="form-group">
275
+ <label>YouTube URL</label>
276
+ <input type="text" id="videoUrl" placeholder="https://youtube.com/watch?v=...">
277
+ </div>
278
+
279
+ <div id="analyzeOptions" class="options-section">
280
+ <div class="row">
281
+ <div class="form-group">
282
+ <label>Number of Clips</label>
283
+ <input type="number" id="numClips" value="5" min="1" max="20">
284
+ </div>
285
+ <div class="form-group">
286
+ <label>Clip Length (sec)</label>
287
+ <input type="number" id="clipLength" value="40" min="10" max="120">
288
+ </div>
289
+ <div class="form-group">
290
+ <label>Quality</label>
291
+ <select id="quality">
292
+ <option value="360">360p</option>
293
+ <option value="720" selected>720p</option>
294
+ <option value="1080">1080p</option>
295
+ </select>
296
+ </div>
297
+ </div>
298
+ </div>
299
+
300
+ <div id="transcriptOptions" class="options-section" style="display:none;">
301
+ <div class="row">
302
+ <div class="form-group">
303
+ <label>Start Time (sec)</label>
304
+ <input type="number" id="startTime" value="0" min="0">
305
+ </div>
306
+ <div class="form-group">
307
+ <label>End Time (sec)</label>
308
+ <input type="number" id="endTime" value="60" min="10">
309
+ </div>
310
+ </div>
311
+ </div>
312
+
313
+ <button id="analyzeBtn" onclick="analyzeVideo()">
314
+ 🔍 Analyze Video
315
+ </button>
316
+ </div>
317
+
318
+ <div id="loading" class="loading" style="display:none;">
319
+ <div class="spinner"></div>
320
+ <p>Analyzing video for viral clips...</p>
321
+ </div>
322
+
323
+ <div id="error" class="error" style="display:none;"></div>
324
+
325
+ <div id="results" class="results">
326
+ <div id="videoInfo" class="video-info"></div>
327
+ <div id="clipsList"></div>
328
+ </div>
329
+ </div>
330
+
331
+ <script>
332
+ let currentEndpoint = 'analyze';
333
+
334
+ // Endpoint selector
335
+ document.querySelectorAll('.endpoint-btn').forEach(btn => {
336
+ btn.addEventListener('click', () => {
337
+ document.querySelectorAll('.endpoint-btn').forEach(b => b.classList.remove('active'));
338
+ btn.classList.add('active');
339
+ currentEndpoint = btn.dataset.endpoint;
340
+
341
+ // Show/hide options
342
+ document.getElementById('analyzeOptions').style.display =
343
+ currentEndpoint === 'analyze' ? 'block' : 'none';
344
+ document.getElementById('transcriptOptions').style.display =
345
+ currentEndpoint === 'transcript' ? 'block' : 'none';
346
+
347
+ // Update button text
348
+ const btnText = {
349
+ 'analyze': '🔍 Analyze Video',
350
+ 'extract': '📹 Extract Info',
351
+ 'transcript': '📝 Get Transcript'
352
+ };
353
+ document.getElementById('analyzeBtn').textContent = btnText[currentEndpoint];
354
+ });
355
+ });
356
+
357
+ async function analyzeVideo() {
358
+ const apiUrl = document.getElementById('apiUrl').value.trim();
359
+ const videoUrl = document.getElementById('videoUrl').value.trim();
360
+
361
+ if (!apiUrl) {
362
+ showError('Please enter your API base URL');
363
+ return;
364
+ }
365
+ if (!videoUrl) {
366
+ showError('Please enter a YouTube URL');
367
+ return;
368
+ }
369
+
370
+ // Show loading
371
+ document.getElementById('loading').style.display = 'block';
372
+ document.getElementById('results').style.display = 'none';
373
+ document.getElementById('error').style.display = 'none';
374
+
375
+ const endpoint = `${apiUrl.replace(/\/$/, '')}/${currentEndpoint}`;
376
+
377
+ let body = { url: videoUrl };
378
+
379
+ if (currentEndpoint === 'analyze') {
380
+ body.num_clips = parseInt(document.getElementById('numClips').value);
381
+ body.clip_length = parseInt(document.getElementById('clipLength').value);
382
+ body.quality = document.getElementById('quality').value;
383
+ } else if (currentEndpoint === 'transcript') {
384
+ body.start = parseInt(document.getElementById('startTime').value);
385
+ body.end = parseInt(document.getElementById('endTime').value);
386
+ }
387
+
388
+ try {
389
+ const response = await fetch(endpoint, {
390
+ method: 'POST',
391
+ headers: { 'Content-Type': 'application/json' },
392
+ body: JSON.stringify(body)
393
+ });
394
+
395
+ const data = await response.json();
396
+
397
+ if (!data.success) {
398
+ throw new Error(data.error || 'Unknown error');
399
+ }
400
+
401
+ if (currentEndpoint === 'analyze') {
402
+ displayResults(data);
403
+ } else if (currentEndpoint === 'extract') {
404
+ displayVideoInfo(data.data);
405
+ } else if (currentEndpoint === 'transcript') {
406
+ displayTranscript(data);
407
+ }
408
+
409
+ } catch (err) {
410
+ showError(err.message);
411
+ } finally {
412
+ document.getElementById('loading').style.display = 'none';
413
+ }
414
+ }
415
+
416
+ function displayResults(data) {
417
+ document.getElementById('results').style.display = 'block';
418
+
419
+ // Video info
420
+ const videoInfo = document.getElementById('videoInfo');
421
+ videoInfo.innerHTML = `
422
+ <h3>📹 ${data.video_title}</h3>
423
+ <p>Duration: ${formatTime(data.video_duration)} | Clips found: ${data.clips.length}</p>
424
+ `;
425
+
426
+ // Clips
427
+ const clipsList = document.getElementById('clipsList');
428
+ clipsList.innerHTML = data.clips.map((clip, i) => `
429
+ <div class="clip-card">
430
+ <div class="clip-header">
431
+ <span class="clip-rank">#${i + 1}</span>
432
+ <span class="clip-score">${clip.viral_score}% Viral</span>
433
+ </div>
434
+ <div class="clip-time">
435
+ ⏱️ ${clip.start_formatted} - ${clip.end_formatted} (${clip.duration}s)
436
+ </div>
437
+ <div class="clip-reasons">
438
+ ${clip.reasons.map(r => `<span class="reason-tag">${r}</span>`).join('')}
439
+ </div>
440
+ ${clip.transcript_preview ? `
441
+ <div class="transcript-preview">
442
+ "${clip.transcript_preview}"
443
+ </div>
444
+ ` : ''}
445
+ <div class="clip-actions">
446
+ <a href="${clip.youtube_url}" target="_blank" class="btn-small">Open on YouTube</a>
447
+ </div>
448
+ </div>
449
+ `).join('');
450
+ }
451
+
452
+ function displayVideoInfo(data) {
453
+ document.getElementById('results').style.display = 'block';
454
+ document.getElementById('videoInfo').innerHTML = `
455
+ <h3>📹 ${data.title}</h3>
456
+ <p><strong>Channel:</strong> ${data.uploader}</p>
457
+ <p><strong>Duration:</strong> ${formatTime(data.duration)}</p>
458
+ <p><strong>Views:</strong> ${data.view_count?.toLocaleString() || 'N/A'}</p>
459
+ <p><strong>Upload Date:</strong> ${data.upload_date || 'N/A'}</p>
460
+ <p><strong>Chapters:</strong> ${data.chapters?.length || 0}</p>
461
+ `;
462
+ document.getElementById('clipsList').innerHTML = '';
463
+ }
464
+
465
+ function displayTranscript(data) {
466
+ document.getElementById('results').style.display = 'block';
467
+ document.getElementById('videoInfo').innerHTML = `
468
+ <h3>📝 Transcript</h3>
469
+ <p>Time: ${data.start_formatted} - ${data.end_formatted}</p>
470
+ <p>Words: ${data.word_count}</p>
471
+ `;
472
+ document.getElementById('clipsList').innerHTML = `
473
+ <div class="clip-card">
474
+ <p style="line-height: 1.6;">${data.transcript || 'No transcript available'}</p>
475
+ </div>
476
+ `;
477
+ }
478
+
479
+ function showError(message) {
480
+ document.getElementById('error').textContent = message;
481
+ document.getElementById('error').style.display = 'block';
482
+ }
483
+
484
+ function formatTime(seconds) {
485
+ if (!seconds) return 'N/A';
486
+ const mins = Math.floor(seconds / 60);
487
+ const secs = seconds % 60;
488
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
489
+ }
490
+ </script>
491
+ </body>
492
+ </html>
examples/postman-collection.json ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "info": {
3
+ "name": "Viral Clip Extractor API",
4
+ "description": "API collection for extracting viral clips from YouTube videos",
5
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
6
+ },
7
+ "item": [
8
+ {
9
+ "name": "Health Check",
10
+ "request": {
11
+ "method": "GET",
12
+ "header": [],
13
+ "url": {
14
+ "raw": "{{base_url}}/health",
15
+ "host": ["{{base_url}}"],
16
+ "path": ["health"]
17
+ },
18
+ "description": "Check if API is running"
19
+ }
20
+ },
21
+ {
22
+ "name": "API Info",
23
+ "request": {
24
+ "method": "GET",
25
+ "header": [],
26
+ "url": {
27
+ "raw": "{{base_url}}",
28
+ "host": ["{{base_url}}"]
29
+ },
30
+ "description": "Get API information"
31
+ }
32
+ },
33
+ {
34
+ "name": "Extract Video Info",
35
+ "request": {
36
+ "method": "POST",
37
+ "header": [
38
+ {
39
+ "key": "Content-Type",
40
+ "value": "application/json"
41
+ }
42
+ ],
43
+ "body": {
44
+ "mode": "raw",
45
+ "raw": "{\n \"url\": \"https://youtube.com/watch?v=VIDEO_ID\"\n}"
46
+ },
47
+ "url": {
48
+ "raw": "{{base_url}}/extract",
49
+ "host": ["{{base_url}}"],
50
+ "path": ["extract"]
51
+ },
52
+ "description": "Extract basic video information"
53
+ }
54
+ },
55
+ {
56
+ "name": "Analyze Video (Full)",
57
+ "request": {
58
+ "method": "POST",
59
+ "header": [
60
+ {
61
+ "key": "Content-Type",
62
+ "value": "application/json"
63
+ }
64
+ ],
65
+ "body": {
66
+ "mode": "raw",
67
+ "raw": "{\n \"url\": \"https://youtube.com/watch?v=VIDEO_ID\",\n \"num_clips\": 5,\n \"clip_length\": 40,\n \"window_step\": 10,\n \"quality\": \"720\"\n}"
68
+ },
69
+ "url": {
70
+ "raw": "{{base_url}}/analyze",
71
+ "host": ["{{base_url}}"],
72
+ "path": ["analyze"]
73
+ },
74
+ "description": "Full viral clip analysis"
75
+ }
76
+ },
77
+ {
78
+ "name": "Score Time Range",
79
+ "request": {
80
+ "method": "POST",
81
+ "header": [
82
+ {
83
+ "key": "Content-Type",
84
+ "value": "application/json"
85
+ }
86
+ ],
87
+ "body": {
88
+ "mode": "raw",
89
+ "raw": "{\n \"url\": \"https://youtube.com/watch?v=VIDEO_ID\",\n \"start\": \"2:00\",\n \"end\": \"2:40\"\n}"
90
+ },
91
+ "url": {
92
+ "raw": "{{base_url}}/score",
93
+ "host": ["{{base_url}}"],
94
+ "path": ["score"]
95
+ },
96
+ "description": "Score a specific time range"
97
+ }
98
+ },
99
+ {
100
+ "name": "Get Transcript",
101
+ "request": {
102
+ "method": "POST",
103
+ "header": [
104
+ {
105
+ "key": "Content-Type",
106
+ "value": "application/json"
107
+ }
108
+ ],
109
+ "body": {
110
+ "mode": "raw",
111
+ "raw": "{\n \"url\": \"https://youtube.com/watch?v=VIDEO_ID\",\n \"start\": 120,\n \"end\": 180\n}"
112
+ },
113
+ "url": {
114
+ "raw": "{{base_url}}/transcript",
115
+ "host": ["{{base_url}}"],
116
+ "path": ["transcript"]
117
+ },
118
+ "description": "Get transcript for a time range"
119
+ }
120
+ },
121
+ {
122
+ "name": "Get Download URL",
123
+ "request": {
124
+ "method": "POST",
125
+ "header": [
126
+ {
127
+ "key": "Content-Type",
128
+ "value": "application/json"
129
+ }
130
+ ],
131
+ "body": {
132
+ "mode": "raw",
133
+ "raw": "{\n \"url\": \"https://youtube.com/watch?v=VIDEO_ID\",\n \"start\": \"2:00\",\n \"end\": \"2:40\",\n \"quality\": \"720\"\n}"
134
+ },
135
+ "url": {
136
+ "raw": "{{base_url}}/download",
137
+ "host": ["{{base_url}}"],
138
+ "path": ["download"]
139
+ },
140
+ "description": "Get download URL for a clip"
141
+ }
142
+ }
143
+ ],
144
+ "variable": [
145
+ {
146
+ "key": "base_url",
147
+ "value": "https://your-api.vercel.app",
148
+ "type": "string",
149
+ "description": "Your API base URL"
150
+ }
151
+ ]
152
+ }
examples/python-client.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Viral Clip Extractor - Python Client Example
3
+ Usage: python python-client.py
4
+ """
5
+
6
+ import requests
7
+ import json
8
+
9
+ # Configuration
10
+ API_BASE_URL = "https://your-api.vercel.app" # Replace with your API URL
11
+
12
+
13
+ def analyze_video(url: str, num_clips: int = 5, clip_length: int = 40, quality: str = "720"):
14
+ """
15
+ Analyze a YouTube video for viral clips
16
+
17
+ Args:
18
+ url: YouTube video URL
19
+ num_clips: Number of clips to return
20
+ clip_length: Target clip length in seconds
21
+ quality: Video quality (360, 720, 1080)
22
+ """
23
+ endpoint = f"{API_BASE_URL}/analyze"
24
+
25
+ payload = {
26
+ "url": url,
27
+ "num_clips": num_clips,
28
+ "clip_length": clip_length,
29
+ "quality": quality
30
+ }
31
+
32
+ print(f"🔍 Analyzing: {url}")
33
+ print(f" Clips: {num_clips} | Length: {clip_length}s | Quality: {quality}p")
34
+ print("-" * 60)
35
+
36
+ try:
37
+ response = requests.post(endpoint, json=payload, timeout=60)
38
+ response.raise_for_status()
39
+ data = response.json()
40
+
41
+ if not data.get("success"):
42
+ print(f"❌ Error: {data.get('error')}")
43
+ return None
44
+
45
+ print(f"✅ Found {len(data['clips'])} viral clips!")
46
+ print(f"📹 Video: {data['video_title']}")
47
+ print()
48
+
49
+ for i, clip in enumerate(data['clips'], 1):
50
+ print(f"CLIP #{i}")
51
+ print(f" Score: {clip['viral_score']}% viral")
52
+ print(f" Time: {clip['start_formatted']} - {clip['end_formatted']}")
53
+ print(f" Reasons: {', '.join(clip['reasons'])}")
54
+ print(f" Preview: {clip['transcript_preview'][:80]}...")
55
+ print(f" URL: {clip['youtube_url']}")
56
+ print()
57
+
58
+ return data
59
+
60
+ except requests.exceptions.RequestException as e:
61
+ print(f"❌ Request failed: {e}")
62
+ return None
63
+
64
+
65
+ def extract_video_info(url: str):
66
+ """Extract basic video information"""
67
+ endpoint = f"{API_BASE_URL}/extract"
68
+
69
+ print(f"📹 Extracting info: {url}")
70
+
71
+ try:
72
+ response = requests.post(endpoint, json={"url": url}, timeout=30)
73
+ response.raise_for_status()
74
+ data = response.json()
75
+
76
+ if data.get("success"):
77
+ info = data["data"]
78
+ print(f"✅ Title: {info['title']}")
79
+ print(f" Channel: {info['uploader']}")
80
+ print(f" Duration: {info['duration']}s")
81
+ print(f" Views: {info.get('view_count', 'N/A')}")
82
+ return info
83
+ else:
84
+ print(f"❌ Error: {data.get('error')}")
85
+ return None
86
+
87
+ except requests.exceptions.RequestException as e:
88
+ print(f"❌ Request failed: {e}")
89
+ return None
90
+
91
+
92
+ def get_transcript(url: str, start: int = 0, end: int = 60):
93
+ """Get transcript for a specific time range"""
94
+ endpoint = f"{API_BASE_URL}/transcript"
95
+
96
+ payload = {
97
+ "url": url,
98
+ "start": start,
99
+ "end": end
100
+ }
101
+
102
+ print(f"📝 Getting transcript: {start}s - {end}s")
103
+
104
+ try:
105
+ response = requests.post(endpoint, json=payload, timeout=30)
106
+ response.raise_for_status()
107
+ data = response.json()
108
+
109
+ if data.get("success"):
110
+ print(f"✅ Transcript ({data['word_count']} words):")
111
+ print(f" {data['transcript'][:200]}...")
112
+ return data
113
+ else:
114
+ print(f"❌ Error: {data.get('error')}")
115
+ return None
116
+
117
+ except requests.exceptions.RequestException as e:
118
+ print(f"❌ Request failed: {e}")
119
+ return None
120
+
121
+
122
+ def score_clip(url: str, start: str, end: str):
123
+ """Score a specific time range for viral potential"""
124
+ endpoint = f"{API_BASE_URL}/score"
125
+
126
+ payload = {
127
+ "url": url,
128
+ "start": start,
129
+ "end": end
130
+ }
131
+
132
+ print(f"🎯 Scoring clip: {start} - {end}")
133
+
134
+ try:
135
+ response = requests.post(endpoint, json=payload, timeout=30)
136
+ response.raise_for_status()
137
+ data = response.json()
138
+
139
+ if data.get("success"):
140
+ clip = data["clip"]
141
+ print(f"✅ Viral Score: {clip['viral_score']}%")
142
+ print(f" Reasons: {', '.join(clip['reasons'])}")
143
+ print(f" Words: {clip['word_count']}")
144
+ return clip
145
+ else:
146
+ print(f"❌ Error: {data.get('error')}")
147
+ return None
148
+
149
+ except requests.exceptions.RequestException as e:
150
+ print(f"❌ Request failed: {e}")
151
+ return None
152
+
153
+
154
+ def get_download_url(url: str, start: str, end: str, quality: str = "720"):
155
+ """Get download URL for a clip"""
156
+ endpoint = f"{API_BASE_URL}/download"
157
+
158
+ payload = {
159
+ "url": url,
160
+ "start": start,
161
+ "end": end,
162
+ "quality": quality
163
+ }
164
+
165
+ print(f"⬇️ Getting download URL: {start} - {end}")
166
+
167
+ try:
168
+ response = requests.post(endpoint, json=payload, timeout=30)
169
+ response.raise_for_status()
170
+ data = response.json()
171
+
172
+ if data.get("success"):
173
+ info = data["download_info"]
174
+ print(f"✅ Download ready!")
175
+ print(f" URL: {info['download_url'][:80]}...")
176
+ print(f" Formats available: {len(info['formats'])}")
177
+ return info
178
+ else:
179
+ print(f"❌ Error: {data.get('error')}")
180
+ return None
181
+
182
+ except requests.exceptions.RequestException as e:
183
+ print(f"❌ Request failed: {e}")
184
+ return None
185
+
186
+
187
+ def batch_analyze(urls: list, num_clips: int = 3):
188
+ """Analyze multiple videos in batch"""
189
+ results = []
190
+
191
+ print(f"🔄 Batch analyzing {len(urls)} videos...")
192
+ print("=" * 60)
193
+
194
+ for i, url in enumerate(urls, 1):
195
+ print(f"\n[{i}/{len(urls)}]")
196
+ result = analyze_video(url, num_clips=num_clips)
197
+ if result:
198
+ results.append(result)
199
+
200
+ print("\n" + "=" * 60)
201
+ print(f"✅ Batch complete! Analyzed {len(results)} videos")
202
+
203
+ return results
204
+
205
+
206
+ # Example usage
207
+ if __name__ == "__main__":
208
+ # Replace with your API URL
209
+ API_BASE_URL = input("Enter your API base URL: ").strip()
210
+
211
+ # Example YouTube URL (replace with actual URL)
212
+ test_url = input("Enter YouTube URL: ").strip()
213
+
214
+ print("\n" + "=" * 60)
215
+ print("VIRAL CLIP EXTRACTOR - Python Client")
216
+ print("=" * 60 + "\n")
217
+
218
+ # Menu
219
+ print("Choose an option:")
220
+ print("1. Full Analysis (find viral clips)")
221
+ print("2. Extract Video Info")
222
+ print("3. Get Transcript")
223
+ print("4. Score Specific Range")
224
+ print("5. Get Download URL")
225
+
226
+ choice = input("\nEnter choice (1-5): ").strip()
227
+
228
+ if choice == "1":
229
+ num = int(input("Number of clips (default 5): ") or "5")
230
+ length = int(input("Clip length in seconds (default 40): ") or "40")
231
+ analyze_video(test_url, num_clips=num, clip_length=length)
232
+
233
+ elif choice == "2":
234
+ extract_video_info(test_url)
235
+
236
+ elif choice == "3":
237
+ start = int(input("Start time in seconds: "))
238
+ end = int(input("End time in seconds: "))
239
+ get_transcript(test_url, start, end)
240
+
241
+ elif choice == "4":
242
+ start = input("Start time (MM:SS or seconds): ")
243
+ end = input("End time (MM:SS or seconds): ")
244
+ score_clip(test_url, start, end)
245
+
246
+ elif choice == "5":
247
+ start = input("Start time (MM:SS or seconds): ")
248
+ end = input("End time (MM:SS or seconds): ")
249
+ quality = input("Quality (360/720/1080, default 720): ") or "720"
250
+ get_download_url(test_url, start, end, quality)
251
+
252
+ else:
253
+ print("Invalid choice!")
install_ffmpeg.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import urllib.request
3
+ import zipfile
4
+ import os
5
+ import shutil
6
+
7
+ FFMPEG_URL = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip"
8
+ DOWNLOAD_path = "ffmpeg.zip"
9
+ EXTRACT_DIR = "ffmpeg_temp"
10
+ FINAL_DIR = "ffmpeg"
11
+
12
+ def install_ffmpeg():
13
+ print(f"Downloading FFmpeg from {FFMPEG_URL}...")
14
+ try:
15
+ urllib.request.urlretrieve(FFMPEG_URL, DOWNLOAD_path)
16
+ print("Download complete.")
17
+
18
+ print("Extracting...")
19
+ with zipfile.ZipFile(DOWNLOAD_path, 'r') as zip_ref:
20
+ zip_ref.extractall(EXTRACT_DIR)
21
+
22
+ # Move bin folder to top level
23
+ # The zip structure is usually ffmpeg-master-latest-win64-gpl/bin/ffmpeg.exe
24
+ # We want ffmpeg/bin/ffmpeg.exe
25
+
26
+ extracted_root = os.path.join(EXTRACT_DIR, os.listdir(EXTRACT_DIR)[0])
27
+
28
+ if os.path.exists(FINAL_DIR):
29
+ print("Removing old installation...")
30
+ shutil.rmtree(FINAL_DIR)
31
+
32
+ shutil.move(extracted_root, FINAL_DIR)
33
+
34
+ # Cleanup
35
+ os.remove(DOWNLOAD_path)
36
+ shutil.rmtree(EXTRACT_DIR)
37
+
38
+ print(f"FFmpeg installed to {os.path.abspath(FINAL_DIR)}")
39
+
40
+ # Verify
41
+ bin_path = os.path.join(FINAL_DIR, "bin", "ffmpeg.exe")
42
+ if os.path.exists(bin_path):
43
+ print(f"✅ Verified: {bin_path}")
44
+ else:
45
+ print("❌ Error: ffmpeg.exe not found in expected path")
46
+
47
+ except Exception as e:
48
+ print(f"Error installing FFmpeg: {e}")
49
+
50
+ if __name__ == "__main__":
51
+ install_ffmpeg()
list_models.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import google.generativeai as genai
3
+ import os
4
+
5
+ API_KEY = "AIzaSyAOXzvcFroHyNa5UtYRi3c8AGX0MJ1Tb5E"
6
+ genai.configure(api_key=API_KEY)
7
+
8
+ print("Listing available models...")
9
+ try:
10
+ for m in genai.list_models():
11
+ if 'generateContent' in m.supported_generation_methods:
12
+ print(m.name)
13
+ except Exception as e:
14
+ print(f"Error: {e}")
requirements.txt ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Viral Clip Extractor API - Dependencies
2
+ # Python 3.9+
3
+
4
+ # Web Framework
5
+ flask==3.0.3
6
+ flask-cors==4.0.1
7
+ werkzeug==3.0.3
8
+
9
+ # YouTube Download
10
+ yt-dlp==2024.8.6
11
+ google-generativeai==0.7.2
12
+ openai==1.61.0 # For Nvidia API
13
+ gunicorn==21.2.0 # Production Server
14
+
15
+ # HTTP Requests
16
+ requests==2.32.3
17
+ urllib3==2.2.2
18
+ certifi==2024.7.4
19
+ charset-normalizer==3.3.2
20
+ idna==3.7
21
+
22
+ # Data Processing
23
+ # (Using built-in json, re, typing modules)
24
+
25
+ # Serverless Support
26
+ # Vercel Python runtime handles these automatically
27
+ # python-dateutil is often useful
28
+ python-dateutil==2.8.2
29
+
30
+ # Optional: For better JSON handling
31
+ # (Using built-in json module)
32
+
33
+ # Development dependencies (not needed for deployment)
34
+ # pytest==8.3.2
35
+ # black==24.8.0
36
+ # flake8==7.1.1
test_ai_mode.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import requests
3
+ import os
4
+ import json
5
+ import sys
6
+
7
+ # REPLACE THIS WITH YOUR KEY or set GEMINI_API_KEY env var
8
+ # Get from: https://aistudio.google.com/app/apikey
9
+ DEFAULT_KEY = "AIzaSyAOXzvcFroHyNa5UtYRi3c8AGX0MJ1Tb5E"
10
+ API_KEY = os.environ.get("GEMINI_API_KEY", DEFAULT_KEY)
11
+
12
+ URL = "https://youtu.be/uVkFrqugXFQ"
13
+ BASE_URL = "http://127.0.0.1:5000"
14
+
15
+ def test_ai():
16
+ print("Testing AI Mode...")
17
+
18
+ # Check if key is set
19
+ if "YOUR_GEMINI" in API_KEY:
20
+ print("\nWARNING: You haven't set your Gemini API Key!")
21
+ print("1. Get a key here: https://aistudio.google.com/app/apikey")
22
+ print("2. Open this script and replace 'YOUR_GEMINI_API_KEY_HERE'")
23
+ print(" OR set env var: $env:GEMINI_API_KEY='your_key'")
24
+ return
25
+
26
+ print(f"Analyzing {URL} using Gemini...")
27
+ print("This may take 10-20 seconds...")
28
+
29
+ try:
30
+ # Test the GET /clips endpoint with mode=ai
31
+ resp = requests.get(f"{BASE_URL}/clips", params={
32
+ "url": URL,
33
+ "mode": "ai",
34
+ "gemini_key": API_KEY,
35
+ "num": 3,
36
+ "seconds": 30
37
+ }, timeout=120)
38
+
39
+ if resp.status_code == 200:
40
+ data = resp.json()
41
+ print("\nSUCCESS! AI Candidates Found:")
42
+ clips = data.get('clips', [])
43
+
44
+ if not clips:
45
+ print(" (No clips found. AI might have filtered everything or returned empty)")
46
+
47
+ for i, clip in enumerate(clips):
48
+ print(f"\nClip {i+1}: {clip.get('title')}")
49
+ print(f" Score: {clip.get('viral_score')}")
50
+ print(f" Download: {clip.get('download_link')}")
51
+ print(f" Reason: {clip.get('reason')}")
52
+ else:
53
+ print(f"\nError: {resp.status_code}")
54
+ try:
55
+ print(json.dumps(resp.json(), indent=2))
56
+ except:
57
+ print(resp.text)
58
+
59
+ except Exception as e:
60
+ print(f"\nException: {e}")
61
+
62
+ if __name__ == "__main__":
63
+ test_ai()
test_api.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test suite for Viral Clip Extractor API
3
+ Run with: python -m pytest test_api.py
4
+ """
5
+
6
+ import pytest
7
+ import json
8
+ from api.index import app, extract_video_id, time_to_seconds, seconds_to_time
9
+
10
+ @pytest.fixture
11
+ def client():
12
+ """Create test client"""
13
+ app.config['TESTING'] = True
14
+ with app.test_client() as client:
15
+ yield client
16
+
17
+ def test_health_endpoint(client):
18
+ """Test health check"""
19
+ response = client.get('/health')
20
+ assert response.status_code == 200
21
+ data = json.loads(response.data)
22
+ assert data['status'] == 'healthy'
23
+
24
+ def test_index_endpoint(client):
25
+ """Test API info endpoint"""
26
+ response = client.get('/')
27
+ assert response.status_code == 200
28
+ data = json.loads(response.data)
29
+ assert 'name' in data
30
+ assert 'endpoints' in data
31
+
32
+ def test_extract_video_id():
33
+ """Test video ID extraction from various URLs"""
34
+ # Standard URL
35
+ assert extract_video_id('https://youtube.com/watch?v=dQw4w9WgXcQ') == 'dQw4w9WgXcQ'
36
+ # Short URL
37
+ assert extract_video_id('https://youtu.be/dQw4w9WgXcQ') == 'dQw4w9WgXcQ'
38
+ # Embed URL
39
+ assert extract_video_id('https://youtube.com/embed/dQw4w9WgXcQ') == 'dQw4w9WgXcQ'
40
+ # Shorts URL
41
+ assert extract_video_id('https://youtube.com/shorts/dQw4w9WgXcQ') == 'dQw4w9WgXcQ'
42
+ # Just ID
43
+ assert extract_video_id('dQw4w9WgXcQ') == 'dQw4w9WgXcQ'
44
+ # Invalid
45
+ assert extract_video_id('not-a-valid-url') is None
46
+
47
+ def test_time_conversions():
48
+ """Test time conversion functions"""
49
+ # Seconds to time
50
+ assert seconds_to_time(120) == '2:00'
51
+ assert seconds_to_time(65) == '1:05'
52
+ assert seconds_to_time(0) == '0:00'
53
+
54
+ # Time to seconds
55
+ assert time_to_seconds('2:00') == 120
56
+ assert time_to_seconds('1:30') == 90
57
+ assert time_to_seconds('1:30:45') == 5445
58
+ assert time_to_seconds(120) == 120
59
+
60
+ def test_extract_endpoint_missing_url(client):
61
+ """Test extract endpoint with missing URL"""
62
+ response = client.post('/extract',
63
+ data=json.dumps({}),
64
+ content_type='application/json')
65
+ assert response.status_code == 400
66
+ data = json.loads(response.data)
67
+ assert 'error' in data
68
+
69
+ def test_extract_endpoint_invalid_url(client):
70
+ """Test extract endpoint with invalid URL"""
71
+ response = client.post('/extract',
72
+ data=json.dumps({'url': 'invalid-url'}),
73
+ content_type='application/json')
74
+ assert response.status_code == 400
75
+ data = json.loads(response.data)
76
+ assert 'error' in data
77
+
78
+ def test_analyze_endpoint_missing_url(client):
79
+ """Test analyze endpoint with missing URL"""
80
+ response = client.post('/analyze',
81
+ data=json.dumps({}),
82
+ content_type='application/json')
83
+ assert response.status_code == 400
84
+
85
+ def test_score_endpoint_missing_params(client):
86
+ """Test score endpoint with missing parameters"""
87
+ response = client.post('/score',
88
+ data=json.dumps({'url': 'https://youtube.com/watch?v=test'}),
89
+ content_type='application/json')
90
+ assert response.status_code == 400
91
+
92
+ def test_transcript_endpoint(client):
93
+ """Test transcript endpoint structure"""
94
+ response = client.post('/transcript',
95
+ data=json.dumps({
96
+ 'url': 'https://youtube.com/watch?v=test',
97
+ 'start': 0,
98
+ 'end': 60
99
+ }),
100
+ content_type='application/json')
101
+ # Will fail due to invalid video, but tests structure
102
+ assert response.status_code in [400, 500]
103
+
104
+ def test_cors_headers(client):
105
+ """Test CORS is enabled"""
106
+ response = client.get('/')
107
+ assert 'Access-Control-Allow-Origin' in response.headers
108
+
109
+ if __name__ == '__main__':
110
+ pytest.main([__file__, '-v'])
test_download_clip.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from api.index import ViralClipExtractor
3
+ import os
4
+
5
+ def test_download():
6
+ extractor = ViralClipExtractor()
7
+ url = "https://youtu.be/uVkFrqugXFQ"
8
+ print("Testing download of 5s clip...")
9
+ try:
10
+ # Try to download 5s segment
11
+ path = extractor.download_clip(url, "0:00", "0:05") # Method doesn't exist yet in my class, I need to add it or test raw yt-dlp
12
+ print(f"Downloaded to: {path}")
13
+ except Exception as e:
14
+ print(f"Error: {e}")
15
+
16
+ if __name__ == "__main__":
17
+ # monkey patch for testing purposes or just write raw yt-dlp code here
18
+ import yt_dlp
19
+
20
+ url = "https://youtu.be/uVkFrqugXFQ"
21
+ ydl_opts = {
22
+ 'format': 'bestvideo[height<=720]+bestaudio/best',
23
+ 'outtmpl': 'test_clip.%(ext)s',
24
+ 'download_ranges': lambda info, ydl: [{'start_time': 0, 'end_time': 5}],
25
+ 'force_keyframes_at_cuts': True,
26
+ }
27
+
28
+ try:
29
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
30
+ ydl.download([url])
31
+ print("Download success")
32
+ except Exception as e:
33
+ print(f"Download failed: {e}")
test_download_e2e.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import requests
3
+ import time
4
+ import os
5
+
6
+ BASE_URL = "http://127.0.0.1:5000"
7
+
8
+ def test_e2e_download():
9
+ print("Testing /clips and download link...")
10
+ url = "https://youtu.be/uVkFrqugXFQ"
11
+
12
+ try:
13
+ # 1. Get Clips
14
+ print("Fetching clips...")
15
+ resp = requests.get(f"{BASE_URL}/clips", params={
16
+ "url": url,
17
+ "seconds": 5, # Short clip for speed
18
+ "num": 1
19
+ }, timeout=60)
20
+
21
+ if resp.status_code != 200:
22
+ print(f"Failed to get clips: {resp.text}")
23
+ return
24
+
25
+ data = resp.json()
26
+ clips = data.get('clips', [])
27
+ if not clips:
28
+ print("No clips found in response")
29
+ return
30
+
31
+ clip = clips[0]
32
+ download_link = clip.get('download_link')
33
+ if not download_link:
34
+ print("No download_link in clip data")
35
+ print(clip)
36
+ return
37
+
38
+ print(f"Found download link: {download_link}")
39
+
40
+ # 2. Download File
41
+ full_download_url = f"{BASE_URL}{download_link}"
42
+ print(f"Downloading from {full_download_url}...")
43
+
44
+ t0 = time.time()
45
+ file_resp = requests.get(full_download_url, stream=True, timeout=120)
46
+
47
+ if file_resp.status_code == 200:
48
+ filename = "test_clip_downloaded.mp4"
49
+ with open(filename, 'wb') as f:
50
+ for chunk in file_resp.iter_content(chunk_size=8192):
51
+ f.write(chunk)
52
+
53
+ size = os.path.getsize(filename)
54
+ print(f"Downloaded file: {filename} ({size/1024:.2f} KB)")
55
+ print(f"Time: {time.time() - t0:.2f}s")
56
+ else:
57
+ print(f"Failed to download file: {file_resp.status_code}")
58
+ print(file_resp.text)
59
+
60
+ except Exception as e:
61
+ print(f"Error: {e}")
62
+
63
+ if __name__ == "__main__":
64
+ test_e2e_download()
test_nvidia_mode.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import requests
3
+ import os
4
+ import json
5
+ import sys
6
+
7
+ # REPLACE THIS WITH YOUR NVIDIA KEY or set NVIDIA_API_KEY env var
8
+ # Get from: https://build.nvidia.com/explore/discover
9
+ DEFAULT_KEY = "YOUR_NVIDIA_API_KEY_HERE"
10
+ API_KEY = os.environ.get("NVIDIA_API_KEY", DEFAULT_KEY)
11
+
12
+ URL = "https://youtu.be/uVkFrqugXFQ"
13
+ BASE_URL = "http://127.0.0.1:5000"
14
+
15
+ def test_nvidia():
16
+ print("Testing Nvidia/DeepSeek Mode...")
17
+
18
+ # Check if key is set (simple length check or placeholder check)
19
+ if "YOUR_NVIDIA" in API_KEY:
20
+ print("\nWARNING: You haven't set your Nvidia API Key!")
21
+ print("1. Get a key here: https://build.nvidia.com/explore/discover")
22
+ print("2. Open this script and replace 'YOUR_NVIDIA_API_KEY_HERE'")
23
+ print(" OR set env var: $env:NVIDIA_API_KEY='your_key'")
24
+ return
25
+
26
+ if API_KEY.startswith("AIza"):
27
+ print("\nERROR: You are using a Google Gemini Key (starts with AIza...)!")
28
+ print(" Nvidia keys usually start with 'nvapi-'.")
29
+ print(" Please use 'mode=ai' for Gemini, or get an Nvidia key.")
30
+ return
31
+
32
+ print(f"Analyzing {URL} using DeepSeek V3 (via Nvidia)...")
33
+ print("This may take 15-30 seconds...")
34
+
35
+ try:
36
+ # Test the GET /clips endpoint with mode=nvidia
37
+ resp = requests.get(f"{BASE_URL}/clips", params={
38
+ "url": URL,
39
+ "mode": "nvidia",
40
+ "nvidia_key": API_KEY,
41
+ "num": 3,
42
+ "seconds": 30
43
+ }, timeout=120)
44
+
45
+ if resp.status_code == 200:
46
+ data = resp.json()
47
+ print("\nSUCCESS! AI Candidates Found (DeepSeek):")
48
+ clips = data.get('clips', [])
49
+
50
+ if not clips:
51
+ print(" (No clips found. AI might have filtered everything or returned empty)")
52
+
53
+ for i, clip in enumerate(clips):
54
+ print(f"\nClip {i+1}: {clip.get('title')}")
55
+ print(f" Score: {clip.get('viral_score')}")
56
+ print(f" Download: {clip.get('download_link')}")
57
+ print(f" Reason: {clip.get('reason')}")
58
+ else:
59
+ print(f"\nError: {resp.status_code}")
60
+ try:
61
+ print(json.dumps(resp.json(), indent=2))
62
+ except:
63
+ print(resp.text)
64
+
65
+ except Exception as e:
66
+ print(f"\nException: {e}")
67
+
68
+ if __name__ == "__main__":
69
+ test_nvidia()
vercel.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": 2,
3
+ "name": "viral-clip-extractor-api",
4
+ "builds": [
5
+ {
6
+ "src": "api/index.py",
7
+ "use": "@vercel/python",
8
+ "config": {
9
+ "maxLambdaSize": "50mb",
10
+ "runtime": "python3.9"
11
+ }
12
+ }
13
+ ],
14
+ "routes": [
15
+ {
16
+ "src": "/(.*)",
17
+ "dest": "api/index.py"
18
+ }
19
+ ],
20
+ "env": {
21
+ "PYTHONUNBUFFERED": "1"
22
+ },
23
+ "functions": {
24
+ "api/index.py": {
25
+ "maxDuration": 60
26
+ }
27
+ }
28
+ }