ismdrobiul489 commited on
Commit
cce7cd9
·
1 Parent(s): 64aa81b

Add Pixabay video API: dual search (Pexels + Pixabay), optional both APIs, 9:16 portrait filtering

Browse files
config.py CHANGED
@@ -57,6 +57,7 @@ class NCAkitConfig(BaseConfig):
57
  # Video Creator Module Config
58
  # ===================
59
  pexels_api_key: Optional[str] = None
 
60
  hf_tts: Optional[str] = None
61
  whisper_model: str = "tiny.en"
62
  whisper_verbose: bool = False
 
57
  # Video Creator Module Config
58
  # ===================
59
  pexels_api_key: Optional[str] = None
60
+ pixabay_api_key: Optional[str] = None # Optional: Pixabay API for additional video source
61
  hf_tts: Optional[str] = None
62
  whisper_model: str = "tiny.en"
63
  whisper_verbose: bool = False
modules/video_creator/__init__.py CHANGED
@@ -22,14 +22,15 @@ def register(app: FastAPI, config):
22
  from .services.libraries.tts_client import TTSClient
23
  from .services.libraries.whisper_client import WhisperClient
24
  from .services.libraries.pexels_client import PexelsClient
 
25
  from .services.music_manager import MusicManager
26
  from .services.short_creator import ShortCreator
27
 
28
  logger.info("Registering video_creator module...")
29
 
30
  # Validate environment variables
31
- if not config.pexels_api_key:
32
- logger.warning("PEXELS_API_KEY is missing! Video generation will fail.")
33
 
34
  if not config.hf_tts:
35
  logger.warning("HF_TTS is missing! TTS will fail.")
@@ -45,9 +46,21 @@ def register(app: FastAPI, config):
45
  model_dir=config.whisper_model_dir
46
  )
47
 
48
- # Initialize Pexels client
49
- logger.info("Initializing Pexels client...")
50
- pexels_client = PexelsClient(config.pexels_api_key)
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  # Initialize music manager
53
  logger.info("Initializing music manager...")
@@ -59,13 +72,14 @@ def register(app: FastAPI, config):
59
  logger.warning("Creating empty music directory")
60
  config.music_dir_path.mkdir(parents=True, exist_ok=True)
61
 
62
- # Initialize short creator
63
  logger.info("Initializing short creator...")
64
  short_creator = ShortCreator(
65
  config=config,
66
  tts_client=tts_client,
67
  whisper_client=whisper_client,
68
  pexels_client=pexels_client,
 
69
  music_manager=music_manager
70
  )
71
 
@@ -79,3 +93,4 @@ def register(app: FastAPI, config):
79
  app.include_router(router, prefix=MODULE_PREFIX, tags=["Video Creator"])
80
 
81
  logger.info("video_creator module registered successfully!")
 
 
22
  from .services.libraries.tts_client import TTSClient
23
  from .services.libraries.whisper_client import WhisperClient
24
  from .services.libraries.pexels_client import PexelsClient
25
+ from .services.libraries.pixabay_client import PixabayClient
26
  from .services.music_manager import MusicManager
27
  from .services.short_creator import ShortCreator
28
 
29
  logger.info("Registering video_creator module...")
30
 
31
  # Validate environment variables
32
+ if not config.pexels_api_key and not config.pixabay_api_key:
33
+ logger.warning("Neither PEXELS_API_KEY nor PIXABAY_API_KEY is set! Video generation may fail.")
34
 
35
  if not config.hf_tts:
36
  logger.warning("HF_TTS is missing! TTS will fail.")
 
46
  model_dir=config.whisper_model_dir
47
  )
48
 
49
+ # Initialize Pexels client (optional)
50
+ pexels_client = None
51
+ if config.pexels_api_key:
52
+ logger.info("Initializing Pexels client...")
53
+ pexels_client = PexelsClient(config.pexels_api_key)
54
+ else:
55
+ logger.info("Pexels API key not set, skipping Pexels client")
56
+
57
+ # Initialize Pixabay client (optional)
58
+ pixabay_client = None
59
+ if config.pixabay_api_key:
60
+ logger.info("Initializing Pixabay client...")
61
+ pixabay_client = PixabayClient(config.pixabay_api_key)
62
+ else:
63
+ logger.info("Pixabay API key not set, skipping Pixabay client")
64
 
65
  # Initialize music manager
66
  logger.info("Initializing music manager...")
 
72
  logger.warning("Creating empty music directory")
73
  config.music_dir_path.mkdir(parents=True, exist_ok=True)
74
 
75
+ # Initialize short creator with both video clients
76
  logger.info("Initializing short creator...")
