Pepguy commited on
Commit
2f6bc27
·
verified ·
1 Parent(s): a662741

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +21 -124
app.py CHANGED
@@ -1,115 +1,27 @@
1
- from fastapi import FastAPI, HTTPException, Request
2
- from fastapi.responses import HTMLResponse
3
- from pydantic import BaseModel
4
  import subprocess
5
  import base64
6
  import os
7
  import uuid
8
  import shutil
9
- import yt_dlp
10
- import socket
11
- import re
12
-
13
- # Force IPv4
14
- orig_getaddrinfo = socket.getaddrinfo
15
- def hooked_getaddrinfo(*args, **kwargs):
16
- res = orig_getaddrinfo(*args, **kwargs)
17
- return [x for x in res if x[0] == socket.AF_INET]
18
- socket.getaddrinfo = hooked_getaddrinfo
19
 
20
  app = FastAPI()
21
 
22
- class VideoRequest(BaseModel):
23
- url: str
24
- platform: str
25
- is_pro: bool = False
26
- fps: int = 2
27
 
28
  def file_to_base64(filepath):
29
  if not os.path.exists(filepath): return None
30
  with open(filepath, "rb") as f:
31
  return base64.b64encode(f.read()).decode('utf-8')
32
 
33
- def clean_url(url: str):
34
- # Removes trailing text like "This post is shared via..."
35
- match = re.search(r'(https?://[^\s]+)', url)
36
- return match.group(0) if match else url
37
-
38
- @app.get("/", response_class=HTMLResponse)
39
- def index():
40
- return """
41
- <!DOCTYPE html>
42
- <html>
43
- <head>
44
- <title>Viral Cat | Media Worker</title>
45
- <script src="https://cdn.tailwindcss.com"></script>
46
- <style>
47
- body { background: #090A0F; color: white; font-family: sans-serif; }
48
- .neon { color: #12D8C3; text-shadow: 0 0 10px rgba(18,216,195,0.5); }
49
- input { background: #16181F; border: 1px solid #2D3748; color: white; }
50
- </style>
51
- </head>
52
- <body class="p-10">
53
- <div class="max-w-2xl mx-auto bg-[#16181F] p-8 rounded-3xl border border-[#2D3748]">
54
- <h1 class="text-3xl font-bold mb-2">Viral Cat <span class="neon">Media Worker</span></h1>
55
- <p class="text-gray-400 mb-6">Test TikTok or Instagram links below.</p>
56
-
57
- <input type="text" id="url" placeholder="Paste link here..." class="w-full p-4 rounded-xl mb-4">
58
-
59
- <select id="platform" class="w-full p-4 rounded-xl mb-4 bg-[#090A0F] border border-[#2D3748]">
60
- <option value="tiktok">TikTok</option>
61
- <option value="instagram">Instagram Reel</option>
62
- </select>
63
-
64
- <button onclick="process()" id="btn" class="w-full bg-[#12D8C3] text-black font-bold p-4 rounded-xl">Process Video</button>
65
-
66
- <div id="status" class="mt-6 text-sm text-gray-400"></div>
67
- <div id="results" class="mt-6 grid grid-cols-4 gap-2"></div>
68
- </div>
69
-
70
- <script>
71
- async function process() {
72
- const btn = document.getElementById('btn');
73
- const status = document.getElementById('status');
74
- const results = document.getElementById('results');
75
-
76
- btn.disabled = true;
77
- btn.innerText = "Processing...";
78
- status.innerText = "Downloading and extracting frames...";
79
- results.innerHTML = "";
80
-
81
- try {
82
- const res = await fetch('/process-video', {
83
- method: 'POST',
84
- headers: {'Content-Type': 'application/json'},
85
- body: JSON.stringify({
86
- url: document.getElementById('url').value,
87
- platform: document.getElementById('platform').value,
88
- fps: 2
89
- })
90
- });
91
- const data = await res.json();
92
- if(data.success) {
93
- status.innerText = "Success! Extracted " + data.total_frames + " frames.";
94
- data.frames.slice(0, 12).forEach(img => {
95
- results.innerHTML += `<img src="data:image/jpeg;base64,${img}" class="rounded-lg">`;
96
- });
97
- } else {
98
- status.innerText = "Error: " + data.error;
99
- }
100
- } catch(e) {
101
- status.innerText = "Network Error: " + e.message;
102
- }
103
- btn.disabled = false;
104
- btn.innerText = "Process Video";
105
- }
106
- </script>
107
- </body>
108
- </html>
109
- """
110
-
111
  @app.post("/process-video")
112
- def process_video(req: VideoRequest):
 
 
 
 
113
  job_id = str(uuid.uuid4())
114
  work_dir = f"/tmp/viralcat_{job_id}"
115
  os.makedirs(work_dir, exist_ok=True)
@@ -117,29 +29,12 @@ def process_video(req: VideoRequest):
117
  video_path = os.path.join(work_dir, "video.mp4")
118
  audio_path = os.path.join(work_dir, "audio.mp3")
119
 
120
- target_url = clean_url(req.url)
121
-
122
  try:
123
- # 🚨 FORMAT FIX: Use 'best' to avoid split-stream errors on TikTok/IG
124
- ydl_opts = {
125
- 'format': 'best[height<=720]',
126
- 'outtmpl': video_path,
127
- 'quiet': True,
128
- 'no_warnings': True,
129
- 'nocheckcertificate': True,
130
- 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
131
- 'extractor_args': {'youtube': ['player_client=android,web']}
132
- }
133
-
134
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
135
- ydl.download([target_url])
136
-
137
- if not os.path.exists(video_path):
138
- files = os.listdir(work_dir)
139
- if files: video_path = os.path.join(work_dir, files[0])
140
- else: raise ValueError("Download failed - No file saved.")
141
 
142
- # PROBE DURATION
143
  probe = subprocess.run([
144
  "ffprobe", "-v", "error", "-show_entries",
145
  "format=duration", "-of", "default=noprint_wrappers=1:nokey=1",
@@ -147,26 +42,27 @@ def process_video(req: VideoRequest):
147
  ], capture_output=True, text=True, check=True)
148
 
149
  duration = float(probe.stdout.strip() or 0)
150
- if duration > 120 and not req.is_pro:
151
  raise ValueError(f"Video ({duration:.1f}s) exceeds 120s limit.")
152
 
153
- # EXTRACT FRAMES (2 per second)
154
  subprocess.run([
155
  "ffmpeg", "-y", "-i", video_path,
156
- "-vf", f"fps={req.fps}",
157
  "-q:v", "4", f"{work_dir}/frame_%04d.jpg"
158
  ], check=True, capture_output=True)
159
 
160
- # EXTRACT AUDIO
161
  subprocess.run([
162
  "ffmpeg", "-y", "-i", video_path,
163
  "-q:a", "0", "-map", "a", "-ac", "1", "-b:a", "64k", audio_path
164
  ], check=True, capture_output=True)
165
 
 
166
  frame_files = sorted([f for f in os.listdir(work_dir) if f.startswith("frame_") and f.endswith(".jpg")])
167
- if len(frame_files) > 80: frame_files = frame_files[:80]
168
 
169
- frames_b64 = [file_to_base64(os.path.join(work_dir, f)) for f in frame_files]
170
  audio_b64 = file_to_base64(audio_path)
171
 
172
  return {
@@ -182,5 +78,6 @@ def process_video(req: VideoRequest):
182
  return {"success": False, "error": str(e)}
183
 
184
  finally:
 
185
  if os.path.exists(work_dir):
186
  shutil.rmtree(work_dir, ignore_errors=True)
 
1
+ from fastapi import FastAPI, HTTPException, UploadFile, File, Form
 
 
2
  import subprocess
3
  import base64
4
  import os
5
  import uuid
6
  import shutil
 
 
 
 
 
 
 
 
 
 
7
 
8
  app = FastAPI()
9
 
10
+ @app.get("/")
11
+ def greet_json():
12
+ return {"status": "online", "engine": "Viral Cat Local Processor"}
 
 
13
 
14
  def file_to_base64(filepath):
15
  if not os.path.exists(filepath): return None
16
  with open(filepath, "rb") as f:
17
  return base64.b64encode(f.read()).decode('utf-8')
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  @app.post("/process-video")
20
+ async def process_video(
21
+ file: UploadFile = File(...),
22
+ fps: int = Form(2),
23
+ is_pro: bool = Form(False)
24
+ ):
25
  job_id = str(uuid.uuid4())
26
  work_dir = f"/tmp/viralcat_{job_id}"
27
  os.makedirs(work_dir, exist_ok=True)
 
29
  video_path = os.path.join(work_dir, "video.mp4")
30
  audio_path = os.path.join(work_dir, "audio.mp3")
31
 
 
 
32
  try:
33
+ # 1. Save the uploaded file locally
34
+ with open(video_path, "wb") as buffer:
35
+ shutil.copyfileobj(file.file, buffer)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
+ # 2. Check Duration (120s limit for free)
38
  probe = subprocess.run([
39
  "ffprobe", "-v", "error", "-show_entries",
40
  "format=duration", "-of", "default=noprint_wrappers=1:nokey=1",
 
42
  ], capture_output=True, text=True, check=True)
43
 
44
  duration = float(probe.stdout.strip() or 0)
45
+ if duration > 120 and not is_pro:
46
  raise ValueError(f"Video ({duration:.1f}s) exceeds 120s limit.")
47
 
48
+ # 3. Extract Frames (2 per second)
49
  subprocess.run([
50
  "ffmpeg", "-y", "-i", video_path,
51
+ "-vf", f"fps={fps}",
52
  "-q:v", "4", f"{work_dir}/frame_%04d.jpg"
53
  ], check=True, capture_output=True)
54
 
55
+ # 4. Extract Audio
56
  subprocess.run([
57
  "ffmpeg", "-y", "-i", video_path,
58
  "-q:a", "0", "-map", "a", "-ac", "1", "-b:a", "64k", audio_path
59
  ], check=True, capture_output=True)
60
 
61
+ # 5. Base64 Conversion
62
  frame_files = sorted([f for f in os.listdir(work_dir) if f.startswith("frame_") and f.endswith(".jpg")])
63
+ if len(frame_files) > 80: frame_files = frame_files[:80] # Safety cap
64
 
65
+ frames_b64 =[file_to_base64(os.path.join(work_dir, f)) for f in frame_files]
66
  audio_b64 = file_to_base64(audio_path)
67
 
68
  return {
 
78
  return {"success": False, "error": str(e)}
79
 
80
  finally:
81
+ # CLEANUP
82
  if os.path.exists(work_dir):
83
  shutil.rmtree(work_dir, ignore_errors=True)