Build SocialFaktory
KINDAreplaces $49/mosaves $588/yrback to the verdict
A personal video pipeline whose hard problems are proving a render is labelled as synthetic and keeping that marking through every re-encode, never paying twice for a render that timed out rather than failed, and keeping the publishing where the platform credentials already live.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the pipeline and burns the visible label into the video.
Get it nodejs.org for Node, your package manager for ffmpeg. open ↗
Verify
node --version and ffmpeg -version both print - API keypennies per script
Why Writes the hook, script and visual prompt from the brief.
Get it Create a key with the provider you prefer and set a spend limit in their console.
Verify
One brief returns three validated fields - API keybilled per second of output; measure one render before planning a schedule
Why This is the expensive call and the one that must never be made twice for the same script.
Get it Create a key with a provider whose queue API returns a request identifier you can poll.
Verify
One submit returns a request id and one poll returns a finished file - accountfree software; your own hosting
Why It holds the platform credentials and the labelling fields, and it is the reason this build does not need a single platform developer account.
Get it Run Postiz yourself and connect the channels you post to, then create an API key in it. open ↗
Verify
A test post from this app appears in the Postiz queue - decidefree
Why Article 50 covers disclosure; a person's voice and face are a separate matter and the one that produces angry messages.
Get it List the stock presenters you will use, and for any real person obtain and file written permission with a date.
Verify
Every presenters row of kind real_person has a consent reference and date
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- briefs(id, topic, tone, target_seconds, created_at, brand_id) - scripts(id, brief_id, hook, spoken, visual_prompt, model_id, prompt_sha, cost_minor, created_at) - renders(id, script_id, provider, model_id, provider_request_id, state, mp4_path, seconds, cost_minor, submitted_at, finished_at) · provider_request_id is written before the first poll, not after - presenters(id, kind, source, consent_ref, consent_granted_at, notes) · kind is stock or real_person, and a real person without a consent_ref cannot be selected - disclosures(render_id, visible_label, burnt_in_at, marking_present_at_download, marking_present_after_encode, checked_at) - publications(id, render_id, postiz_post_id, channel, scheduled_for, ai_flag_set, published_at, state) - No row reaches publications without a disclosures row whose two marking checks are recorded, and no render starts without a presenter row that carries its consent evidence.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
SCRIPT_API_KEYsecret | required | sk-...redacted | The text model that writes scripts. |
VIDEO_API_KEYsecret | required | key-...redacted | The video model. Set a provider-side spend limit as well. |
VIDEO_MODEL_ID | required | a dated video model identifier | Stored per render, because output quality and pricing both move. |
POSTIZ_URL | required | https://postiz.your-host.example | Your own instance. This app talks to nothing else on the network except the two model providers. |
POSTIZ_API_KEYsecret | required | pz-...redacted | Created inside your Postiz instance. |
DISCLOSURE_LABEL | required | AI-generated video | Burnt into every render. Cannot be empty. |
MEDIA_DIR | required | ./media | Downloaded and encoded files. These are the expensive artefacts, so back them up. |
MONTHLY_COST_CEILING | required | 60.00 | Submissions stop here. Review and publishing keep working. |
The build, in order
A brief becomes a script you can read
One model call turns a topic and tone into a hook, a spoken script and a visual prompt, all stored and editable.
You will tune this weekly and need to know which version wrote what.
Files
prompts/script.mdsrc/script/generate.tsA missing hook is better as an error than as an empty caption.
Files
src/script/schema.tstests/script.test.tsThe render is the expensive step and the script is free to fix.
Files
src/web/script.ejs
done when · tick each as it passeswatch out- Letting the script step feed straight into the render step means you pay for video before a human has read a word, and a script that misreads the brief costs a full render to discover. Put the script on screen as text with an edit box, and make rendering a separate deliberate action.
A render that is never paid for twice
Submitting to the video model records the request identifier before polling, and a restart resumes rather than resubmits.
That identifier is the only proof you already paid.
Files
src/render/submit.tstests/resume.test.tsA crash is not a reason to buy the clip again.
Files
src/render/resume.tsThe runaway retry loop is the failure mode with a price tag.
Files
src/render/cap.ts
done when · tick each as it passeswatch out- A polling loop that treats a slow job as a failure and submits again turns one twenty-second clip into two invoices, and video models are priced per second of output rather than per call, so this is the mistake that actually hurts. Store the request id before the first poll, resume from it after any crash, and put a hard cap on how many times a script may be submitted at all.
The label, and keeping it through the encode
Every finished video has a visible synthetic-content label and its machine-readable marking is checked twice.
You need a before to have a loss.
Files
src/label/marking.tsRe-encoding is where provenance quietly dies.
Files
src/label/burn.tstests/label.test.tsUnknown is not the same as present.
Files
src/publish/gate.ts
done when · tick each as it passeswatch out- Most transcoding drops metadata it does not recognise, so the provenance marking a generator attached at creation is routinely lost the moment you re-encode to burn in a caption, and nobody notices because the video still plays. Check for the marking on the downloaded file and again on the encoded one, store both results, and treat a silent loss as a finding rather than an accident.
Who is in this video, and did they agree
A presenter is chosen from a register, and a real person cannot be used without recorded consent.
A foreign key can be checked; a sentence cannot.
Files
src/db/migrations/003_presenters.sqlA warning is a thing people click through at eleven at night.
Files
src/render/presenter.tstests/consent.test.tsThe moment of approval is when it matters.
Files
src/web/review.ejs
done when · tick each as it passeswatch out- Treating the presenter as a string in the visual prompt means nothing in the system knows whether the face belongs to a stock model or to a colleague whose voice you sampled from a meeting recording, and the difference is the entire legal exposure. Make the presenter a foreign key, make consent evidence mandatory on real-person rows, and block the render rather than warning about it.
Handing it to the publisherproduct builder
Approved videos go to your Postiz instance with the platform's synthetic-content flag set, and the month has a spending ceiling.
Platform tokens are somebody's full-time job; do not make it yours.
Files
src/publish/postiz.tsThe disclosure rules do not have a best-effort setting.
Files
src/publish/flags.tstests/flags.test.tsVideo generation bills by the second and the surprise is monthly.
Files
src/cost/ceiling.tssrc/cli/export.ts
done when · tick each as it passeswatch out- Wiring your own client identifiers into the platform APIs to skip the publisher means you now own the platform review process, the token refreshes and the rate limits, and the first thing that breaks is a posting schedule you stopped watching. Let the instance that already holds those credentials do the posting, and keep this code to producing a labelled file and a caption.
That is the whole plan for SocialFaktory. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Direct platform posting or platform developer accounts.
- Voice or likeness cloning from recordings.
- Multi-brand workspaces and team approvals.
- An automated quality gate that decides what is postable.
- pre-approved platform OAuth apps (TikTok/Instagram/YouTube reviews take weeks)
- prompt pipelines tuned per model, format, and tone
- an automated QA pass before anything ships
- multi-brand workspaces and a scheduling calendar
- a flat bill · DIY pays every model invoice directly, and video models are not cheap
- A per-channel caption variant, since the same script reads differently in a feed that shows three lines and one that shows fifteen
- A cost-per-finished-minute report from real renders, which is the only number that tells you whether this is cheaper than the subscription
- A retention rule for media, because finished mp4s accumulate faster than anything else in this build
Need the files? The project pack on the verdict page hands your agent the whole brief · more social media.