Commit Graph
10 Commits
Author SHA1 Message Date
m105andClaude Sonnet 5 6a51fda61e Add thumbnail, metadata, and YouTube publishing handlers (#40, #41, #42)
ThumbnailGenerationHandler (#40): generates thumbnail candidates via
StabilityAI, one per style (default: two contrasting styles) — all
stored as assets on the same step, so the existing approval UI's
"show every asset on this step side by side" behavior is the A/B
comparison the task calls for, no extra UI needed.

MetadataGenerationHandler (#41): SEO title/description/tags/hashtags
from the project's script via OpenAI in JSON mode, same pattern as
StoryboardHandler.

YoutubePublishHandler (#42): real OAuth2 + YouTube Data API v3
resumable upload (token refresh -> init session -> PUT video bytes),
supporting scheduling via publishAt (forces privacyStatus=private per
YouTube's own constraint). Reuses the AiProvider table for credentials
(provider='youtube', api_key=refresh_token, config=client_id/secret)
rather than a new table — added 'youtube' to AiProvider::PROVIDERS.
Does not cover analytics retrieval: that's an ongoing query against a
published video, not a one-shot publish action, so it doesn't fit the
per-video step model at all.

All 8 WorkflowStep::TYPES now have real handlers, so the "unhandled
type" test fixtures (which used real-but-not-yet-implemented types)
would go stale every time a new module lands — switched them to
fabricated type strings so they stay meaningful regardless of what
gets built next.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 21:26:56 +03:00
m105andClaude Sonnet 5 c249a14d78 Add storyboard/video-gen/video-editing handlers and asset previews (#37-39, #45, #46)
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>
2026-08-06 21:15:31 +03:00
m105andClaude Sonnet 5 55fa2ebfda Add script/TTS step handlers, dashboard, and pipeline UI (#35, #36, #43, #44)
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>
2026-08-06 20:56:01 +03:00
m105andClaude Sonnet 5 0e315ada04 Add AI provider abstraction and asset storage (tasks #33, #34)
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>
2026-08-06 20:47:06 +03:00
m105andClaude Sonnet 5 6ff7f78c9c Add Video Projects schema and workflow engine core (tasks #31, #32)
Foundational layer for the YouTube Content Creation System project,
built as a fully independent domain per the earlier decision (task
#47) — no foreign keys into the existing projects/tasks/ideas tables.

Schema: video_projects, workflow_steps, assets, ai_providers,
workflow_templates (named to avoid colliding with the existing
Post-templates `templates` table).

WorkflowEngine (app/Support/VideoWorkflow/) drives a project's step
sequence: start/startFromTemplate, currentStep, completeStep (with
optional approval gating), approveStep/rejectStep, failStep/retryStep,
skipStep. It only manages state transitions — actual step execution
(AI calls, rendering, etc.) is out of scope here (tasks #33+).

Also fixes four Publisher classes (WordPress/Reddit/Twitter/LinkedIn)
that were missing a `use App\Models\SocialAccount;` import after an
earlier interface change, which broke the entire test suite at
bootstrap; and PostFactory, which still set the now-removed
`social_account_id` column instead of the SocialAccount pivot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 20:38:52 +03:00
m105 90f313c134 Add Cross-Posting support for multiple social accounts
- Create post_social_account pivot table for many-to-many relationship
- Add migration to migrate existing social_account_id to pivot table
- Remove social_account_id column from posts table
- Update Post model with socialAccounts() BelongsToMany relationship
- Update Posts Livewire component to use array of selectedAccountIds
- Update Posts view with checkboxes for selecting multiple accounts
- Update PublishPostJob to iterate through all social accounts of matching type
- Update Publisher interface to accept optional SocialAccount parameter
- Update all Publishers (WordPress, Reddit, Twitter, LinkedIn) to support cross-posting
- Display multiple social accounts as badges in posts table

Now a single post can be published to multiple social accounts of the same type.
For example: a Twitter post can be published to multiple Twitter accounts.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-07-27 21:24:49 +03:00
m105 44f979820f Add Twitter and LinkedIn publishers with Playwright sidecars
- 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>
2026-07-27 21:08:39 +03:00
m105andClaude Sonnet 5 4b3af4cc89 Make the image optional for WordPress posts too
Some blog posts just don't have one. WordPressPublisher no longer
rejects publishing a type=blog post without image_url — it uploads
and attaches it as the featured image when present, and simply skips
that step (no featured_media set) otherwise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 20:07:47 +03:00
m105andClaude Sonnet 5 42abc658f2 Add an image field, required for WordPress posts
Posts get a nullable image_url. WordPressPublisher now downloads it
and uploads it to WordPress's media endpoint before creating the
post, setting it as featured_media — and rejects publishing (without
touching status) if a blog post has no image set, mirroring the
existing title requirement. Every other network treats it as optional.

Not enforced at save time, same as title — you can still draft an
incomplete WordPress post, it just can't be published until an image
is set. Exposed through the UI form (with a thumbnail/""missing""
indicator in the table), the JSON API, and the Python CLI wrapper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 20:03:25 +03:00
m105andClaude Sonnet 5 9278d2e8a0 Add a Publisher abstraction and queued job to publish from the UI
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>
2026-07-21 14:04:45 +03:00