StepHandler abstraction (app/Support/VideoWorkflow/StepHandlers/) plugs
into WorkflowEngine without coupling it to how any given step type is
actually executed:
- ScriptGenerationHandler: builds a prompt from the project's title/
description (or an explicit outline/tone/keywords/target length in
step input), generates via an active OpenAI AiProvider, stores the
result as a 'script' asset.
- TtsVoiceoverHandler: voices step.input.text, or falls back to the
project's latest script asset, via an active ElevenLabs AiProvider;
voice_id/model_id are configurable per step. No direct "speed"
control — ElevenLabs' base TTS endpoint doesn't expose one.
RunWorkflowStepJob resolves a step's handler by type and runs it on
the queue, matching the existing PublishPostJob pattern (including a
stale-dispatch guard: no-op if the step isn't 'in_progress' anymore).
UI, under its own /video-projects routes and nav entry — separate
from /projects per the earlier decision:
- VideoProjects/Index (#43): list projects with status and step
progress, create new ones.
- VideoProjects/Show (#44): start a project from a WorkflowTemplate or
a hardcoded default 8-step pipeline; per-step Run/Approve/Reject/
Retry/Skip actions; auto-queues the next step after any transition
when a handler exists for its type (steps with no handler yet just
sit in_progress rather than auto-failing); lists project assets.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AiProviderDriver: one generic interface (generate(prompt, options) ->
AiGenerationResult) rather than per-capability interfaces, since text
(OpenAI chat completions), image (StabilityAI stable-image), and
speech (ElevenLabs TTS) generation all reduce to prompt-in/content-out.
Drivers throw AiGenerationException on failure; AiProviderManager
resolves the right driver from an AiProvider record's `provider`
column and guards on is_active. Real request/response shapes per each
provider's API, verified with Http::fake() — no live API keys
available yet to smoke test against the real services.
AssetStorage: stores generated content under
video-projects/{id}/{type}/v{n}.{ext}, versioned per (project, type[,
step]) slot so retries don't clobber earlier output, plus
contents/url/delete and pruneOldVersions for cleanup.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Create twitter-post sidecar with Playwright for browser automation
- Create linkedin-post sidecar with Playwright for browser automation
- Add TwitterPublisher class that calls twitter-post sidecar
- Add LinkedInPublisher class that calls linkedin-post sidecar
- Register both publishers in PublisherManager via AppServiceProvider
- Update Posts index view to show Publish button for twitter/linkedin types
Both publishers use the same pattern as RedditPublisher:
- No API keys required (uses stored credentials from Social Accounts)
- Browser automation via Playwright
- Returns JSON result with success/failure and URL
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
Introduce Publisher/PublisherManager (WordPress + Reddit implementations)
so publishing logic is shared between the CLI, the queue, and future
network integrations. Add a "Publish" button on the Posts page that
dispatches PublishPostJob instead of blocking the request — Reddit's
publisher shells out to the existing Playwright sidecar and can take
tens of seconds, so it can't run synchronously in a web request.
Adds a `target` field (e.g. subreddit) and `publishing` status to Post,
plus a `publish_error` field so failures surface in the UI. Refactors
posts:publish to delegate to WordPressPublisher without changing its
CLI output contract (existing tests pass unchanged).
Also fixes two bugs found along the way: UpdatePostRequest was silently
dropping `published_url` from PATCH requests (never in its validation
rules, despite AI_API.md documenting it), and three Livewire index-page
tests were broken by the earlier auth change (never re-run after adding
the `auth` middleware) since they hit routes via HTTP without a logged-in
user.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>