Installation:
composer require darkwood/media-bundle
Ensure bin/console is executable (chmod +x bin/console).
First Use Case: Generate a video from the bundled example:
php bin/console app:video:generate examples/video.yaml
Verify output in var/output/ (configurable via .env).
Where to Look First:
examples/video.yaml (defines scenes, voiceovers, and assets).docs/mvp-video.md (environment variables, Replicate setup).var/output/scenes/).manifest.json (metadata for all assets).Quick Test: Run the mock tests to validate setup:
make phpunit
This skips live Replicate calls, using HTTP mocks instead.
storage/app/video_templates/welcome.yaml) with scenes like:
scenes:
- type: text
content: "Welcome to {{user.name}}!"
voice: "elevenlabs/voice-clone"
- type: video
source: "storage/app/media/intro.mp4"
php bin/console app:video:generate storage/app/video_templates/welcome.yaml
manifest.json in Laravel to store metadata in a Video model:
$manifest = json_decode(file_get_contents('var/output/manifest.json'), true);
Video::create([
'user_id' => $user->id,
'scenes' => $manifest['scenes'],
'status' => 'generated',
]);
// app/Console/Commands/GenerateVideoCommand.php
use Symfony\Component\Process\Process;
class GenerateVideoCommand extends Command {
protected $signature = 'video:generate {yaml} {--user= : User ID}';
protected $description = 'Generate video for a user';
public function handle() {
$process = new Process(['php', 'bin/console', 'app:video:generate', $this->argument('yaml')]);
$process->run();
if (!$process->isSuccessful()) {
$this->error($process->getErrorOutput());
return 1;
}
// Store manifest in DB
$manifest = json_decode(file_get_contents('var/output/manifest.json'), true);
Video::create([
'user_id' => $this->option('user'),
'manifest' => $manifest,
]);
}
}
// routes/api.php
Route::post('/videos', [VideoController::class, 'generate']);
// app/Http/Controllers/VideoController.php
class VideoController extends Controller {
public function generate(Request $request) {
$yamlPath = $request->user()->videoTemplatePath();
Artisan::call('video:generate', ['yaml' => $yamlPath, '--user' => $request->user()->id]);
return response()->json(['status' => 'queued']);
}
}
// app/Services/YamlGenerator.php
class YamlGenerator {
public function generateWelcomeVideo(User $user) {
$yaml = <<<YAML
scenes:
- type: text
content: "Welcome, {$user->name}!"
voice: "elevenlabs/voice-clone"
YAML;
file_put_contents(storage_path("app/video_templates/{$user->id}.yaml"), $yaml);
return storage_path("app/video_templates/{$user->id}.yaml");
}
}
.env:
MEDIA_BUNDLE_OUTPUT_DIR=storage/app/video_assets
ln -s storage/app/video_assets/public var/www/html/videos
// app/Console/Commands/CleanupVideoAssets.php
class CleanupVideoAssets extends Command {
public function handle() {
$assetsDir = storage_path('app/video_assets');
$this->deleteOldFiles($assetsDir, now()->subDays(30));
}
}
php bin/console app:video:generate video.yaml --env=local --benchmark
{{order.id}}).GenerateVideoJob with the YAML path.// app/Jobs/GenerateVideoJob.php
class GenerateVideoJob implements ShouldQueue {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle() {
$yamlPath = $this->generateYamlForOrder($order);
Artisan::queue('video:generate', ['yaml' => $yamlPath]);
}
}
// app/Console/Commands/BatchGenerateVideos.php
class BatchGenerateVideos extends Command {
public function handle() {
$yamlFiles = glob(storage_path('app/video_templates/batch/*.yaml'));
foreach ($yamlFiles as $file) {
Artisan::queue('video:generate', ['yaml' => $file]);
}
}
}
// routes/web.php
Route::post('/webhooks/video-generate', function (Request $request) {
$yaml = $request->yaml;
file_put_contents(storage_path("app/webhook_templates/{$request->id}.yaml"), $yaml);
Artisan::queue('video:generate', ['yaml' => storage_path("app/webhook_templates/{$request->id}.yaml")]);
return response()->json(['status' => 'accepted']);
});
.env:
REPLICATE_API_TOKEN=your_token_here
php bin/console app:video:generate video.yaml --env=production
try {
Artisan::call('video:generate', ['yaml' => $yamlPath]);
} catch (\Exception $e) {
Video::updateOrCreate(
['user_id' => $user->id],
['status' => 'failed', 'error' => $e->getMessage()]
);
Log::error("Video generation failed: {$e->getMessage()}");
}
// tests/Feature/VideoGenerationTest.php
use Symfony\Component\Process\Process;
public function test_video_generation() {
$process = Process::fromShellCommandline(
'php bin/console app:video:generate examples/video.yaml --benchmark'
);
$process->run();
$this->assertTrue($process->isSuccessful());
$this->assertFileExists('var/output/manifest.json');
}
config/packages/darkwood_media.yaml).custom_scene type:
# config/packages/darkwood_media.yaml
darkwood_media:
scene_types:
custom_scene:
handler: App\Services\CustomSceneHandler
// app/Services/CustomSceneHandler.php
class CustomSceneHandler {
public function render(array $scene) {
// Custom logic (e.g., call a GraphQL
How can I help you explore Laravel packages today?