// ════════════════════════════════════════════════════════════
// [GLOBAL] — Used by BOTH Seedream and Seedance
// ════════════════════════════════════════════════════════════
define('GMI_API_BASE', 'https://console.gmicloud.ai/api/v1/ie/requestqueue/apikey');
define('GMI_REQUESTS', GMI_API_BASE . '/requests');
define('TIMEOUT_CONNECT', 10);
// CORS headers (send before any output)
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
// ── extract_media_urls() ──────────────────────────────────────
// Handles image_url (Seedream) AND video_url / thumbnail_image_url (Seedance)
function extract_media_urls(array $outcome): array {
$urls = [];
foreach (['image_url', 'output_url', 'video_url', 'thumbnail_image_url'] as $key) {
if (!empty($outcome[$key]) && is_string($outcome[$key])) $urls[] = $outcome[$key];
}
foreach (['images', 'media_urls', 'image_urls'] as $key) {
if (!empty($outcome[$key]) && is_array($outcome[$key])) {
foreach ($outcome[$key] as $item) {
if (is_string($item)) $urls[] = $item;
elseif (isset($item['url'])) $urls[] = $item['url'];
elseif (isset($item['image_url'])) $urls[] = $item['image_url'];
}
}
}
return array_values(array_unique(array_filter($urls)));
}
// ── gmi_curl() ────────────────────────────────────────────────
function gmi_curl(string $url, string $apiKey, string $method, ?array $payload, int $timeout): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => TIMEOUT_CONNECT,
CURLOPT_ENCODING => '', // accept gzip/deflate — IMPORTANT
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
'Accept: application/json',
],
]);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
}
$raw = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
$curlNo = curl_errno($ch); // read BEFORE curl_close()
curl_close($ch);
if ($raw === false || $curlErr) {
if ($curlNo === 28) return ['error' => "Timed out after {$timeout}s.", 'is_timeout' => true];
return ['error' => "cURL ({$curlNo}): {$curlErr}"];
}
if ($httpCode === 401) return ['error' => '401 Unauthorized — use ie_model scoped key.', 'http_code' => 401];
$decoded = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) return ['error' => 'Non-JSON response (HTTP '.$httpCode.').'];
if ($httpCode >= 400) return ['error' => $decoded['message'] ?? 'GMI error '.$httpCode, 'http_code' => $httpCode];
return $decoded;
}
// ── [GLOBAL] Poll action ──────────────────────────────────────
// POST ?action=poll body: { apiKey, request_id, model }
// Works identically for both Seedream and Seedance.
$response = gmi_curl(GMI_REQUESTS . '/' . urlencode($requestId), $apiKey, 'GET', null, 15);
if (($response['status'] ?? '') === 'dispatched') $response['status'] = 'queued';
if (($response['status'] ?? '') === 'success') {
$response['outcome']['media_urls'] = extract_media_urls($response['outcome'] ?? []);
}
$response['_raw_outcome'] = $response['outcome'] ?? null;
// ════════════════════════════════════════════════════════════
// [SEEDREAM] — Image Generation Block
// POST ?action=seedream_generate
// ════════════════════════════════════════════════════════════
define('SEEDREAM_MODEL', 'seedream-5.0-lite');
define('SEEDREAM_TIMEOUT_GEN', 35); // Cold-start can be slow
define('SEEDREAM_TIMEOUT_POLL', 15);
// ── Build GMI payload ─────────────────────────────────────────
$sizeMap = ['2K' => '2560x1440', '3K' => '3072x1728', '2048x2048' => '2048x2048'];
$resolvedSize = $sizeMap[$size] ?? (preg_match('/^\d+x\d+$/', $size) ? $size : '2048x2048');
$gmiPayload = [
'model' => SEEDREAM_MODEL,
'payload' => [
'prompt' => $prompt, // string, required
'size' => $resolvedSize, // "2048x2048" | "2560x1440" | "3072x1728"
'watermark' => $watermark, // bool
'sequential_image_generation' => $seqGen, // "auto" | "disabled"
'sequential_image_generation_options' => ['max_images' => $maxImages], // 1–15
],
];
// Optional reference images (array of {url:...} objects)
if (!empty($refImages)) {
$gmiPayload['payload']['image'] = array_map(
fn($i) => is_string($i) ? ['url' => $i] : $i, $refImages
);
}
// ── Submit ────────────────────────────────────────────────────
$resp = gmi_curl(GMI_REQUESTS, $apiKey, 'POST', $gmiPayload, SEEDREAM_TIMEOUT_GEN);
// Returns: { request_id, model, status:"dispatched", created_at, ... }
// ── Poll ──────────────────────────────────────────────────────
$poll = gmi_curl(GMI_REQUESTS . '/' . $reqId, $apiKey, 'GET', null, SEEDREAM_TIMEOUT_POLL);
// On success outcome keys: image_url | images[] | media_urls[]
// Use extract_media_urls($poll['outcome']) to normalise.
// ════════════════════════════════════════════════════════════
// [SEEDANCE] — Video Generation Block
// POST ?action=seedance_generate
// ════════════════════════════════════════════════════════════
define('SEEDANCE_MODEL', 'seedance-2-0-fast-260128');
define('SEEDANCE_TIMEOUT_GEN', 40); // Video jobs take longer to queue
define('SEEDANCE_TIMEOUT_POLL', 15);
// ── Build GMI payload ─────────────────────────────────────────
$innerPayload = [
'prompt' => $prompt, // string, required
'duration' => $duration, // int 4–15, default 5
'resolution' => $resolution, // "480p" | "720p" | "1080p"
'ratio' => $ratio, // "16:9"|"4:3"|"1:1"|"3:4"|"9:16"|"21:9"|"adaptive"
'watermark' => $watermark, // bool
'generate_audio' => $genAudio, // bool, default true
'web_search' => $webSearch, // bool, default false
];
// All optional fields — only add if provided:
if ($seed !== null) $innerPayload['seed'] = $seed; // 0–4294967295
if ($firstFrame !== '') $innerPayload['first_frame'] = $firstFrame; // public URL
if ($lastFrame !== '') $innerPayload['last_frame'] = $lastFrame; // public URL
if (!empty($refImages)) $innerPayload['reference_images'] = $refImages; // string[]
if (!empty($refVideos)) $innerPayload['reference_videos'] = $refVideos; // string[]
if (!empty($refAudios)) $innerPayload['reference_audios'] = $refAudios; // string[]
if (!empty($refAssets)) $innerPayload['reference_asset_ids'] = $refAssets; // string[]
$gmiPayload = ['model' => SEEDANCE_MODEL, 'payload' => $innerPayload];
// ── Submit ────────────────────────────────────────────────────
$resp = gmi_curl(GMI_REQUESTS, $apiKey, 'POST', $gmiPayload, SEEDANCE_TIMEOUT_GEN);
// Returns: { request_id, model, status:"queued", created_at, ... }
// ── Poll ──────────────────────────────────────────────────────
$poll = gmi_curl(GMI_REQUESTS . '/' . $reqId, $apiKey, 'GET', null, SEEDANCE_TIMEOUT_POLL);
// On success outcome keys: video_url | thumbnail_image_url
// Use extract_media_urls($poll['outcome']) to get both URLs.
// video_url → .mp4 playback
// thumbnail_image_url → preview image