where('provider', 'stability_ai')->where('is_active', true)->first(); if (! $provider) { $engine->failStep($step, 'No active StabilityAI provider configured.'); return; } $scenes = $this->resolveScenes($step); if (! $scenes) { $engine->failStep($step, 'No storyboard available — complete a storyboard step first.'); return; } $style = ($step->input ?? [])['style'] ?? null; $assetIds = []; foreach ($scenes as $scene) { $prompt = $scene['visual_prompt'] ?? $scene['description'] ?? null; if (! $prompt) { continue; } if ($style) { $prompt .= ", {$style}"; } try { $result = $this->aiProviders->generate($provider, $prompt); } catch (AiGenerationException $e) { $engine->failStep($step, "scene {$scene['scene_number']}: {$e->getMessage()}"); return; } $extension = str($result->contentType)->after('image/')->toString() ?: 'png'; $asset = $this->assets->store( $step->videoProject, 'image', $result->content, $extension, step: $step, mimeType: $result->contentType, metadata: ['scene_number' => $scene['scene_number'] ?? null, 'prompt' => $prompt], ); $assetIds[] = $asset->id; } if (empty($assetIds)) { $engine->failStep($step, 'No scenes had a usable visual_prompt.'); return; } $engine->completeStep($step, [ 'asset_ids' => $assetIds, 'scene_count' => count($assetIds), ]); } /** * @return array>|null */ private function resolveScenes(WorkflowStep $step): ?array { $input = $step->input ?? []; if (! empty($input['scenes'])) { return $input['scenes']; } $storyboardAsset = $step->videoProject->assets() ->where('type', 'other') ->whereJsonContains('metadata->kind', 'storyboard') ->orderByDesc('version') ->first(); if (! $storyboardAsset) { return null; } $decoded = json_decode($this->assets->contents($storyboardAsset), true); return $decoded['scenes'] ?? null; } }