Alpha123B commited on
Commit
5d21a5c
·
verified ·
1 Parent(s): c5726a6

Add stock media service

Browse files
Files changed (1) hide show
  1. backend/services/stock_media.py +300 -0
backend/services/stock_media.py ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Stock media fetching service using Pexels and Unsplash APIs."""
2
+ import os
3
+ import httpx
4
+ import logging
5
+ import asyncio
6
+ from pathlib import Path
7
+ from typing import List, Optional
8
+ from dataclasses import dataclass
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY", "")
13
+ UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY", "")
14
+
15
+
16
+ @dataclass
17
+ class StockImage:
18
+ url: str
19
+ photographer: str
20
+ alt: str
21
+ width: int
22
+ height: int
23
+ local_path: str = ""
24
+
25
+
26
+ @dataclass
27
+ class StockVideo:
28
+ url: str
29
+ duration: int
30
+ width: int
31
+ height: int
32
+ local_path: str = ""
33
+
34
+
35
+ async def fetch_stock_images(
36
+ keywords: List[str],
37
+ output_dir: str,
38
+ per_keyword: int = 2,
39
+ orientation: str = "portrait"
40
+ ) -> List[StockImage]:
41
+ """Fetch stock images from Pexels API.
42
+
43
+ Args:
44
+ keywords: Search keywords
45
+ output_dir: Directory to save downloaded images
46
+ per_keyword: Number of images per keyword
47
+ orientation: Image orientation (portrait, landscape, square)
48
+
49
+ Returns:
50
+ List of StockImage objects with local paths
51
+ """
52
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
53
+ all_images = []
54
+
55
+ if PEXELS_API_KEY:
56
+ all_images = await _pexels_images(keywords, output_dir, per_keyword, orientation)
57
+ elif UNSPLASH_ACCESS_KEY:
58
+ all_images = await _unsplash_images(keywords, output_dir, per_keyword, orientation)
59
+ else:
60
+ logger.warning("No stock image API key configured, generating placeholder images")
61
+ all_images = _generate_placeholder_images(keywords, output_dir, per_keyword)
62
+
63
+ return all_images
64
+
65
+
66
+ async def _pexels_images(
67
+ keywords: List[str], output_dir: str, per_keyword: int, orientation: str
68
+ ) -> List[StockImage]:
69
+ """Fetch images from Pexels API."""
70
+ images = []
71
+
72
+ async with httpx.AsyncClient() as client:
73
+ for keyword in keywords:
74
+ try:
75
+ response = await client.get(
76
+ "https://api.pexels.com/v1/search",
77
+ params={
78
+ "query": keyword,
79
+ "per_page": per_keyword,
80
+ "orientation": orientation
81
+ },
82
+ headers={"Authorization": PEXELS_API_KEY},
83
+ timeout=15.0
84
+ )
85
+
86
+ if response.status_code != 200:
87
+ logger.warning(f"Pexels API error for '{keyword}': {response.status_code}")
88
+ continue
89
+
90
+ data = response.json()
91
+
92
+ for i, photo in enumerate(data.get("photos", [])):
93
+ # Download the image
94
+ img_url = photo["src"]["large2x"] # High quality
95
+ local_path = str(Path(output_dir) / f"{keyword.replace(' ', '_')}_{i}.jpg")
96
+
97
+ try:
98
+ img_response = await client.get(img_url, timeout=15.0)
99
+ with open(local_path, "wb") as f:
100
+ f.write(img_response.content)
101
+
102
+ images.append(StockImage(
103
+ url=img_url,
104
+ photographer=photo.get("photographer", "Unknown"),
105
+ alt=photo.get("alt", keyword),
106
+ width=photo["width"],
107
+ height=photo["height"],
108
+ local_path=local_path
109
+ ))
110
+ except Exception as e:
111
+ logger.warning(f"Failed to download image: {e}")
112
+
113
+ except Exception as e:
114
+ logger.error(f"Pexels search error for '{keyword}': {e}")
115
+
116
+ logger.info(f"Fetched {len(images)} images from Pexels")
117
+ return images
118
+
119
+
120
+ async def _unsplash_images(
121
+ keywords: List[str], output_dir: str, per_keyword: int, orientation: str
122
+ ) -> List[StockImage]:
123
+ """Fetch images from Unsplash API."""
124
+ images = []
125
+
126
+ async with httpx.AsyncClient() as client:
127
+ for keyword in keywords:
128
+ try:
129
+ response = await client.get(
130
+ "https://api.unsplash.com/search/photos",
131
+ params={
132
+ "query": keyword,
133
+ "per_page": per_keyword,
134
+ "orientation": orientation
135
+ },
136
+ headers={"Authorization": f"Client-ID {UNSPLASH_ACCESS_KEY}"},
137
+ timeout=15.0
138
+ )
139
+
140
+ if response.status_code != 200:
141
+ continue
142
+
143
+ data = response.json()
144
+
145
+ for i, photo in enumerate(data.get("results", [])):
146
+ img_url = photo["urls"]["regular"]
147
+ local_path = str(Path(output_dir) / f"{keyword.replace(' ', '_')}_{i}.jpg")
148
+
149
+ try:
150
+ img_response = await client.get(img_url, timeout=15.0)
151
+ with open(local_path, "wb") as f:
152
+ f.write(img_response.content)
153
+
154
+ images.append(StockImage(
155
+ url=img_url,
156
+ photographer=photo["user"]["name"],
157
+ alt=photo.get("alt_description", keyword),
158
+ width=photo["width"],
159
+ height=photo["height"],
160
+ local_path=local_path
161
+ ))
162
+ except Exception as e:
163
+ logger.warning(f"Failed to download Unsplash image: {e}")
164
+
165
+ except Exception as e:
166
+ logger.error(f"Unsplash search error for '{keyword}': {e}")
167
+
168
+ return images
169
+
170
+
171
+ async def fetch_stock_videos(
172
+ keywords: List[str],
173
+ output_dir: str,
174
+ per_keyword: int = 1
175
+ ) -> List[StockVideo]:
176
+ """Fetch stock video clips from Pexels Videos API.
177
+
178
+ Args:
179
+ keywords: Search keywords
180
+ output_dir: Directory to save videos
181
+ per_keyword: Number of videos per keyword
182
+
183
+ Returns:
184
+ List of StockVideo objects
185
+ """
186
+ if not PEXELS_API_KEY:
187
+ return []
188
+
189
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
190
+ videos = []
191
+
192
+ async with httpx.AsyncClient() as client:
193
+ for keyword in keywords:
194
+ try:
195
+ response = await client.get(
196
+ "https://api.pexels.com/videos/search",
197
+ params={
198
+ "query": keyword,
199
+ "per_page": per_keyword,
200
+ "orientation": "portrait",
201
+ "size": "medium"
202
+ },
203
+ headers={"Authorization": PEXELS_API_KEY},
204
+ timeout=15.0
205
+ )
206
+
207
+ if response.status_code != 200:
208
+ continue
209
+
210
+ data = response.json()
211
+
212
+ for i, video in enumerate(data.get("videos", [])):
213
+ # Get the best quality video file
214
+ video_files = video.get("video_files", [])
215
+ hd_file = next(
216
+ (f for f in video_files if f.get("quality") == "hd"),
217
+ video_files[0] if video_files else None
218
+ )
219
+
220
+ if not hd_file:
221
+ continue
222
+
223
+ video_url = hd_file["link"]
224
+ local_path = str(Path(output_dir) / f"broll_{keyword.replace(' ', '_')}_{i}.mp4")
225
+
226
+ try:
227
+ vid_response = await client.get(video_url, timeout=30.0)
228
+ with open(local_path, "wb") as f:
229
+ f.write(vid_response.content)
230
+
231
+ videos.append(StockVideo(
232
+ url=video_url,
233
+ duration=video.get("duration", 0),
234
+ width=hd_file.get("width", 1920),
235
+ height=hd_file.get("height", 1080),
236
+ local_path=local_path
237
+ ))
238
+ except Exception as e:
239
+ logger.warning(f"Failed to download video: {e}")
240
+
241
+ except Exception as e:
242
+ logger.error(f"Pexels video search error: {e}")
243
+
244
+ return videos
245
+
246
+
247
+ def _generate_placeholder_images(
248
+ keywords: List[str], output_dir: str, per_keyword: int
249
+ ) -> List[StockImage]:
250
+ """Generate placeholder gradient images when no API key is available."""
251
+ from PIL import Image, ImageDraw, ImageFont
252
+ import random
253
+
254
+ images = []
255
+ colors = [
256
+ ((30, 60, 120), (80, 150, 220)),
257
+ ((120, 30, 60), (220, 80, 150)),
258
+ ((30, 120, 60), (80, 220, 150)),
259
+ ((100, 50, 150), (200, 100, 250)),
260
+ ((150, 100, 30), (250, 200, 80)),
261
+ ]
262
+
263
+ for keyword in keywords:
264
+ for i in range(per_keyword):
265
+ local_path = str(Path(output_dir) / f"{keyword.replace(' ', '_')}_{i}.jpg")
266
+
267
+ # Create gradient image
268
+ img = Image.new("RGB", (1080, 1920))
269
+ draw = ImageDraw.Draw(img)
270
+
271
+ color_pair = random.choice(colors)
272
+ for y in range(1920):
273
+ ratio = y / 1920
274
+ r = int(color_pair[0][0] * (1 - ratio) + color_pair[1][0] * ratio)
275
+ g = int(color_pair[0][1] * (1 - ratio) + color_pair[1][1] * ratio)
276
+ b = int(color_pair[0][2] * (1 - ratio) + color_pair[1][2] * ratio)
277
+ draw.line([(0, y), (1080, y)], fill=(r, g, b))
278
+
279
+ # Add keyword text
280
+ try:
281
+ font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 48)
282
+ except:
283
+ font = ImageFont.load_default()
284
+
285
+ bbox = draw.textbbox((0, 0), keyword, font=font)
286
+ text_width = bbox[2] - bbox[0]
287
+ draw.text(((1080 - text_width) // 2, 900), keyword, fill="white", font=font)
288
+
289
+ img.save(local_path, quality=90)
290
+
291
+ images.append(StockImage(
292
+ url="",
293
+ photographer="Generated",
294
+ alt=keyword,
295
+ width=1080,
296
+ height=1920,
297
+ local_path=local_path
298
+ ))
299
+
300
+ return images