kavehtaheri commited on
Commit
0824ca5
·
verified ·
1 Parent(s): 0d83276

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +236 -294
app.py CHANGED
@@ -1,307 +1,249 @@
1
- import gradio as gr
2
- import easyocr
3
- from PIL import Image
4
- import numpy as np
5
- import google.generativeai as genai
6
- import time
7
- from gradio_client import Client, handle_file
8
- import requests # <-- ADDED for API calls
9
- import io # <-- ADDED to handle image bytes
10
-
11
- # --- Configuration ---
12
- # Gemini API key
13
- GEMINI_API_KEY = "AIzaSyAKI92YawOKQ1-HRLmvaryMEWk_y4alJgA" # Replace with your actual Gemini API Key
14
-
15
- # One-API Token for Instagram Downloader
16
- # IMPORTANT: Replace with your token from one-api.ir
17
- ONE_API_TOKEN = "268976:66f4f58a2a905"
18
-
19
- # URL to your background image hosted on your Hugging Face Space.
20
- BACKGROUND_IMAGE_URL = "1.jpg"
21
-
22
- # Global reader - initialize once
23
- reader = None
24
-
25
- def initialize_reader():
26
- """Initialize EasyOCR reader"""
27
- global reader
28
- if reader is None:
29
- print("Loading EasyOCR model...")
30
- reader = easyocr.Reader(['en'], gpu=False, verbose=False)
31
- print("EasyOCR model loaded successfully!")
32
- return reader
33
-
34
- # --- NEW FUNCTION TO DOWNLOAD FROM INSTAGRAM ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  def download_instagram_image(url):
36
  """
37
- Downloads the first image from an Instagram post URL using One-API.
38
- Returns a PIL Image object or an error message string.
39
  """
40
- if not url or not url.strip():
41
- return "Please enter an Instagram URL."
42
- if ONE_API_TOKEN == "YOUR_ONE_API_TOKEN_HERE":
43
- print("ERROR: ONE_API_TOKEN is not set.")
44
- return "ERROR: The Instagram downloader API key is not configured on the server."
45
-
46
- api_endpoint = f"https://one-api.ir/instagram/?token={ONE_API_TOKEN}&action=post&url={url.strip()}"
47
 
48
- print(f"DEBUG: Calling One-API for URL: {url.strip()}")
49
-
50
- try:
51
- response = requests.get(api_endpoint, timeout=30) # 30-second timeout
52
- response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
53
-
54
- data = response.json()
55
-
56
- if data.get("status") != 200 or not data.get("result"):
57
- error_message = data.get("message", "Unknown API error.")
58
- print(f"ERROR: One-API failed. Message: {error_message}")
59
- return f"API Error: {error_message}"
60
-
61
- # Find the first image URL in the media list
62
- media_list = data["result"].get("media", [])
63
- image_url = None
64
- for item in media_list:
65
- if item.get("type") == "image":
66
- image_url = item.get("url")
67
- break
68
-
69
- if not image_url:
70
- print("ERROR: No image found in the Instagram post media.")
71
- return "No image found in this Instagram post. It might be a video-only post."
72
-
73
- print(f"DEBUG: Found image URL: {image_url[:50]}...")
74
-
75
- # Download the image content
76
- image_response = requests.get(image_url, timeout=30)
77
- image_response.raise_for_status()
78
-
79
- # Open the image from the downloaded bytes and return a PIL Image
80
- image = Image.open(io.BytesIO(image_response.content))
81
- print("DEBUG: Instagram image downloaded and converted to PIL Image successfully.")
82
- return image
83
-
84
- except requests.exceptions.RequestException as e:
85
- print(f"ERROR: Network error while contacting One-API or downloading image: {e}")
86
- return f"Network Error: Could not retrieve data. {e}"
87
- except Exception as e:
88
- print(f"ERROR: An unexpected error occurred in download_instagram_image: {e}")
89
- return f"An unexpected error occurred: {str(e)}"
90
 
91
- def extract_text_from_quote(image):
92
- """Extract text from quote image using EasyOCR"""
93
- if image is None:
94
- return "Please upload an image first.", "Words: 0"
95
 
