Files
muninn/app/Support/VideoWorkflow/StepHandlers/StoryboardHandler.php
T

117 lines
3.7 KiB
PHP
Raw Normal View History

<?php
namespace App\Support\VideoWorkflow\StepHandlers;
use App\Models\AiProvider;
use App\Models\WorkflowStep;
use App\Support\AiProviders\AiGenerationException;
use App\Support\AiProviders\AiProviderManager;
use App\Support\AssetStorage\AssetStorage;
use App\Support\VideoWorkflow\WorkflowEngine;
/**
* Breaks the project's script into a numbered scene-by-scene storyboard —
* each scene gets a description and an image-generation prompt — via an
* OpenAI-backed AiProvider in JSON mode. Produces the text/description
* layer only; VideoGenerationHandler turns each scene's visual_prompt into
* an actual image.
*/
class StoryboardHandler implements StepHandler
{
public function __construct(
private readonly AiProviderManager $aiProviders,
private readonly AssetStorage $assets,
) {}
public function type(): string
{
return 'storyboard';
}
public function handle(WorkflowStep $step, WorkflowEngine $engine): void
{
$provider = AiProvider::query()->where('provider', 'openai')->where('is_active', true)->first();
if (! $provider) {
$engine->failStep($step, 'No active OpenAI provider configured.');
return;
}
$script = $this->resolveScript($step);
if (! $script) {
$engine->failStep($step, 'No script available to storyboard — complete a script_generation step first.');
return;
}
try {
$result = $this->aiProviders->generate($provider, $this->buildPrompt($step, $script), ['json_mode' => true]);
} catch (AiGenerationException $e) {
$engine->failStep($step, $e->getMessage());
return;
}
$decoded = json_decode($result->content, true);
$scenes = $decoded['scenes'] ?? null;
if (! is_array($scenes) || empty($scenes)) {
$engine->failStep($step, 'AI provider returned an unusable storyboard: no scenes found.');
return;
}
$asset = $this->assets->store(
$step->videoProject,
'other',
json_encode(['scenes' => $scenes], JSON_PRETTY_PRINT),
'json',
step: $step,
mimeType: 'application/json',
metadata: ['kind' => 'storyboard', 'scene_count' => count($scenes)],
);
$engine->completeStep($step, [
'asset_id' => $asset->id,
'scene_count' => count($scenes),
]);
}
private function resolveScript(WorkflowStep $step): ?string
{
$input = $step->input ?? [];
if (! empty($input['script'])) {
return $input['script'];
}
$scriptAsset = $step->videoProject->assets()
->where('type', 'script')
->orderByDesc('version')
->first();
return $scriptAsset ? $this->assets->contents($scriptAsset) : null;
}
private function buildPrompt(WorkflowStep $step, string $script): string
{
$input = $step->input ?? [];
$prompt = "Break the following video script into a numbered scene-by-scene storyboard.\n\n"
."Script:\n{$script}\n\n"
.'For each scene provide: scene_number (integer), description (what happens on screen), '
."and visual_prompt (a concise, self-contained image-generation prompt describing this scene's visual).";
if (! empty($input['style'])) {
$prompt .= " Every visual_prompt must consistently reflect this visual style: {$input['style']}.";
}
$prompt .= "\n\nRespond with JSON only, in exactly this shape: "
.'{"scenes": [{"scene_number": 1, "description": "...", "visual_prompt": "..."}]}';
return $prompt;
}
}