77
  short_creator = ShortCreator(
78
  config=config,
79
  tts_client=tts_client,
80
  whisper_client=whisper_client,
81
  pexels_client=pexels_client,
82
+ pixabay_client=pixabay_client,
83
  music_manager=music_manager
84
  )
85
 
 
93
  app.include_router(router, prefix=MODULE_PREFIX, tags=["Video Creator"])
94
 
95
  logger.info("video_creator module registered successfully!")
96
+
modules/video_creator/services/libraries/pixabay_client.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pixabay Video Client
3
+ Fetches background videos from Pixabay API
4
+ """
5
+ import logging
6
+ import random
7
+ import requests
8
+ from typing import List, Optional
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class PixabayClient:
14
+ """Client for Pixabay API to fetch background videos"""
15
+
16
+ def __init__(self, api_key: str):
17
+ """
18
+ Initialize Pixabay client
19
+
20
+ Args:
21
+ api_key: Pixabay API key
22
+ """
23
+ self.api_key = api_key
24
+ self.base_url = "https://pixabay.com/api/videos/"
25
+ self.joker_terms = ["nature", "abstract", "space", "ocean"]
26
+
27
+ def find_video(
28
+ self,
29
+ search_terms: List[str],
30
+ duration: float,
31
+ exclude_ids: Optional[List[int]] = None,
32
+ orientation: str = "portrait"
33
+ ) -> Optional[dict]:
34
+ """
35
+ Find a suitable video from Pixabay
36
+
37
+ Args:
38
+ search_terms: Keywords to search for
39
+ duration: Required video duration in seconds
40
+ exclude_ids: List of video IDs to exclude
41
+ orientation: 'portrait' or 'landscape'
42
+
43
+ Returns:
44
+ Dict with 'id' and 'url' of the selected video, or None
45
+ """
46
+ exclude_ids = exclude_ids or []
47
+
48
+ # Try user-provided search terms first
49
+ for term in search_terms:
50
+ video = self._search_and_select(term, duration, exclude_ids, orientation)
51
+ if video:
52
+ return video
53
+
54
+ # Fall back to joker terms
55
+ logger.info(f"Pixabay: No videos found for {search_terms}, using joker terms")
56
+ for term in self.joker_terms:
57
+ video = self._search_and_select(term, duration, exclude_ids, orientation)
58
+ if video:
59
+ return video
60
+
61
+ return None
62
+
63
+ def _search_and_select(
64
+ self,
65
+ query: str,
66
+ min_duration: float,
67
+ exclude_ids: List[int],
68
+ orientation: str
69
+ ) -> Optional[dict]:
70
+ """Search for videos and select a suitable one"""
71
+ try:
72
+ logger.debug(f"Searching Pixabay for: {query} ({orientation})")
73
+
74
+ response = requests.get(
75
+ self.base_url,
76
+ params={
77
+ "key": self.api_key,
78
+ "q": query,
79
+ "per_page": 30,
80
+ "safesearch": "true",
81
+ "video_type": "film"
82
+ },
83
+ timeout=15
84
+ )
85
+
86
+ if response.status_code != 200:
87
+ logger.warning(f"Pixabay API error: {response.status_code}")
88
+ return None
89
+
90
+ data = response.json()
91
+ videos = data.get("hits", [])
92
+
93
+ if not videos:
94
+ logger.debug(f"Pixabay: No videos found for query: {query}")
95
+ return None
96
+
97
+ # Filter suitable videos
98
+ suitable_videos = []
99
+ for video in videos:
100
+ if video["id"] in exclude_ids:
101
+ continue
102
+
103
+ video_files = video.get("videos", {})
104
+ if not video_files:
105
+ continue
106
+
107
+ # Get large or medium quality
108
+ large = video_files.get("large", {})
109
+ medium = video_files.get("medium", {})
110
+
111
+ # Prefer large, fallback to medium
112
+ selected_file = large if large.get("url") else medium
113
+
114
+ if not selected_file.get("url"):
115
+ continue
116
+
117
+ width = selected_file.get("width", 0)
118
+ height = selected_file.get("height", 0)
119
+
120
+ # Filter by orientation
121
+ if orientation == "portrait":
122
+ # Portrait: height > width (9:16)
123
+ if height <= width:
124
+ continue
125
+ else:
126
+ # Landscape: width > height (16:9)
127
+ if width <= height:
128
+ continue
129
+
130
+ video_duration = video.get("duration", 0)
131
+
132
+ suitable_videos.append({
133
+ "id": video["id"],
134
+ "url": selected_file["url"],
135
+ "duration": video_duration,
136
+ "width": width,
137
+ "height": height
138
+ })
139
+
140
+ if not suitable_videos:
141
+ logger.debug(f"Pixabay: No {orientation} videos found for: {query}")
142
+ return None
143
+
144
+ # Filter by duration if possible
145
+ duration_threshold = min(min_duration * 0.5, 15)
146
+ long_enough_videos = [v for v in suitable_videos if v["duration"] >= duration_threshold]
147
+
148
+ if long_enough_videos:
149
+ # Select FIRST (most relevant) video
150
+ selected = long_enough_videos[0]
151
+ logger.info(f"Pixabay: Selected video ID {selected['id']} (duration: {selected['duration']}s) for '{query}'")
152
+ return selected
153
+
154
+ # Fallback to first suitable video
155
+ selected = suitable_videos[0]
156
+ logger.info(f"Pixabay: Selected video ID {selected['id']} ({selected['duration']}s) for '{query}' (fallback)")
157
+ return selected
158
+
159
+ except Exception as e:
160
+ logger.error(f"Pixabay search error: {e}")
161
+ return None
modules/video_creator/services/short_creator.py CHANGED
@@ -28,13 +28,15 @@ class ShortCreator:
28
  config: Any, # NCAkitConfig from parent
29
  tts_client: TTSClient,
30
  whisper_client: WhisperClient,
31
- pexels_client: PexelsClient,
32
- music_manager: MusicManager
 
33
  ):
34
  self.config = config
35
  self.tts = tts_client
36
  self.whisper = whisper_client
37
  self.pexels = pexels_client
 
38
  self.music_manager = music_manager
39
  self.queue: List[Dict] = []
40
  self.processing = False
@@ -195,19 +197,46 @@ class ShortCreator:
195
  # Ensure keywords is a list for find_video
196
  search_keywords = keywords if isinstance(keywords, list) else [keywords]
197
 
198
- pexels_video = self.pexels.find_video(
199
- search_keywords, # Pass full list, not just first keyword
200
- search_duration,
201
- exclude_video_ids,
202
- orientation
203
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
  video_path = self.config.temp_dir_path / f"{temp_vid_id}.mp4"
206
  temp_files.append(video_path)
207
 
208
  # Download video
209
  logger.debug(f"Downloading video for '{keyword}' (Target: {audio_duration:.2f}s)")
210
- response = requests.get(pexels_video["url"], stream=True, timeout=30)
211
  response.raise_for_status()
212
 
213
  with open(video_path, 'wb') as f:
 
28
  config: Any, # NCAkitConfig from parent
29
  tts_client: TTSClient,
30
  whisper_client: WhisperClient,
31
+ pexels_client: PexelsClient = None,
32
+ pixabay_client = None, # Optional PixabayClient
33
+ music_manager: MusicManager = None
34
  ):
35
  self.config = config
36
  self.tts = tts_client
37
  self.whisper = whisper_client
38
  self.pexels = pexels_client
39
+ self.pixabay = pixabay_client
40
  self.music_manager = music_manager
41
  self.queue: List[Dict] = []
42
  self.processing = False
 
197
  # Ensure keywords is a list for find_video
198
  search_keywords = keywords if isinstance(keywords, list) else [keywords]
199
 
200
+ # Try Pexels first (if configured)
201
+ pexels_video = None
202
+ pixabay_video = None
203
+ selected_video = None
204
+
205
+ if self.pexels:
206
+ logger.debug(f"Trying Pexels for: {search_keywords}")
207
+ pexels_video = self.pexels.find_video(
208
+ search_keywords,
209
+ search_duration,
210
+ exclude_video_ids,
211
+ orientation
212
+ )
213
+ if pexels_video:
214
+ selected_video = pexels_video
215
+ logger.info(f"Found video on Pexels for '{keyword}'")
216
+
217
+ # Try Pixabay if Pexels didn't find anything (and Pixabay is configured)
218
+ if not selected_video and self.pixabay:
219
+ logger.debug(f"Trying Pixabay for: {search_keywords}")
220
+ pixabay_video = self.pixabay.find_video(
221
+ search_keywords,
222
+ search_duration,
223
+ exclude_video_ids,
224
+ orientation
225
+ )
226
+ if pixabay_video:
227
+ selected_video = pixabay_video
228
+ logger.info(f"Found video on Pixabay for '{keyword}'")
229
+
230
+ # If no video found on either platform
231
+ if not selected_video:
232
+ raise Exception(f"No video found for {search_keywords} on any platform")
233
 
234
  video_path = self.config.temp_dir_path / f"{temp_vid_id}.mp4"
235
  temp_files.append(video_path)
236
 
237
  # Download video
238
  logger.debug(f"Downloading video for '{keyword}' (Target: {audio_duration:.2f}s)")
239
+ response = requests.get(selected_video["url"], stream=True, timeout=30)
240
  response.raise_for_status()
241
 
242
  with open(video_path, 'wb') as f: