Source path: app/Jobs/PollVideo.php
Extracted from supplied specification. Review before copying into application.

<?php
namespace App\Jobs;

use App\Events\VideoReady;
use App\Models\Media;
use App\Services\Ai\AiManager;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class PollVideo implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 1;
    public int $timeout = 900;

    public function __construct(public Media $media, public int $round = 0) {}

    public function handle(AiManager $ai): void
    {
        // Find the same provider (and config) that submitted the job
        $entry = collect($ai->chain('video', $this->media->organization))->first(fn ($e) => $e[2] === $this->media->provider);
        if (!$entry) { $this->fail_('Provider no longer configured.'); return; }
        [$driver, $cfg] = $entry;

        $r = $driver->poll($this->media->provider_meta ?? [], $cfg);

        if ($r['status'] === 'processing') {
            if ($this->round > 120) { $this->fail_('Generation timed out.'); return; }
            self::dispatch($this->media, $this->round + 1)->delay(now()->addSeconds(20));
            return;
        }
        if ($r['status'] === 'failed') { $this->fail_($r['error'] ?? 'Provider error.'); return; }

        $disk = Storage::disk(config('filesystems.default'));
        $raw = 'tmp/' . Str::uuid() . '.mp4';
        $out = "media/{$this->media->organization_id}/" . Str::uuid() . '.mp4';
        $disk->put($raw, Http::timeout(300)->get($r['url'])->body());

        // Download to local temp for FFmpeg (works for S3 too)
        $in = tempnam(sys_get_temp_dir(), 'in') . '.mp4';
        $outLocal = tempnam(sys_get_temp_dir(), 'out') . '.mp4';
        file_put_contents($in, $disk->get($raw));

        $p = Process::timeout(600)->run([
            'ffmpeg', '-y', '-i', $in,
            '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'veryfast',
            '-c:a', 'aac', '-movflags', '+faststart', $outLocal,
        ]);

        $disk->delete($raw);
        @unlink($in);

        if ($p->failed()) { @unlink($outLocal); $this->fail_('FFmpeg failed: ' . Str::limit($p->errorOutput(), 400)); return; }

        $disk->put($out, file_get_contents($outLocal));
        @unlink($outLocal);

        $this->media->update(['path' => $out, 'status' => 'ready']);
        VideoReady::dispatch($this->media);
    }

    private function fail_(string $msg): void
    {
        $this->media->update(['status' => 'failed', 'error' => $msg]);
        $this->media->organization->refund(config('ai.cost.video'));
    }
}

MediaController changes:

php
public function generateImage(Request $r)
{
    Roles::require($r, 'editor');
    $data = $r->validate(['prompt' => 'required|string|max:1000', 'size' => 'in:1024x1024,1024x1536,1536x1024']);
    $org = app('currentOrg');
    abort_if(!$org->spend(config('ai.cost.image')), 402, 'Not enough AI credits.');

    $m = Media::create([
        'user_id' => $r->user()->id, 'type' => 'image', 'source' => 'ai',
        'prompt' => $data['prompt'], 'status' => 'pending',
    ]); // organization_id set by trait
    GenerateImage::dispatch($m, $data['size'] ?? '1024x1024');
    return response()->json($m, 202);
}

public function generateVideo(Request $r)
{
    Roles::require($r, 'editor');
    $data = $r->validate(['prompt' => 'required|string|max:1000', 'aspect' => 'in:9:16,16:9,1:1']);
    abort_if(!app('currentOrg')->spend(config('ai.cost.video')), 402, 'Not enough AI credits.');

    $m = Media::create([
        'user_id' => $r->user()->id, 'type' => 'video', 'source' => 'ai',
        'prompt' => $data['prompt'], 'status' => 'pending',
    ]);
    GenerateVideo::dispatch($m, $data['aspect'] ?? '9:16');
    return response()->json($m, 202);
}

Uploads use $file->store("media/" . app('currentOrg')->id, ...). Add 'provider_meta' => 'array' to the Media casts and an organization() relation, which the trait already gives you.

Caption + AI settings controller (app/Http/Controllers/AiController.php):

php
<?php
namespace App\Http\Controllers;

use App\Models\OrgAiProvider;
use App\Services\Ai\AiManager;
use App\Support\Roles;
use Illuminate\Http\Request;

class AiController extends Controller
{
    public function caption(Request $r, AiManager $ai)
    {
        Roles::require($r, 'editor');
        $d = $r->validate([
            'topic' => 'required|string|max:1000',
            'platform' => 'required|in:facebook,instagram,x,linkedin,tiktok,youtube',
            'tone' => 'nullable|string|max:50',
        ]);
        $system = "You write {$d['platform']} social posts. Tone: " . ($d['tone'] ?? 'professional')
                . ". Respect the platform's length norms. Return only the post text.";

        foreach ($ai->chain('text', app('currentOrg')) as [$driver, $cfg]) {
            try { return ['caption' => $driver->complete($system, $d['topic'], $cfg)]; }
            catch (\Throwable $e) { continue; }
        }
        abort(503, 'No text provider available.');
    }

    // Org admins add their own keys
    public function saveProvider(Request $r)
    {
        Roles::require($r, 'admin');
        $d = $r->validate([
            'kind' => 'required|in:image,video,text',
            'provider' => 'required|string|in:' . implode(',', array_keys(array_merge(...array_values(config('ai.drivers'))))),
            'api_key' => 'required|string',
            'account_id' => 'nullable|string',
            'model' => 'nullable|string|max:191',
            'priority' => 'integer|min:1|max:100',
        ]);

        return OrgAiProvider::updateOrCreate(
            ['organization_id' => app('currentOrg')->id, 'kind' => $d['kind'], 'provider' => $d['provider']],
            [
                'credentials' => array_filter(['api_key' => $d['api_key'], 'account_id' => $d['account_id'] ?? null]),
                'model' => $d['model'] ?? null, 'priority' => $d['priority'] ?? 10, 'is_enabled' => true,
            ]
        );
    }
}