96
- try:
97
- reader = initialize_reader()
98
- img_array = np.array(image)
99
- results = reader.readtext(img_array, paragraph=True)
100
-
101
- if results:
102
- text_parts = [result[1].strip() for result in results if len(result) >= 2 and result[1].strip()]
103
- if text_parts:
104
- extracted_text = ' '.join(text_parts)
105
- word_count = f"Words: {len(extracted_text.split())}"
106
- return extracted_text, word_count
107
-
108
- return "No text detected in the image.", "Words: 0"
109
-
110
- except Exception as e:
111
- return f"Error processing image: {str(e)}", "Words: 0"
112
-
113
- def translate_extracted(text, lang):
114
- """Translate the extracted English text using Gemini API"""
115
- if not text or "No text" in text or "Error" in text or "Please upload" in text:
116
- return "No valid text to translate."
117
 
118
  try:
119
- print(f"DEBUG: API Key loaded (first 5 chars: {GEMINI_API_KEY[:5]}...). Starting translation to {lang}")
120
- genai.configure(api_key=GEMINI_API_KEY)
121
 
122
- for attempt in range(3):
123
- try:
124
- model = genai.GenerativeModel('gemini-1.5-flash')
125
- prompt = f"""
126
- You are a cool, chill translator with a fun and warm personality, inspired by Persian Twitter style.
127
- Your translations should be natural, slangy, and relatable. Use colloquial words and contractions.
128
- No emojis, keep it RTL-friendly. Be concise but preserve the emotional depth.
129
- Maintain correct grammar, even with slang. Avoid literal translations.
130
- Translate the following English quote into this style.
131
-
132
- English Quote: "{text}"
133
-
134
- Format your output for an image overlay: Break the Persian text into short, visually appealing lines.
135
- Output ONLY the translated Persian text.
136
- """
137
- response = model.generate_content(prompt)
138
- translated = response.text.strip()
139
- print(f"DEBUG: Translation successful on attempt {attempt+1}: {translated[:50]}...")
140
- return translated
141
- except Exception as inner_e:
142
- print(f"DEBUG: Attempt {attempt+1} failed: {str(inner_e)}. Retrying in 2s...")
143
- time.sleep(2)
144
-
145
- raise Exception("All translation retries failed.")
146
-
147
- except Exception as e:
148
- error_msg = f"Error translating: {str(e)}. Check network access to Google API."
149
- print(f"DEBUG: Translation failed: {str(e)}")
150
- return error_msg
151
-
152
- def overlay_text_on_image(translated_text):
153
- """
154
- Sends translated text to the 'textoverimage1' Space and gets the resulting image.
155
- """
156
- if not translated_text or "Error" in translated_text or "No valid text" in translated_text:
157
- print("DEBUG: Skipping image overlay due to invalid translated text.")
158
- return None
159
 
