darkwood/media-bundle
Symfony CLI tool that converts a YAML video script into per-scene assets (voice and video), saves generation state, and outputs a render manifest. Supports Replicate-based providers, benchmark mode, and clear output file locations.
## Getting Started
### Minimal Steps
1. **Installation**:
```bash
composer require darkwood/media-bundle:^8.1.1
Ensure bin/console is executable (chmod +x bin/console).
First Use Case: Generate a video from the provided example YAML:
php bin/console app:video:generate examples/video.yaml
Verify output in var/output/ (default directory; configurable via .env).
Where to Look First:
docs/mvp-video.md for environment variables (e.g., REPLICATE_API_TOKEN).examples/video.yaml for scene/asset structure.var/output/ for generated assets (e.g., scenes/, voiceovers/).TrueAsyncDriver in the [Flow] section for async workflows (see Implementation Patterns).Benchmark Mode: Test locally without API calls:
REPLICATE_MODE=benchmark php bin/console app:video:generate examples/video.yaml
Laravel Artisan Wrapper: Create a Laravel command to invoke the tool and handle output:
// app/Console/Commands/GenerateVideo.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
class GenerateVideo extends Command
{
protected $signature = 'video:generate {yaml : Path to YAML file} {--async}';
protected $description = 'Generate video assets via Darkwood Media Bundle (supports async mode)';
public function handle()
{
$process = new Process([
'php', 'bin/console', 'app:video:generate', $this->argument('yaml'),
$this->option('async') ? '--async' : ''
]);
$process->run();
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
// Parse manifest and save to DB
$manifest = json_decode(file_get_contents(storage_path('app/video_manifests/manifest.json')), true);
$this->call('video:store-manifest', ['manifest' => json_encode($manifest)]);
}
}
YAML Generation with Async Support: Dynamically create YAML in Laravel and pass it to the tool with async flag:
// Generate YAML from a model (e.g., VideoTemplate)
$yaml = "scenes:\n";
$yaml .= " - title: Welcome\n video: path/to/video.mp4\n voice: Hello, {$user->name}!\n";
file_put_contents(storage_path("app/video_templates/{$id}.yaml"), $yaml);
$this->call('video:generate', [
'yaml' => "app/video_templates/{$id}.yaml",
'--async' => true
]);
Asset Management with Async Workflows:
Use Laravel’s Storage facade to handle generated assets and queue async jobs:
// Store manifest in DB
$manifest = json_decode(file_get_contents($manifestPath), true);
Video::create([
'manifest' => $manifest,
'asset_path' => 'storage/app/public/videos/' . $manifest['id'],
'status' => 'queued', // Track async status
]);
// Dispatch async job
GenerateVideoJob::dispatch($manifest['id'], $this->option('async'));
Template → Async Asset Pipeline:
Laravel Model/Service → YAML Generation → Darkwood Tool (Async) → Queue Worker → Asset Output → Laravel DB/Storage
Error Handling for Async:
monolog with async context:
$process->run(function ($type, $buffer) {
if ($type === Process::ERR) {
\Log::error('Async Video Generation Error: ' . $buffer, [
'job_id' => $job->id,
'manifest_id' => $manifest['id']
]);
}
});
failed() method for async job failures:
// app/Jobs/GenerateVideoJob.php
public function failed(\Throwable $exception)
{
\Log::error('Job failed: ' . $exception->getMessage());
// Notify admin or trigger retry
}
Queue-Based Async Generation:
// app/Jobs/GenerateVideoJob.php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Darkwood\MediaBundle\AsyncDriver;
class GenerateVideoJob implements ShouldQueue
{
use Queueable;
public function __construct(
public string $manifestId,
public bool $asyncMode
) {}
public function handle(AsyncDriver $driver)
{
$yamlPath = storage_path("app/video_templates/{$this->manifestId}.yaml");
$driver->generate($yamlPath, $this->asyncMode);
}
}
Environment Variables:
Sync Laravel’s .env with the tool’s requirements (no changes needed):
# Laravel .env
REPLICATE_API_TOKEN=${REPLICATE_API_TOKEN}
MEDIA_BUNDLE_OUTPUT=storage/app/public/videos
Testing Async Workflows:
// tests/Feature/VideoGenerationTest.php
public function test_async_video_generation()
{
$this->artisan('video:generate', [
'yaml' => 'tests/fixtures/video.yaml',
'--async' => true
])->expectsOutput('Async job dispatched!')
->assertExitCode(0);
$this->assertDatabaseHas('jobs', [
'payload' => json_contains(['commandName' => 'GenerateVideoJob'])
]);
}
REPLICATE_MODE=benchmark ./vendor/bin/phpunit
Extending YAML Schema with Async Flags: Add async-specific fields to YAML:
# Custom YAML example with async metadata
scenes:
- title: Async Scene
video: custom_path.mp4
voice: Custom text
async: true # Hint for async processing
priority: high
Filesystem Permissions:
storage/app/) and the tool’s output path are writable by the PHP process.chmod -R 755 storage/ and verify the www-data (or equivalent) user has access.Replicate API Limits:
use Illuminate\Support\Facades\Http;
Http::retry(3, 100)->post('https://api.replicate.com/...');
YAML Schema Mismatches:
use Symfony\Component\Yaml\Yaml;
try {
$data = Yaml::parseFile($yamlPath);
} catch (\Exception $e) {
\Log::error("Invalid YAML: " . $e->getMessage());
throw new \InvalidArgumentException("YAML validation failed");
}
Stateful Async Runs:
TrueAsyncDriver may require additional state management for concurrent runs.MEDIA_BUNDLE_PERSIST_STATE=false php bin/console app:video:generate video.yaml --async
Manifest Parsing:
"require": {
"darkwood/media-bundle": "^8.1.1"
}
Symfony 8.1.1 Upgrade:
TrueAsyncDriver requires Symfony’s async components).Verbose Output: Enable debug mode for detailed logs:
php bin/console app:video:generate video.yaml --verbose
Dry Runs: Test without generating assets:
MEDIA_BUNDLE_DRY_RUN=true php bin/console
How can I help you explore Laravel packages today?