359 lines
13 KiB
Python
359 lines
13 KiB
Python
"""
|
|
Investigation Script 2: Keyword Translation and Pexels Media Fetcher
|
|
|
|
This script demonstrates:
|
|
1. Translating Japanese keywords to English using Gemini
|
|
2. Searching Pexels API for relevant B-Roll footage
|
|
3. Downloading videos to local assets folder
|
|
4. Caching translations to avoid repeated API calls
|
|
|
|
Target: Find stock footage for insurance/financial video production
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
from typing import List, Dict, Any, Optional
|
|
from dotenv import load_dotenv
|
|
from google import genai
|
|
import requests
|
|
from rich.console import Console
|
|
from rich.progress import Progress, SpinnerColumn, TextColumn, DownloadColumn, TransferSpeedColumn
|
|
from rich.panel import Panel
|
|
from rich.table import Table
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
# Initialize Rich console
|
|
console = Console()
|
|
|
|
# Configure APIs
|
|
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
|
PEXELS_API_KEY = os.getenv("PEXELS_API_KEY")
|
|
|
|
if not GEMINI_API_KEY or not PEXELS_API_KEY:
|
|
console.print("[red]❌ API keys not found in .env file[/red]")
|
|
console.print("Required: GEMINI_API_KEY, PEXELS_API_KEY")
|
|
exit(1)
|
|
|
|
# Initialize Gemini client
|
|
client = genai.Client(api_key=GEMINI_API_KEY)
|
|
|
|
|
|
class KeywordTranslator:
|
|
"""Translate Japanese keywords to English using Gemini"""
|
|
|
|
def __init__(self, cache_file: str = "assets/1_processed/translation_cache.json"):
|
|
self.model = 'gemini-3-pro-preview' # Latest model for translation
|
|
self.cache_file = Path(cache_file)
|
|
self.cache = self._load_cache()
|
|
console.print("[green]✓[/green] Initialized Gemini translator with cache")
|
|
|
|
def _load_cache(self) -> Dict[str, str]:
|
|
"""Load translation cache from file"""
|
|
if self.cache_file.exists():
|
|
with open(self.cache_file, 'r', encoding='utf-8') as f:
|
|
return json.load(f)
|
|
return {}
|
|
|
|
def _save_cache(self):
|
|
"""Save translation cache to file"""
|
|
self.cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(self.cache_file, 'w', encoding='utf-8') as f:
|
|
json.dump(self.cache, f, ensure_ascii=False, indent=2)
|
|
|
|
def translate(self, japanese_keywords: List[str]) -> Dict[str, str]:
|
|
"""
|
|
Translate Japanese keywords to English
|
|
|
|
Args:
|
|
japanese_keywords: List of Japanese keywords
|
|
|
|
Returns:
|
|
Dictionary mapping Japanese to English keywords
|
|
"""
|
|
translations = {}
|
|
keywords_to_translate = []
|
|
|
|
# Check cache first
|
|
for keyword in japanese_keywords:
|
|
if keyword in self.cache:
|
|
translations[keyword] = self.cache[keyword]
|
|
else:
|
|
keywords_to_translate.append(keyword)
|
|
|
|
if keywords_to_translate:
|
|
console.print(f"[cyan]Translating {len(keywords_to_translate)} new keywords...[/cyan]")
|
|
|
|
# Batch translation for efficiency
|
|
prompt = f"""
|
|
Translate the following Japanese keywords to English for searching stock footage/videos.
|
|
Provide natural, searchable English terms that would work well for video search.
|
|
|
|
Japanese keywords: {', '.join(keywords_to_translate)}
|
|
|
|
Return ONLY a JSON object mapping Japanese to English, like this:
|
|
{{
|
|
"日本語キーワード1": "english keyword 1",
|
|
"日本語キーワード2": "english keyword 2"
|
|
}}
|
|
"""
|
|
|
|
response = client.models.generate_content(
|
|
model=self.model,
|
|
contents=prompt
|
|
)
|
|
|
|
# Display token usage
|
|
if hasattr(response, 'usage_metadata'):
|
|
token_info = {
|
|
"input_tokens": response.usage_metadata.prompt_token_count,
|
|
"output_tokens": response.usage_metadata.candidates_token_count,
|
|
"total_tokens": response.usage_metadata.total_token_count
|
|
}
|
|
console.print(f"[cyan]📊 Translation Tokens:[/cyan] {token_info['total_tokens']:,} (in: {token_info['input_tokens']:,}, out: {token_info['output_tokens']:,})")
|
|
|
|
try:
|
|
# Parse response
|
|
response_text = response.text
|
|
if "```json" in response_text:
|
|
json_start = response_text.find("```json") + 7
|
|
json_end = response_text.find("```", json_start)
|
|
response_text = response_text[json_start:json_end].strip()
|
|
elif "```" in response_text:
|
|
json_start = response_text.find("```") + 3
|
|
json_end = response_text.find("```", json_start)
|
|
response_text = response_text[json_start:json_end].strip()
|
|
|
|
new_translations = json.loads(response_text)
|
|
|
|
# Update cache and results
|
|
self.cache.update(new_translations)
|
|
translations.update(new_translations)
|
|
self._save_cache()
|
|
|
|
console.print(f"[green]✓[/green] Translated {len(new_translations)} keywords")
|
|
|
|
except json.JSONDecodeError as e:
|
|
console.print(f"[yellow]⚠[/yellow] Translation parsing failed: {e}")
|
|
# Fallback: use romanized version
|
|
for keyword in keywords_to_translate:
|
|
translations[keyword] = keyword
|
|
|
|
return translations
|
|
|
|
|
|
class PexelsFetcher:
|
|
"""Fetch B-Roll videos from Pexels API"""
|
|
|
|
def __init__(self, api_key: str, output_dir: str = "assets/1_processed/broll"):
|
|
self.api_key = api_key
|
|
self.output_dir = Path(output_dir)
|
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
self.base_url = "https://api.pexels.com/videos/search"
|
|
console.print("[green]✓[/green] Initialized Pexels API client")
|
|
|
|
def search_videos(self, query: str, max_results: int = 3, min_duration: int = 5) -> List[Dict[str, Any]]:
|
|
"""
|
|
Search for videos on Pexels
|
|
|
|
Args:
|
|
query: Search query (in English)
|
|
max_results: Maximum number of videos to return
|
|
min_duration: Minimum video duration in seconds
|
|
|
|
Returns:
|
|
List of video metadata dictionaries
|
|
"""
|
|
headers = {
|
|
"Authorization": self.api_key
|
|
}
|
|
|
|
params = {
|
|
"query": query,
|
|
"per_page": max_results,
|
|
"orientation": "landscape", # Better for editing
|
|
"size": "medium" # Balance quality and file size
|
|
}
|
|
|
|
try:
|
|
response = requests.get(self.base_url, headers=headers, params=params)
|
|
response.raise_for_status()
|
|
|
|
data = response.json()
|
|
videos = []
|
|
|
|
for video in data.get("videos", []):
|
|
# Find best quality HD file
|
|
video_files = video.get("video_files", [])
|
|
hd_file = None
|
|
|
|
for vf in video_files:
|
|
if vf.get("quality") == "hd" and vf.get("width") == 1920:
|
|
hd_file = vf
|
|
break
|
|
|
|
# Fallback to first available file
|
|
if not hd_file and video_files:
|
|
hd_file = video_files[0]
|
|
|
|
if hd_file and video.get("duration", 0) >= min_duration:
|
|
videos.append({
|
|
"id": video["id"],
|
|
"url": video["url"],
|
|
"duration": video["duration"],
|
|
"width": video["width"],
|
|
"height": video["height"],
|
|
"download_url": hd_file["link"],
|
|
"file_size": hd_file.get("size", 0),
|
|
"photographer": video["user"]["name"]
|
|
})
|
|
|
|
return videos[:max_results]
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
console.print(f"[red]❌ Pexels API error:[/red] {e}")
|
|
return []
|
|
|
|
def download_video(self, video: Dict[str, Any], keyword: str) -> Optional[str]:
|
|
"""
|
|
Download a video file
|
|
|
|
Args:
|
|
video: Video metadata dictionary
|
|
keyword: Keyword used for search (for filename)
|
|
|
|
Returns:
|
|
Path to downloaded file, or None if failed
|
|
"""
|
|
# Create safe filename
|
|
safe_keyword = "".join(c for c in keyword if c.isalnum() or c in (' ', '-', '_')).strip()
|
|
safe_keyword = safe_keyword.replace(' ', '_')
|
|
filename = f"{safe_keyword}_{video['id']}.mp4"
|
|
filepath = self.output_dir / filename
|
|
|
|
# Skip if already downloaded
|
|
if filepath.exists():
|
|
console.print(f"[yellow]⊘[/yellow] Already exists: {filename}")
|
|
return str(filepath)
|
|
|
|
try:
|
|
with Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
DownloadColumn(),
|
|
TransferSpeedColumn(),
|
|
console=console
|
|
) as progress:
|
|
task = progress.add_task(f"Downloading {filename[:40]}...", total=video.get('file_size'))
|
|
|
|
response = requests.get(video['download_url'], stream=True)
|
|
response.raise_for_status()
|
|
|
|
with open(filepath, 'wb') as f:
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
f.write(chunk)
|
|
progress.update(task, advance=len(chunk))
|
|
|
|
console.print(f"[green]✓[/green] Downloaded: {filename}")
|
|
return str(filepath)
|
|
|
|
except Exception as e:
|
|
console.print(f"[red]❌ Download failed:[/red] {e}")
|
|
if filepath.exists():
|
|
filepath.unlink() # Remove partial file
|
|
return None
|
|
|
|
|
|
def main():
|
|
"""Main investigation workflow"""
|
|
console.print(Panel.fit(
|
|
"[bold cyan]Pexels Media Fetch Investigation Script[/bold cyan]\n"
|
|
"Testing keyword translation and B-Roll video download",
|
|
border_style="cyan"
|
|
))
|
|
|
|
# Initialize components
|
|
translator = KeywordTranslator()
|
|
fetcher = PexelsFetcher(PEXELS_API_KEY)
|
|
|
|
# Test keywords (typical for insurance/life planning videos)
|
|
test_keywords_ja = [
|
|
"家族",
|
|
"老後",
|
|
"貯蓄",
|
|
"住宅",
|
|
"教育",
|
|
"保険",
|
|
"将来",
|
|
"不安",
|
|
"安心",
|
|
"計画"
|
|
]
|
|
|
|
console.print("\n[bold]Test Keywords (Japanese):[/bold]")
|
|
console.print(", ".join(test_keywords_ja))
|
|
|
|
# Translate keywords
|
|
console.print("\n" + "="*60)
|
|
translations = translator.translate(test_keywords_ja)
|
|
|
|
# Display translations
|
|
table = Table(title="Translation Results", show_header=True, header_style="bold cyan")
|
|
table.add_column("Japanese", style="yellow")
|
|
table.add_column("English", style="green")
|
|
|
|
for ja, en in translations.items():
|
|
table.add_row(ja, en)
|
|
|
|
console.print(table)
|
|
|
|
# Search and download videos
|
|
console.print("\n" + "="*60)
|
|
console.print("[bold]Searching Pexels for B-Roll footage...[/bold]\n")
|
|
|
|
results = {}
|
|
|
|
for ja_keyword, en_keyword in list(translations.items())[:5]: # Limit to 5 for testing
|
|
console.print(f"\n[cyan]Searching:[/cyan] '{en_keyword}' (from '{ja_keyword}')")
|
|
|
|
videos = fetcher.search_videos(en_keyword, max_results=2)
|
|
|
|
if videos:
|
|
console.print(f" Found {len(videos)} video(s)")
|
|
|
|
downloaded = []
|
|
for video in videos:
|
|
filepath = fetcher.download_video(video, en_keyword)
|
|
if filepath:
|
|
downloaded.append({
|
|
"filepath": filepath,
|
|
"duration": video["duration"],
|
|
"photographer": video["photographer"]
|
|
})
|
|
|
|
results[ja_keyword] = {
|
|
"english": en_keyword,
|
|
"videos": downloaded
|
|
}
|
|
else:
|
|
console.print(f" [yellow]No videos found[/yellow]")
|
|
|
|
# Save results summary
|
|
output_file = Path("assets/1_processed/broll_fetch_results.json")
|
|
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
json.dump(results, f, ensure_ascii=False, indent=2)
|
|
|
|
console.print("\n" + "="*60)
|
|
console.print(f"[bold green]✓ Investigation Complete![/bold green]")
|
|
console.print(f"Downloaded videos saved to: {fetcher.output_dir}")
|
|
console.print(f"Results summary saved to: {output_file}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|