160
- try:
161
- print("DEBUG: Initializing client for 'kavehtaheri/textoverimage1'")
162
- client = Client("kavehtaheri/textoverimage1")
163
-
164
- print(f"DEBUG: Sending data to API endpoint '/overlay_text_on_image'")
165
- print(f"DEBUG: Persian Text: {translated_text}")
166
- print(f"DEBUG: Image URL: {BACKGROUND_IMAGE_URL}")
167
-
168
- result_image_path = client.predict(
169
- persian_text=translated_text,
170
- url="",
171
- upload=handle_file(BACKGROUND_IMAGE_URL),
172
- username="",
173
- text_color="Black",
174
- api_name="/overlay_text_on_image"
175
- )
176
-
177
- print(f"DEBUG: Received image path from API: {result_image_path}")
178
- return result_image_path
179
-
180
- except Exception as e:
181
- print(f"ERROR: Could not get image from 'textoverimage1' space. Error: {e}")
182
- return None
183
-
184
- def clear_all():
185
- """Clear all inputs and outputs"""
186
- return None, "", "Your extracted quote will appear here...", "Words: 0", "Translation will appear here...", None, True
187
-
188
- # --- Main Processing Functions ---
189
- def process_everything(image, lang):
190
- """The core pipeline: OCR -> Translate -> Overlay. Accepts a PIL image."""
191
- # Check if the input is valid before starting
192
- if image is None:
193
- return "Please provide an image first.", "Words: 0", "Translation failed: No image provided.", None
194
-
195
- text, wc = extract_text_from_quote(image)
196
- if "No text" in text or "Error" in text:
197
- return text, wc, "Translation failed: No text extracted.", None
198
-
199
- translated = translate_extracted(text, lang)
200
- final_image = overlay_text_on_image(translated)
201
-
202
- return text, wc, translated, final_image
203
-
204
- def process_from_url(url, lang):
205
- """Handler for the Instagram URL workflow."""
206
- gr.Info("Downloading image from Instagram...")
207
- download_result = download_instagram_image(url)
208
-
209
- # Check if download_instagram_image returned an error string
210
- if isinstance(download_result, str):
211
- # The download failed, return the error message to the text output
212
- gr.Error(f"Download Failed: {download_result}")
213
- return download_result, "Words: 0", "Process failed.", None, None
214
-
215
- # If successful, it returned a PIL image, so we can process it.
216
- gr.Info("Image downloaded! Starting OCR and Translation...")
217
- text, wc, translated, final_image = process_everything(download_result, lang)
218
-
219
- # We also return the downloaded image to populate the input image box
220
- return text, wc, translated, final_image, download_result
221
-
222
- # --- Gradio Interface ---
223
- with gr.Blocks(title="Quote OCR & Overlay", theme=gr.themes.Soft()) as demo:
224
-
225
- gr.Markdown("# 📝 Quote Text Extractor & Image Generator")
226
- gr.Markdown("Extract text from an image, translate it, and overlay it onto a new background. Choose your input method below.")
227
-
228
- with gr.Row():
229
- # --- INPUT COLUMN ---
230
- with gr.Column(scale=1):
231
- with gr.Tabs():
232
- with gr.Tab("1. Upload Image"):
233
- image_input = gr.Image(label="Upload Quote Image", type="pil", sources=["upload", "clipboard"])
234
- auto_process_cb = gr.Checkbox(label="Auto-Process After Upload", value=True)
235
-
236
- with gr.Tab("1. Download from Instagram URL"):
237
- insta_url_input = gr.Textbox(label="Instagram Post URL", placeholder="Paste a link like https://www.instagram.com/p/C0123ABCD.../")
238
- insta_process_btn = gr.Button("Download & Process", variant="primary")
239
 
240
- target_lang = gr.Dropdown(
241
- label="Target Language",
242
- choices=["persian(farsi)"],
243
- value="persian(farsi)",
244
- interactive=False
245
- )
246
 
247
- with gr.Row():
248
- clear_btn = gr.Button("Clear All", variant="secondary")
249
- extract_btn = gr.Button("Process Uploaded Image", variant="primary")
 
 
 
 
 
 
250
 
