StoryboardHandler (#37): breaks the project's script into a numbered scene-by-scene breakdown (description + image prompt per scene) via OpenAI in JSON mode (added json_mode support to OpenAiDriver for reliable structured output), stored as a JSON asset. VideoGenerationHandler (#38): generates one still image per storyboard scene via StabilityAI — the scene *visuals*, not motion/video clips (no text-to-video model integrated; see the task comment for why). VideoEditingHandler (#39): the step that actually produces a video file — assembles the scene stills into an MP4 via ffmpeg's concat demuxer, timed to the voiceover audio's duration (probed via ffprobe) or a fixed per-scene duration if there's no audio, then muxes the audio track in. Verified against the real ffmpeg/ffprobe binaries on this box, not just Process::fake(). Asset previews (#45, #46): a reusable partial renders each asset inline by type — image/audio/video get real playback elements, text/ JSON gets a truncated <pre> block, everything else a download link. Wired into both the awaiting_approval step view (so approving/ rejecting is an actual content review, not just a word count) and a general project assets grid. Also fixes a latent bug in AssetStorage::url(), found while wiring up previews: it called Storage::url() on the default 'local' disk, which has no configured URL driver and throws in production — only worked before because Storage::fake() is more permissive than the real local driver. Now serves assets through a new authenticated streaming route instead (VideoProjectAssetController). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
123 lines
6.6 KiB
PHP
123 lines
6.6 KiB
PHP
<div @if ($hasActiveStep) wire:poll.3s @endif>
|
|
<div class="mb-4">
|
|
<a href="{{ route('video-projects.index') }}" wire:navigate class="text-sm text-blue-600 hover:underline">← Video Projects</a>
|
|
</div>
|
|
|
|
<div class="bg-white p-6 rounded border border-gray-200 mb-6">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<h1 class="text-xl font-semibold">{{ $videoProject->title }}</h1>
|
|
@if ($videoProject->description)
|
|
<p class="text-gray-600 mt-1">{{ $videoProject->description }}</p>
|
|
@endif
|
|
</div>
|
|
<span class="text-sm font-medium px-2 py-1 rounded bg-gray-100">{{ ucfirst($videoProject->status) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
@if ($notice)
|
|
<div class="mb-4 p-3 bg-amber-50 border border-amber-200 text-amber-800 rounded text-sm">{{ $notice }}</div>
|
|
@endif
|
|
|
|
@if ($steps->isEmpty())
|
|
<div class="bg-white p-6 rounded border border-gray-200 mb-6 space-y-4">
|
|
<h2 class="text-lg font-semibold">Start Workflow</h2>
|
|
|
|
@if ($templates->isNotEmpty())
|
|
<div class="flex items-end gap-2">
|
|
<div class="flex-1">
|
|
<label class="block text-sm font-medium">From a template</label>
|
|
<select wire:model="workflowTemplateId" class="w-full border rounded p-2">
|
|
<option value="">Select template</option>
|
|
@foreach ($templates as $template)
|
|
<option value="{{ $template->id }}">{{ $template->name }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<button wire:click="startWithTemplate" class="bg-blue-600 text-white px-4 py-2 rounded">Start</button>
|
|
</div>
|
|
@endif
|
|
|
|
<div>
|
|
<button wire:click="startDefault" class="bg-gray-800 text-white px-4 py-2 rounded">
|
|
Start with default pipeline
|
|
</button>
|
|
<p class="text-xs text-gray-500 mt-1">Script → Voiceover → Storyboard → Video → Editing → Thumbnail → Metadata → Publish, each requiring approval.</p>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<div class="bg-white rounded border border-gray-200 mb-6 divide-y divide-gray-100">
|
|
@foreach ($steps as $step)
|
|
<div wire:key="step-{{ $step->id }}" class="p-4">
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<span class="font-medium">{{ $loop->iteration }}. {{ str($step->type)->headline() }}</span>
|
|
<span @class([
|
|
'text-xs px-2 py-0.5 rounded ml-2',
|
|
'bg-gray-100 text-gray-700' => in_array($step->status, ['pending', 'skipped']),
|
|
'bg-blue-100 text-blue-700' => $step->status === 'in_progress',
|
|
'bg-amber-100 text-amber-700' => $step->status === 'awaiting_approval',
|
|
'bg-green-100 text-green-700' => $step->status === 'completed',
|
|
'bg-red-100 text-red-700' => in_array($step->status, ['failed', 'rejected']),
|
|
])>{{ str($step->status)->headline() }}</span>
|
|
</div>
|
|
|
|
<div class="space-x-2">
|
|
@if ($step->status === 'in_progress')
|
|
<button wire:click="runStep({{ $step->id }})" class="text-blue-600 text-sm">Run</button>
|
|
<button wire:click="skipStep({{ $step->id }})" wire:confirm="Skip this step?" class="text-gray-500 text-sm">Skip</button>
|
|
@elseif ($step->status === 'awaiting_approval')
|
|
<button wire:click="approveStep({{ $step->id }})" class="text-green-700 text-sm">Approve</button>
|
|
<button wire:click="rejectStep({{ $step->id }})" class="text-red-600 text-sm">Reject</button>
|
|
@elseif (in_array($step->status, ['failed', 'rejected']))
|
|
<button wire:click="retryStep({{ $step->id }})" class="text-blue-600 text-sm">Retry</button>
|
|
@elseif ($step->status === 'pending')
|
|
<button wire:click="skipStep({{ $step->id }})" wire:confirm="Skip this step?" class="text-gray-500 text-sm">Skip</button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
@if ($step->error)
|
|
<p class="text-sm text-red-600 mt-1">{{ $step->error }}</p>
|
|
@endif
|
|
|
|
@if ($step->output)
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
@if (isset($step->output['word_count']))
|
|
{{ $step->output['word_count'] }} words generated
|
|
@elseif (isset($step->output['characters']))
|
|
{{ $step->output['characters'] }} characters synthesized
|
|
@elseif (isset($step->output['scene_count']))
|
|
{{ $step->output['scene_count'] }} scenes
|
|
@endif
|
|
</p>
|
|
@endif
|
|
|
|
@if ($step->status === 'awaiting_approval' && $step->assets->isNotEmpty())
|
|
<div class="mt-3 grid grid-cols-2 sm:grid-cols-3 gap-3">
|
|
@foreach ($step->assets->sortBy('version') as $asset)
|
|
<div wire:key="approval-asset-{{ $asset->id }}">
|
|
@include('livewire.video-projects.partials.asset-preview', ['asset' => $asset])
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
|
|
@if ($assets->isNotEmpty())
|
|
<div class="bg-white rounded border border-gray-200 p-4">
|
|
<h2 class="text-lg font-semibold mb-4">Assets</h2>
|
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
@foreach ($assets as $asset)
|
|
<div wire:key="asset-{{ $asset->id }}">
|
|
@include('livewire.video-projects.partials.asset-preview', ['asset' => $asset])
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|