359 lines
19 KiB
PHP
359 lines
19 KiB
PHP
<?php
|
|
namespace AskTamMCP\Tools;
|
|
defined('ABSPATH') || exit;
|
|
|
|
/**
|
|
* AI Media Tools — search, generate and import images/videos into WordPress media library.
|
|
*
|
|
* Sources supported:
|
|
* - Pexels (free stock photos + videos, API key stored in wp_options)
|
|
* - GPT Image (OpenAI text-to-image, API key stored in wp_options)
|
|
* - Gemini Image (Google Gemini / Nano Banana text-to-image, API key stored in wp_options)
|
|
*
|
|
* All tools return: { attachment_id, url, source, width, height, alt }
|
|
* That attachment_id can be dropped straight into any Elementor image/background widget.
|
|
*
|
|
* Admin: WP Admin → AskTam MCP → Settings → "AI Media API Keys" section.
|
|
*/
|
|
class AiMediaTools {
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// OPTION KEYS
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
const OPT_PEXELS_KEY = 'asktam_mcp_pexels_key';
|
|
const OPT_OPENAI_KEY = 'asktam_mcp_openai_key';
|
|
const OPT_GEMINI_KEY = 'asktam_mcp_gemini_key';
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// PEXELS — search photos
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
public static function pexelsSearchPhotos(array $args): array {
|
|
$key = get_option(self::OPT_PEXELS_KEY, '');
|
|
if (empty($key)) throw new \RuntimeException('Pexels API key not configured. Go to WP Admin → AskTam MCP → Settings → AI Media API Keys.');
|
|
|
|
$query = sanitize_text_field($args['query'] ?? '');
|
|
$count = max(1, min(10, (int)($args['count'] ?? 3)));
|
|
$orientation = sanitize_key($args['orientation'] ?? 'landscape');
|
|
$size = sanitize_key($args['size'] ?? 'large');
|
|
$color = sanitize_text_field($args['color'] ?? '');
|
|
$import = (bool)($args['import'] ?? true);
|
|
|
|
if (empty($query)) throw new \RuntimeException('query is required.');
|
|
|
|
$endpoint = add_query_arg(array_filter([
|
|
'query' => $query,
|
|
'per_page' => $count,
|
|
'orientation' => $orientation,
|
|
'size' => $size,
|
|
'color' => $color ?: null,
|
|
]), 'https://api.pexels.com/v1/search');
|
|
|
|
$response = self::httpGet($endpoint, ['Authorization' => $key]);
|
|
$photos = $response['photos'] ?? [];
|
|
|
|
if (empty($photos)) return ['results' => [], 'total' => 0, 'query' => $query];
|
|
|
|
$results = [];
|
|
foreach ($photos as $photo) {
|
|
$item = [
|
|
'pexels_id' => $photo['id'],
|
|
'pexels_url' => $photo['url'],
|
|
'photographer' => $photo['photographer'],
|
|
'photographer_url' => $photo['photographer_url'],
|
|
'avg_color' => $photo['avg_color'] ?? '',
|
|
'alt' => $photo['alt'] ?? $query,
|
|
'src' => $photo['src'],
|
|
'attachment_id' => null,
|
|
'wp_url' => null,
|
|
];
|
|
|
|
if ($import) {
|
|
$img_url = $photo['src']['large2x'] ?? $photo['src']['large'] ?? $photo['src']['original'];
|
|
$att = self::importUrlToMedia($img_url, $item['alt'], "Pexels: {$photo['photographer']}");
|
|
if ($att) {
|
|
$item['attachment_id'] = $att['id'];
|
|
$item['wp_url'] = $att['url'];
|
|
}
|
|
}
|
|
$results[] = $item;
|
|
}
|
|
|
|
return [
|
|
'query' => $query,
|
|
'total' => $response['total_results'] ?? count($results),
|
|
'results' => $results,
|
|
'attribution' => 'Photos provided by Pexels',
|
|
];
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// PEXELS — search videos
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
public static function pexelsSearchVideos(array $args): array {
|
|
$key = get_option(self::OPT_PEXELS_KEY, '');
|
|
if (empty($key)) throw new \RuntimeException('Pexels API key not configured. Go to WP Admin → AskTam MCP → Settings → AI Media API Keys.');
|
|
|
|
$query = sanitize_text_field($args['query'] ?? '');
|
|
$count = max(1, min(5, (int)($args['count'] ?? 2)));
|
|
$orientation = sanitize_key($args['orientation'] ?? 'landscape');
|
|
$min_duration = (int)($args['min_duration'] ?? 0);
|
|
$max_duration = (int)($args['max_duration'] ?? 60);
|
|
|
|
if (empty($query)) throw new \RuntimeException('query is required.');
|
|
|
|
$endpoint = add_query_arg(array_filter([
|
|
'query' => $query,
|
|
'per_page' => $count,
|
|
'orientation' => $orientation,
|
|
'min_duration' => $min_duration ?: null,
|
|
'max_duration' => $max_duration ?: null,
|
|
]), 'https://api.pexels.com/v1/videos/search');
|
|
|
|
$response = self::httpGet($endpoint, ['Authorization' => $key]);
|
|
$videos = $response['videos'] ?? [];
|
|
|
|
if (empty($videos)) return ['results' => [], 'total' => 0, 'query' => $query];
|
|
|
|
$results = [];
|
|
foreach ($videos as $video) {
|
|
$hd_file = null;
|
|
$sd_file = null;
|
|
foreach ($video['video_files'] ?? [] as $file) {
|
|
if ($file['file_type'] !== 'video/mp4') continue;
|
|
if ($file['quality'] === 'hd' && (!$hd_file || ($file['width'] ?? 0) > ($hd_file['width'] ?? 0))) $hd_file = $file;
|
|
if ($file['quality'] === 'sd' && (!$sd_file || ($file['width'] ?? 0) > ($sd_file['width'] ?? 0))) $sd_file = $file;
|
|
}
|
|
$best = $hd_file ?? $sd_file;
|
|
|
|
$thumb_url = $video['image'] ?? ($video['video_pictures'][0]['picture'] ?? null);
|
|
$thumb_att = null;
|
|
if ($thumb_url) {
|
|
$thumb_att = self::importUrlToMedia($thumb_url, $query . ' video thumbnail', 'Pexels Video Thumbnail');
|
|
}
|
|
|
|
$results[] = [
|
|
'pexels_id' => $video['id'],
|
|
'pexels_url' => $video['url'],
|
|
'videographer' => $video['user']['name'] ?? '',
|
|
'videographer_url' => $video['user']['url'] ?? '',
|
|
'duration' => $video['duration'] ?? 0,
|
|
'width' => $video['width'] ?? 0,
|
|
'height' => $video['height'] ?? 0,
|
|
'mp4_url' => $best['link'] ?? '',
|
|
'mp4_quality' => $best['quality'] ?? '',
|
|
'mp4_width' => $best['width'] ?? 0,
|
|
'mp4_height' => $best['height'] ?? 0,
|
|
'thumbnail_url' => $thumb_url,
|
|
'thumbnail_attachment_id' => $thumb_att['id'] ?? null,
|
|
'thumbnail_wp_url' => $thumb_att['url'] ?? null,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'query' => $query,
|
|
'total' => $response['total_results'] ?? count($results),
|
|
'results' => $results,
|
|
'usage_note' => 'Use mp4_url directly in Elementor video widget background. thumbnail_attachment_id can be used as poster/fallback image.',
|
|
'attribution' => 'Videos provided by Pexels',
|
|
];
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GPT IMAGE — generate
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
public static function generateGptImage(array $args): array {
|
|
$key = get_option(self::OPT_OPENAI_KEY, '');
|
|
if (empty($key)) throw new \RuntimeException('OpenAI API key not configured. Go to WP Admin → AskTam MCP → Settings → AI Media API Keys.');
|
|
|
|
$prompt = sanitize_textarea_field($args['prompt'] ?? '');
|
|
$model = sanitize_key($args['model'] ?? 'gpt-image-1');
|
|
$quality = sanitize_key($args['quality'] ?? 'medium');
|
|
$size = sanitize_text_field($args['size'] ?? '1536x1024');
|
|
$n = max(1, min(4, (int)($args['n'] ?? 1)));
|
|
$filename = sanitize_file_name($args['filename'] ?? 'gpt-generated');
|
|
$style = sanitize_key($args['style'] ?? '');
|
|
|
|
if (empty($prompt)) throw new \RuntimeException('prompt is required.');
|
|
|
|
$body = array_filter([
|
|
'model' => $model,
|
|
'prompt' => $prompt,
|
|
'n' => $n,
|
|
'size' => $size,
|
|
'quality' => $quality,
|
|
'style' => $style ?: null,
|
|
]);
|
|
|
|
$response = self::httpPost(
|
|
'https://api.openai.com/v1/images/generations',
|
|
$body,
|
|
['Authorization' => "Bearer {$key}", 'Content-Type' => 'application/json']
|
|
);
|
|
|
|
if (!isset($response['data'])) {
|
|
$err = $response['error']['message'] ?? json_encode($response);
|
|
throw new \RuntimeException("OpenAI API error: {$err}");
|
|
}
|
|
|
|
$results = [];
|
|
foreach ($response['data'] as $i => $img) {
|
|
$fname = count($response['data']) > 1 ? "{$filename}-{$i}" : $filename;
|
|
if (!empty($img['b64_json'])) {
|
|
$att = self::importBase64ToMedia($img['b64_json'], "{$fname}.png", $prompt, 'GPT Image');
|
|
} elseif (!empty($img['url'])) {
|
|
$att = self::importUrlToMedia($img['url'], $prompt, 'GPT Image');
|
|
} else {
|
|
continue;
|
|
}
|
|
if ($att) {
|
|
$results[] = [
|
|
'attachment_id' => $att['id'],
|
|
'url' => $att['url'],
|
|
'prompt' => $prompt,
|
|
'model' => $model,
|
|
'quality' => $quality,
|
|
'size' => $size,
|
|
'source' => 'openai',
|
|
];
|
|
}
|
|
}
|
|
|
|
return ['generated' => count($results), 'results' => $results, 'model' => $model, 'prompt' => $prompt];
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// GEMINI IMAGE — generate (Google Gemini / Nano Banana)
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
public static function generateGeminiImage(array $args): array {
|
|
$key = get_option(self::OPT_GEMINI_KEY, '');
|
|
if (empty($key)) throw new \RuntimeException('Google Gemini API key not configured. Go to WP Admin → AskTam MCP → Settings → AI Media API Keys.');
|
|
|
|
$prompt = sanitize_textarea_field($args['prompt'] ?? '');
|
|
$model = sanitize_key($args['model'] ?? 'gemini-2.0-flash-preview-image-generation');
|
|
$aspect_ratio = sanitize_text_field($args['aspect_ratio'] ?? '16:9');
|
|
$n = max(1, min(4, (int)($args['n'] ?? 1)));
|
|
$filename = sanitize_file_name($args['filename'] ?? 'gemini-generated');
|
|
|
|
if (empty($prompt)) throw new \RuntimeException('prompt is required.');
|
|
|
|
$endpoint = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$key}";
|
|
|
|
$body = [
|
|
'contents' => [['parts' => [['text' => $prompt]]]],
|
|
'generationConfig' => ['responseModalities' => ['TEXT', 'IMAGE'], 'candidateCount' => $n],
|
|
];
|
|
|
|
$response = self::httpPost($endpoint, $body, ['Content-Type' => 'application/json']);
|
|
|
|
if (isset($response['error'])) {
|
|
throw new \RuntimeException("Gemini API error: " . ($response['error']['message'] ?? json_encode($response['error'])));
|
|
}
|
|
|
|
$results = [];
|
|
$i = 0;
|
|
foreach ($response['candidates'] ?? [] as $candidate) {
|
|
foreach ($candidate['content']['parts'] ?? [] as $part) {
|
|
if (!isset($part['inlineData'])) continue;
|
|
$mime = $part['inlineData']['mimeType'] ?? 'image/png';
|
|
$ext = self::mimeToExt($mime);
|
|
$fname = ($n > 1) ? "{$filename}-{$i}.{$ext}" : "{$filename}.{$ext}";
|
|
$att = self::importBase64ToMedia($part['inlineData']['data'], $fname, $prompt, 'Gemini Image');
|
|
if ($att) {
|
|
$results[] = ['attachment_id' => $att['id'], 'url' => $att['url'], 'prompt' => $prompt, 'model' => $model, 'source' => 'gemini'];
|
|
}
|
|
$i++;
|
|
}
|
|
}
|
|
|
|
return ['generated' => count($results), 'results' => $results, 'model' => $model, 'prompt' => $prompt];
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// STATUS CHECK
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
public static function getAiMediaStatus(array $args): array {
|
|
return [
|
|
'pexels_configured' => !empty(get_option(self::OPT_PEXELS_KEY, '')),
|
|
'openai_configured' => !empty(get_option(self::OPT_OPENAI_KEY, '')),
|
|
'gemini_configured' => !empty(get_option(self::OPT_GEMINI_KEY, '')),
|
|
'pexels_key_hint' => self::keyHint(get_option(self::OPT_PEXELS_KEY, '')),
|
|
'openai_key_hint' => self::keyHint(get_option(self::OPT_OPENAI_KEY, '')),
|
|
'gemini_key_hint' => self::keyHint(get_option(self::OPT_GEMINI_KEY, '')),
|
|
'configure_at' => admin_url('admin.php?page=asktam-mcp-settings'),
|
|
];
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// PRIVATE HELPERS
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
private static function importUrlToMedia(string $url, string $alt = '', string $title = ''): ?array {
|
|
require_once ABSPATH . 'wp-admin/includes/media.php';
|
|
require_once ABSPATH . 'wp-admin/includes/file.php';
|
|
require_once ABSPATH . 'wp-admin/includes/image.php';
|
|
|
|
$tmp = download_url($url);
|
|
if (is_wp_error($tmp)) return null;
|
|
|
|
$basename = basename(parse_url($url, PHP_URL_PATH) ?? 'image.jpg');
|
|
$id = media_handle_sideload(['name' => $basename, 'tmp_name' => $tmp], 0, $title ?: $alt);
|
|
@unlink($tmp);
|
|
if (is_wp_error($id)) return null;
|
|
|
|
if ($alt) update_post_meta($id, '_wp_attachment_image_alt', sanitize_text_field($alt));
|
|
if ($title) wp_update_post(['ID' => $id, 'post_title' => sanitize_text_field($title), 'post_excerpt' => sanitize_text_field($alt)]);
|
|
|
|
return ['id' => $id, 'url' => wp_get_attachment_url($id)];
|
|
}
|
|
|
|
private static function importBase64ToMedia(string $b64, string $filename, string $alt = '', string $title = ''): ?array {
|
|
require_once ABSPATH . 'wp-admin/includes/media.php';
|
|
require_once ABSPATH . 'wp-admin/includes/file.php';
|
|
require_once ABSPATH . 'wp-admin/includes/image.php';
|
|
|
|
$data = base64_decode($b64);
|
|
if (!$data) return null;
|
|
|
|
$uploads = wp_upload_dir();
|
|
$tmp_path = $uploads['path'] . '/' . sanitize_file_name($filename);
|
|
if (file_put_contents($tmp_path, $data) === false) return null;
|
|
|
|
$id = media_handle_sideload(['name' => sanitize_file_name($filename), 'tmp_name' => $tmp_path], 0, $title ?: $alt);
|
|
if (is_wp_error($id)) { @unlink($tmp_path); return null; }
|
|
|
|
if ($alt) update_post_meta($id, '_wp_attachment_image_alt', sanitize_text_field($alt));
|
|
if ($title) wp_update_post(['ID' => $id, 'post_title' => sanitize_text_field($title)]);
|
|
|
|
return ['id' => $id, 'url' => wp_get_attachment_url($id)];
|
|
}
|
|
|
|
private static function httpGet(string $url, array $headers = []): array {
|
|
$r = wp_remote_get($url, ['timeout' => 30, 'headers' => $headers]);
|
|
if (is_wp_error($r)) throw new \RuntimeException($r->get_error_message());
|
|
$d = json_decode(wp_remote_retrieve_body($r), true);
|
|
return is_array($d) ? $d : [];
|
|
}
|
|
|
|
private static function httpPost(string $url, array $body, array $headers = []): array {
|
|
$r = wp_remote_post($url, ['timeout' => 60, 'headers' => $headers, 'body' => json_encode($body)]);
|
|
if (is_wp_error($r)) throw new \RuntimeException($r->get_error_message());
|
|
$d = json_decode(wp_remote_retrieve_body($r), true);
|
|
return is_array($d) ? $d : [];
|
|
}
|
|
|
|
private static function mimeToExt(string $mime): string {
|
|
return match($mime) { 'image/jpeg' => 'jpg', 'image/webp' => 'webp', 'image/gif' => 'gif', default => 'png' };
|
|
}
|
|
|
|
private static function keyHint(string $key): string {
|
|
if (strlen($key) < 12) return $key ? '***' : '';
|
|
return substr($key, 0, 6) . str_repeat('•', max(4, strlen($key) - 10)) . substr($key, -4);
|
|
}
|
|
}
|