251
- # --- OUTPUTS COLUMN ---
252
- with gr.Column(scale=2):
253
- text_output = gr.Textbox(label="2. Extracted English Text", placeholder="Extracted text appears here...", lines=4, show_copy_button=True)
254
- word_count = gr.Textbox(label="Word Count", interactive=False, max_lines=1)
255
- translated_output = gr.Textbox(label="3. Translated Persian Text", placeholder="Persian translation appears here...", lines=4, show_copy_button=True)
256
- gr.Markdown("---")
257
- final_image_output = gr.Image(label="4. Final Image with Text Overlay", type="filepath")
258
-
259
- # --- Event Handlers ---
260
-
261
- # 1. For the "Process Uploaded Image" button
262
- extract_btn.click(
263
- fn=process_everything,
264
- inputs=[image_input, target_lang],
265
- outputs=[text_output, word_count, translated_output, final_image_output]
266
- )
267
-
268
- # 2. For auto-processing when an image is uploaded
269
- def auto_process_wrapper(image, lang, is_enabled):
270
- if is_enabled:
271
- return process_everything(image, lang)
272
- # If auto-process is off, just do OCR and stop
273
- text, wc = extract_text_from_quote(image)
274
- return text, wc, "Translation will appear here...", None
275
-
276
- image_input.change(
277
- fn=auto_process_wrapper,
278
- inputs=[image_input, target_lang, auto_process_cb],
279
- outputs=[text_output, word_count, translated_output, final_image_output]
280
- )
281
-
282
- # 3. For the "Download & Process" button in the Instagram tab
283
- insta_process_btn.click(
284
- fn=process_from_url,
285
- inputs=[insta_url_input, target_lang],
286
- # The outputs now include the image_input to show the downloaded image
287
- outputs=[text_output, word_count, translated_output, final_image_output, image_input]
288
- )
289
-
290
- # 4. For the Clear button
291
- clear_btn.click(
292
- fn=clear_all,
293
- outputs=[image_input, insta_url_input, text_output, word_count, translated_output, final_image_output, auto_process_cb]
294
- )
295
-
296
- gr.Markdown("### 💡 How It Works:\n1. **Upload an image** OR **paste an Instagram URL**.\n2. The app automatically extracts the English text using OCR.\n3. The text is translated to a casual, modern Persian.\n4. The Persian text is sent to a second app which overlays it on a background image.\n5. The final image is displayed.")
297
-
298
- if __name__ == "__main__":
299
- # Add these to your requirements.txt file:
300
- # gradio
301
- # easyocr
302
- # pillow
303
- # numpy
304
- # google-generativeai
305
- # gradio_client
306
- # requests
307
- demo.launch(debug=True)
 
 
 
 
1
+ import os
2
+ import requests
3
+ from flask import Flask, request, jsonify, render_template_string, send_from_directory
4
+ from urllib.parse import urlparse, parse_qs
5
+ import re # Import the regular expressions module
6
+
7
+ app = Flask(__name__)
8
+
9
+ # Load API token from environment variable
10
+ API_TOKEN = os.environ.get('ONE_API_TOKEN')
11
+ if not API_TOKEN:
12
+ raise ValueError("No ONE_API_TOKEN set for Flask application")
13
+
14
+ # --- HTML Templates ---
15
+
16
+ # Main page template
17
+ INDEX_TEMPLATE = """
18
+ <!DOCTYPE html>
19
+ <html lang="en">
20
+ <head>
21
+ <meta charset="UTF-8">
22
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
23
+ <title>Multi-Downloader Service</title>
24
+ <style>
25
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; background-color: #f4f4f9; color: #333; margin: 0; padding: 20px; }
26
+ .container { max-width: 800px; margin: 40px auto; background: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
27
+ h1 { color: #5a4bcf; text-align: center; }
28
+ .form-group { margin-bottom: 20px; }
29
+ label { display: block; font-weight: bold; margin-bottom: 8px; }
30
+ input[type="text"], input[type="url"] { width: 100%; padding: 12px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
31
+ button { display: block; width: 100%; background-color: #5a4bcf; color: white; padding: 14px 20px; margin-top: 10px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: bold; }
32
+ button:hover { background-color: #4a3abc; }
33
+ #response { margin-top: 25px; padding: 15px; border-radius: 4px; background-color: #e9ecef; border: 1px solid #ced4da; white-space: pre-wrap; word-wrap: break-word; }
34
+ .loader { display: none; margin: 20px auto; border: 5px solid #f3f3f3; border-top: 5px solid #5a4bcf; border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite; }
35
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
36
+ </style>
37
+ </head>
38
+ <body>
39
+ <div class="container">
40
+ <h1>Universal Downloader</h1>
41
+ <form id="download-form">
42
+ <div class="form-group">
43
+ <label for="url">Enter URL:</label>
44
+ <input type="url" id="url" name="url" placeholder="Enter Instagram, YouTube, etc. URL" required>
45
+ </div>
46
+ <button type="submit">Download</button>
47
+ </form>
48
+ <div class="loader" id="loader"></div>
49
+ <div id="response"></div>
50
+ </div>
51
+
52
+ <script>
53
+ document.getElementById('download-form').addEventListener('submit', function(event) {
54
+ event.preventDefault();
55
+ const url = document.getElementById('url').value;
56
+ const responseDiv = document.getElementById('response');
57
+ const loader = document.getElementById('loader');
58
+
59
+ responseDiv.textContent = '';
60
+ loader.style.display = 'block';
61
+
62
+ fetch('/download', {
63
+ method: 'POST',
64
+ headers: {
65
+ 'Content-Type': 'application/json'
66
+ },
67
+ body: JSON.stringify({ url: url })
68
+ })
69
+ .then(response => response.json())
70
+ .then(data => {
71
+ loader.style.display = 'none';
72
+ let content = '<h3>Result:</h3>';
73
+ if (data.error) {
74
+ content += `<p style="color: red;">Error: ${data.error}</p>`;
75
+ } else {
76
+ content += `<p><strong>Status:</strong> ${data.status}</p>`;
77
+ if (data.filename) {
78
+ content += `<p><strong>File:</strong> <a href="/uploads/${data.filename}" target="_blank">${data.filename}</a></p>`;
79
+ }
80
+ if (data.details) {
81
+ content += '<pre>' + JSON.stringify(data.details, null, 2) + '</pre>';
82
+ }
83
+ }
84
+ responseDiv.innerHTML = content;
85
+ })
86
+ .catch(error => {
87
+ loader.style.display = 'none';
88
+ responseDiv.innerHTML = `<p style="color: red;"><strong>An unexpected error occurred:</strong> ${error}</p>`;
89
+ });
90
+ });
91
+ </script>
92
+ </body>
93
+ </html>
94
+ """
95
+
96
+ # --- Helper Functions ---
97
+
98
+ def get_youtube_video_id(url):
99
+ """Extracts the YouTube video ID from a URL."""
100
+ parsed_url = urlparse(url)
101
+ if parsed_url.hostname == 'youtu.be':
102
+ return parsed_url.path[1:]
103
+ if parsed_url.hostname in ('www.youtube.com', 'youtube.com'):
104
+ if parsed_url.path == '/watch':
105
+ query_params = parse_qs(parsed_url.query)
106
+ return query_params.get('v', [None])[0]
107
+ if parsed_url.path.startswith('/embed/'):
108
+ return parsed_url.path.split('/embed/')[1]
109
+ if parsed_url.path.startswith('/v/'):
110
+ return parsed_url.path.split('/v/')[1]
111
+ return None
112
+
113
+ def download_file(url, local_filename):
114
+ """Downloads a file from a URL to a local path."""
115
+ with requests.get(url, stream=True) as r:
116
+ r.raise_for_status()
117
+ with open(local_filename, 'wb') as f:
118
+ for chunk in r.iter_content(chunk_size=8192):
119
+ f.write(chunk)
120
+ return local_filename
121
+
122
+ # --- API Interaction Functions ---
123
+
124
+ def download_youtube_video(video_id):
125
+ """Fetches YouTube video download link from one-api.ir."""
126
+ api_url = f"https://one-api.ir/youtube/?token={API_TOKEN}&action=download&id={video_id}"
127
+ response = requests.get(api_url)
128
+ if response.status_code != 200:
129
+ return {"error": f"API request failed with status {response.status_code}", "details": response.text}
130
+
131
+ data = response.json()
132
+ if data.get("status") == 200 and data.get("result"):
133
+ # Assuming the best quality is the first one
134
+ download_url = data["result"]["medias"][0]["url"]
135
+ filename = f"{data['result']['title']}.mp4"
136
+ # Ensure the 'uploads' directory exists
137
+ os.makedirs('uploads', exist_ok=True)
138
+ local_filepath = os.path.join('uploads', filename)
139
+ download_file(download_url, local_filepath)
140
+ return {"status": "Success", "filename": filename, "details": data}
141
+ else:
142
+ return {"error": "Failed to get download link from API", "details": data}
143
+
144
+ # ========================================================================
145
+ # MODIFIED FUNCTION
146
+ # ========================================================================
147
  def download_instagram_image(url):
148
  """
149
+ Fetches Instagram post details from one-api.ir using the shortcode,
150
+ then downloads the first image.
151
  """
152
+ # 1. Extract the shortcode from the URL using regex
153
+ # e.g., from "https://www.instagram.com/p/DMaqqN_RuqQ/" -> "DMaqqN_RuqQ"
154
+ match = re.search(r"/p/([^/]+)", url)
155
+ if not match:
156
+ return {"error": "Invalid Instagram post URL. Could not find shortcode."}
 
 
157
 
158
+ shortcode = match.group(1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
+ # 2. Define the new, correct API endpoint and parameters
161
+ api_url = "https://api.one-api.ir/instagram/v1/post/"
162
+ params = {"shortcode": shortcode}
 
163
 
164
+ # 3. Place the token in the headers, not the URL
165
+ headers = {"one-api-token": API_TOKEN}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
  try:
168
+ # 4. Make the API request
169
+ response = requests.get(api_url, params=params, headers=headers)
170
 
171
+ # Check for non-200 status codes (like 403, 404, 500)
172
+ response.raise_for_status()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
+ data = response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
+ # 5. Process the response and download the image
177
+ if data.get("status") == 200 and data.get("result"):
178
+ post_result = data["result"]
 
 
 
179
 
180
+ # Find the first media item that is an image
181
+ image_url = None
182
+ if post_result.get("media_type") == 1: # 1 usually means Image
183
+ image_url = post_result["media_url"]
184
+ elif post_result.get("media_type") == 8: # 8 usually means Carousel/Album
185
+ for item in post_result.get("carousel_media", []):
186
+ if item.get("media_type") == 1:
187
+ image_url = item["media_url"]
188
+ break
189
 
190
+ if not image_url:
191
+ return {"error": "No downloadable image found in the post.", "details": data}
192
+
193
+ # Create a filename and download
194
+ filename = f"instagram_{shortcode}.jpg"
195
+ os.makedirs('uploads', exist_ok=True)
196
+ local_filepath = os.path.join('uploads', filename)
197
+ download_file(image_url, local_filepath)
198
+
199
+ return {"status": "Success", "filename": filename, "details": data}
200
+ else:
201
+ # Handle API-level errors (e.g., status: 404 - not found)
202
+ return {"error": data.get("message", "Failed to get data from API"), "details": data}
203
+
204
+ except requests.exceptions.HTTPError as http_err:
205
+ # This will catch the 403 Forbidden error and others
206
+ return {"error": f"HTTP Error: {http_err}", "details": http_err.response.text}
207
+ except requests.exceptions.RequestException as req_err:
208
+ # Handle other request errors like network issues
209
+ return {"error": f"Request Exception: {req_err}"}
210
+ # ========================================================================
211
+ # END OF MODIFIED FUNCTION
212
+ # ========================================================================
213
+
214
+ # --- Flask Routes ---
215
+
216
+ @app.route('/')
217
+ def index():
218
+ """Serves the main HTML page."""
219
+ return render_template_string(INDEX_TEMPLATE)
220
+
221
+ @app.route('/download', methods=['POST'])
222
+ def download():
223
+ """Handles the download request by dispatching to the correct function."""
224
+ data = request.get_json()
225
+ url = data.get('url')
226
+
227
+ if not url:
228
+ return jsonify({"error": "URL is required"}), 400
229
+
230
+ hostname = urlparse(url).hostname
231
+ if 'youtube.com' in hostname or 'youtu.be' in hostname:
232
+ video_id = get_youtube_video_id(url)
233
+ if not video_id:
234
+ return jsonify({"error": "Invalid YouTube URL"}), 400
235
+ result = download_youtube_video(video_id)
236
+ elif 'instagram.com' in hostname:
237
+ result = download_instagram_image(url)
238
+ else:
239
+ return jsonify({"error": "Unsupported URL. Please use a YouTube or Instagram link."}), 400
240
+
241
+ return jsonify(result)
242
+
243
+ @app.route('/uploads/<filename>')
244
+ def uploaded_file(filename):
245
+ """Serves downloaded files from the 'uploads' directory."""
246
+ return send_from_directory('uploads', filename)
247
+
248
+ if __name__ == '__main__':
249
+ app.run(host='0.0.0.0', port=5000)