TechShorts AI turns a technology topic into a consistent 10–12 second vertical video. It researches the topic, selects a hook, writes and reviews the script, generates narration, synchronizes captions to the spoken words, renders the video with Remotion, validates the finished MP4, and then exports or publishes it on schedule.
The system starts in OFFLINE mode. Follow the local setup first and produce one offline export before connecting any social account.
- How the system works
- Local Windows setup
- Create your first offline video
- Use the dashboard
- Build a content schedule
- Connect YouTube
- Connect TikTok
- Enable publishing
- Deploy with Docker
- Operate and update a deployment
- Configuration reference
- Troubleshooting
flowchart LR
A[Topic or calendar slot] --> B[Research]
B --> C[Hook ranking]
C --> D[Script and quality gate]
D --> E[Voice generation]
E --> F[Word timestamp alignment]
F --> G[Remotion render]
G --> H[MP4 media QA]
H --> I[SEO metadata]
I --> J{Publishing mode}
J -->|Offline| K[Export folder and ZIP]
J -->|Approval required| L[Wait for approval]
L --> M[YouTube and TikTok]
J -->|Automatic| M
The visual system comes from STYLE BIBLE.txt, Style Frame.png, and Grid Plate.png. Those files are required at the project root.
The application has two long-running processes:
serveruns the FastAPI dashboard and API.workerclaims jobs and advances them through the production pipeline.
Both must be running for the dashboard to create and process videos.
Install:
- Python 3.12
- Node.js 22 or newer with npm
- FFmpeg and FFprobe
- Git, if the project is being cloned
Open PowerShell in the project directory and verify each executable:
py -3.12 --version
node --version
npm --version
ffmpeg -version
ffprobe -versionIf FFmpeg was installed while PowerShell was open, close and reopen the terminal before checking again.
From the project root:
py -3.12 -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"If PowerShell blocks activation, the commands can be run without activating the environment:
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"Use the committed lockfile for a reproducible installation:
npm ci
npx remotion browser ensure
npm run video:checkThe browser download happens once. Remotion uses it to render frames without opening a visible browser window.
Copy the example environment file:
Copy-Item .env.example .env
notepad .envAt minimum, configure these values:
APP_ENV=development
APP_URL=http://localhost:8000
APP_TIMEZONE=America/Phoenix
APP_ENCRYPTION_KEY=paste-generated-fernet-key-here
OPENAI_API_KEY=paste-openai-api-key-here
PUBLISHING_MODE=OFFLINEGenerate APP_ENCRYPTION_KEY with:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"Copy the entire output into .env. Do not add quotes or reuse a key from another application.
Create an OpenAI API key at OpenAI API keys. The OpenAI API project must also have active billing. If needed, follow the official prepaid billing setup. A ChatGPT subscription does not configure this API project for the application.
Never commit .env. It is already excluded by .gitignore.
python -m techshorts init-db
python -m techshorts doctorA ready local installation reports OK for:
- Style Bible
- Style Frame
- Grid Plate
- OpenAI key configuration
- FFmpeg and FFprobe
- Node.js
- Remotion
doctor verifies that a key is configured; it does not make a paid OpenAI request or verify the account balance.
In the first PowerShell terminal:
.\.venv\Scripts\Activate.ps1
python -m techshorts serve --host 127.0.0.1 --port 8000Open a second PowerShell terminal in the same project directory:
.\.venv\Scripts\Activate.ps1
python -m techshorts workerKeep both terminals running. Open http://localhost:8000 and confirm:
- OpenAI says Configured.
- Worker says Online.
- FFmpeg says Ready.
- The publishing banner says OFFLINE.
The development API reference is available at http://localhost:8000/api/docs and the health check is http://localhost:8000/health.
Complete this workflow before configuring social publishing.
- Confirm the operating mode is Offline / export.
- Select New production.
- Enter a specific topic, such as
Why the first website had no images. - Select the closest category.
- Leave Schedule empty for an immediate run.
- Select Queue production.
- Watch the job move through Research, Hook, Script, Review, Rendering, SEO, and Exported.
- Select the arrow beside the job to inspect its hook, script, research, sources, metadata, errors, and final video.
- Select Download package after the status becomes
EXPORTED.
A full 1080×1920 render is CPU-intensive. The validation machine took about six minutes for the Remotion render alone. Research, voice generation, and API response time add to that total.
The completed export is also written beneath:
exports/YYYY-MM-DD/<time>_<topic>/
The export contains:
video.mp4metadata.jsonyoutube.txttiktok.txtsources.jsonchecksums.sha256.json- a ZIP containing the complete package
Use the All, Active, and Failed filters to find jobs. Select the arrow on a row to open its production record.
Available actions depend on the job state:
| Action | Purpose |
|---|---|
| Approve | Releases a ready job when APPROVAL_REQUIRED is active |
| Pause | Stops the worker from advancing the job |
| Resume | Returns a paused job to its previous stage |
| Retry | Restarts a failed job from its recorded resume stage |
| Export ZIP | Builds a local package from an already rendered video |
| Download package | Downloads an existing export ZIP |
| Cancel | Permanently stops unfinished pipeline work for that job |
Script and metadata edits are available through the development API at /api/docs. Changing a script invalidates the previous narration, word timing, SEO, and render so the worker cannot publish stale media.
For each job, the worker:
- Researches the topic and records retrieved sources.
- Generates six to ten hooks and scores every candidate.
- Writes a narration-length script.
- Runs the quality gate.
- Generates speech.
- Speeds up narration that slightly exceeds the video window.
- Transcribes the exact generated audio with word timestamps.
- Groups the word timestamps into readable caption cues.
- Renders audio, captions, motion, and category graphics together in Remotion.
- Rejects media without the required dimensions, duration, audio, video, or MP4 container.
- Generates separate YouTube and TikTok metadata.
- Exports or publishes according to the active mode.
- Select Build schedule.
- Choose the first local production date.
- Enter the number of days.
- Enter the number of posts per day.
- Enter one 24-hour posting time for each daily post, separated by commas.
- Select the categories that should rotate through the schedule.
- Optionally enter one topic seed per line. Empty slots receive AI-generated topics.
- Select Create schedule.
Posting windows use APP_TIMEZONE. The application converts them to UTC before storing them. For two posts per day, a valid value is:
09:30, 17:30
If the system is in offline mode, scheduled jobs wait until their due time and then create export packages. They do not contact either platform.
| Mode | Behavior | Recommended use |
|---|---|---|
OFFLINE |
Technically blocks platform publishers and creates local exports | Setup, testing, and review |
APPROVAL_REQUIRED |
Produces the video, then waits for Approve before dispatch | First production publishing mode |
AUTOMATIC |
Dispatches validated jobs when they become due | Only after successful approval-mode tests |
The selected mode is persisted in the database. After the first selection, changing PUBLISHING_MODE in .env does not override the dashboard selection.
Important: The current delivery operation publishes to both YouTube and TikTok. Connect and verify both accounts before selecting
APPROVAL_REQUIREDorAUTOMATIC. A single-platform publishing switch is not currently implemented.
YouTube can be tested with a localhost callback. Use the exact redirect URI shown below unless the dashboard is deployed at a public origin.
- Open the Google Cloud Console.
- Create or select a project.
- Open the YouTube Data API v3 library.
- Select Enable.
-
Configure the project's OAuth consent screen.
-
If the app is in testing mode, add the Google account that owns the target YouTube channel as a test user.
-
Open APIs & Services → Credentials.
-
Create an OAuth client ID with application type Web application.
-
Add this authorized redirect URI for local use:
http://localhost:8000/oauth/youtube/callback -
Copy the client ID and client secret.
Google's official server-side OAuth instructions are available in the YouTube OAuth guide.
Update .env:
YOUTUBE_CLIENT_ID=your-google-client-id
YOUTUBE_CLIENT_SECRET=your-google-client-secret
YOUTUBE_REDIRECT_URI=http://localhost:8000/oauth/youtube/callback
YOUTUBE_CATEGORY_ID=28
YOUTUBE_PRIVACY_STATUS=privateRestart both the dashboard and worker after changing .env.
- Open the dashboard.
- Select Connect YouTube under Systems.
- Sign in with the account that owns the intended channel.
- Grant the
youtube.uploadpermission. - Confirm the dashboard now reports YouTube — Connected.
The application requests only https://www.googleapis.com/auth/youtube.upload. Scheduled uploads are created as private and receive a publishAt time. Public applications may require Google OAuth verification.
TikTok Web Login requires a registered HTTPS callback. Complete the public HTTPS deployment before connecting TikTok. The local http://localhost callback should not be used for a TikTok Web application.
- Sign in to TikTok for Developers.
- Create an app from Manage apps.
- Add Login Kit.
- Add the Content Posting API.
- Configure the Web platform and verify the required website, privacy-policy, and terms URLs.
- Request and obtain access to the
user.info.basicandvideo.publishscopes. - Complete TikTok's sandbox or app-review requirements for Direct Post.
See TikTok's official app creation, Login Kit, and Content Posting API instructions.
For a deployment at https://shorts.example.com, register this exact URI in TikTok:
https://shorts.example.com/oauth/tiktok/callback
The URI must be HTTPS, absolute, static, and free of query parameters or fragments.
Update the deployed .env:
TIKTOK_CLIENT_KEY=your-tiktok-client-key
TIKTOK_CLIENT_SECRET=your-tiktok-client-secret
TIKTOK_REDIRECT_URI=https://shorts.example.com/oauth/tiktok/callback
TIKTOK_PRIVACY_LEVEL=SELF_ONLY
TIKTOK_AUTO_PUBLISH_ACKNOWLEDGED=falseRestart the deployment, open the dashboard, and select Connect TikTok. Complete the consent flow and confirm the dashboard reports TikTok — Connected.
The application queries the creator's allowed privacy levels before posting and rejects an unsupported TIKTOK_PRIVACY_LEVEL. It marks posted content as AI-generated.
Only after TikTok has approved the application and you intentionally accept automatic Direct Post behavior should you set:
TIKTOK_AUTO_PUBLISH_ACKNOWLEDGED=trueRestart the worker after changing this value.
Do not begin with automatic mode.
- Confirm OpenAI, YouTube, TikTok, Worker, and FFmpeg all show healthy or connected.
- Confirm
TIKTOK_AUTO_PUBLISH_ACKNOWLEDGED=trueon the worker. - Change Operating mode to Approval required.
- Create one unscheduled test production.
- Wait until the job is
READY. - Inspect the video, script, sources, YouTube metadata, and TikTok caption.
- Select Approve.
- Monitor the job until it becomes
PUBLISHED. - Verify the result on both platforms.
- Repeat with a future schedule before considering Automatic mode.
Publishing attempts use per-job/per-platform idempotency keys. If YouTube succeeds but TikTok fails, retrying the job does not intentionally create a second YouTube upload.
The included Docker Compose configuration runs one web container and one worker container with shared persistent volumes. This is the recommended deployment shape for the current SQLite architecture.
Use a Linux server or VM with:
- Docker Engine and the Docker Compose plugin
- At least 4 GB of RAM; more CPU cores improve rendering time
- Persistent disk space for the database, narration, videos, and exports
- A domain name pointing to the server
- An HTTPS reverse proxy such as Caddy, Nginx, or a managed load balancer
Use Docker's official Compose installation instructions if Compose is not available.
Verify the server:
docker --version
docker compose versionClone the repository or securely copy the project directory to the server, then change into it:
git clone <repository-url> techshorts
cd techshortsThe three brand assets must exist at the project root before building the image.
cp .env.example .env
docker run --rm python:3.12-slim python -c "import base64,secrets; print(base64.urlsafe_b64encode(secrets.token_bytes(32)).decode())"Edit .env and set at least:
APP_ENV=production
APP_URL=https://shorts.example.com
APP_TIMEZONE=America/Phoenix
APP_ENCRYPTION_KEY=paste-generated-fernet-key-here
DASHBOARD_USERNAME=choose-a-non-default-username
DASHBOARD_PASSWORD=use-a-long-random-password
DATABASE_URL=sqlite:///./data/techshorts.db
DATA_DIR=./data
MEDIA_DIR=./media
EXPORT_DIR=./exports
PUBLISHING_MODE=OFFLINE
OPENAI_API_KEY=paste-openai-api-key-here
YOUTUBE_REDIRECT_URI=https://shorts.example.com/oauth/youtube/callback
TIKTOK_REDIRECT_URI=https://shorts.example.com/oauth/tiktok/callbackAlso add the platform credentials described above. Keep PUBLISHING_MODE=OFFLINE and TIKTOK_AUTO_PUBLISH_ACKNOWLEDGED=false for the first deployment.
Production startup refuses to run without dashboard credentials and APP_ENCRYPTION_KEY.
docker compose config --quiet
docker compose up -d --build
docker compose psThe first build downloads Python packages, npm packages, and Remotion's headless browser. It can take several minutes.
Watch startup logs:
docker compose logs -f web
docker compose logs -f workerStop following logs with Ctrl+C; this does not stop the containers.
The application listens on port 8000 and does not terminate TLS itself. Put it behind a reverse proxy before adding production OAuth callbacks.
A minimal host-installed Caddy configuration is:
shorts.example.com {
reverse_proxy 127.0.0.1:8000
}Configure the server firewall so port 8000 is not exposed publicly once the reverse proxy is active. Public traffic should use ports 80 and 443 through the proxy.
curl https://shorts.example.com/health
docker compose psExpected health output includes:
{"status":"ok","database":true,"ffmpeg":true,"ffprobe":true,"node":true,"remotion":true}Open https://shorts.example.com. The browser should request the production dashboard username and password.
Register these exact callback URIs with the platform applications:
https://shorts.example.com/oauth/youtube/callback
https://shorts.example.com/oauth/tiktok/callback
Update the corresponding .env values, restart the services, and use the dashboard connection buttons:
docker compose up -d --force-recreate web workerCreate and download one offline video from the production deployment. Check its voice, caption timing, category graphics, metadata, and ZIP before enabling platform publishing.
docker compose ps
docker compose logs --tail=200 web
docker compose logs --tail=200 worker
docker compose logs -f workerdocker compose restart web workergit pull
docker compose up -d --build
docker compose psThe application runs Alembic migrations at service startup.
docker compose downThis stops containers but preserves the named volumes. Do not add --volumes unless permanent deletion of the database, media, OAuth credentials, and exports is intended.
Back up all four named volumes shown by:
docker volume lsThe volumes contain:
- SQLite database and encrypted OAuth records
- generated and final media
- offline exports
- temporary Remotion narration assets
Stop the worker or take a provider-level consistent volume snapshot while backing up the SQLite database. Keep the matching APP_ENCRYPTION_KEY in a separate secret manager; OAuth records cannot be decrypted without it.
SQLite is appropriate for the included one-web/one-worker deployment on a single host. Do not add parallel worker replicas while using SQLite. Move DATABASE_URL to a properly supported external database before horizontal scaling; that migration is not included in this version.
.env.example is the canonical list of settings.
| Setting | Required | Purpose |
|---|---|---|
APP_ENV |
Yes | development or production |
APP_URL |
Yes | Canonical public origin; keep the explicit OAuth redirect settings aligned with it |
APP_TIMEZONE |
Yes | IANA timezone used for calendar input |
APP_ENCRYPTION_KEY |
OAuth/production | Fernet key for stored OAuth credentials |
DASHBOARD_USERNAME |
Production | Enables dashboard HTTP Basic authentication |
DASHBOARD_PASSWORD |
Production | Password paired with the dashboard username |
DATABASE_URL |
Yes | SQLAlchemy database URL |
PUBLISHING_MODE |
Yes | Initial mode before a database value is persisted |
| Setting | Default | Purpose |
|---|---|---|
OPENAI_API_KEY |
Empty | Research, writing, review, voice, and transcription authorization |
OPENAI_UTILITY_MODEL |
See .env.example |
Research and utility workload |
OPENAI_CREATIVE_MODEL |
See .env.example |
Hooks, scripts, and creative workload |
OPENAI_REVIEW_MODEL |
See .env.example |
Quality-gate workload |
OPENAI_TTS_MODEL |
tts-1 |
Narration synthesis |
OPENAI_TTS_VOICE |
onyx |
Narration voice |
OPENAI_TRANSCRIPTION_MODEL |
whisper-1 |
Word timestamp alignment |
If the configured account cannot access a model, replace that model value with one available to the OpenAI project and restart the services.
| Setting | Default | Purpose |
|---|---|---|
VIDEO_WIDTH |
1080 |
Output width |
VIDEO_HEIGHT |
1920 |
Output height |
VIDEO_DURATION_SECONDS |
12 |
Composition duration; accepted range is 10–20 |
VIDEO_DURATION_TOLERANCE_SECONDS |
0.35 |
Maximum duration deviation during QA |
NARRATION_END_PADDING_SECONDS |
0.25 |
Silence reserved after speech |
FFMPEG_BINARY |
ffmpeg |
FFmpeg executable or full path |
FFPROBE_BINARY |
ffprobe |
FFprobe executable or full path |
NODE_BINARY |
node |
Node.js executable or full path |
Open the Remotion editor:
npm run video:studioType-check the composition:
npm run video:checkRender the bundled silent-preview properties:
node video/render.mjs video/preview-props.json media/preview.mp4Production jobs do not use evenly distributed preview timing. They synthesize narration first and use timestamps transcribed from that exact audio file.
Run before deploying code changes:
python -m ruff check techshorts tests
python -m pytest
npm run video:check
python -m pip checkThe automated tests cover configuration guards, state transitions, duplicate topics, retry leases, persistent publishing modes, offline blocking, platform request mapping, calendar idempotency, word-to-caption cues, export integrity, dashboard APIs, and security headers.
The OpenAI API project does not have active billing or usable credits. Add billing in the OpenAI Platform, wait a few minutes for the account to update, and retry the failed job from the dashboard.
Confirm the worker terminal or container is running:
python -m techshorts workerDocker:
docker compose ps
docker compose logs --tail=200 workerReopen the terminal after installing FFmpeg, or set the full executable paths:
FFMPEG_BINARY=C:/path/to/ffmpeg.exe
FFPROBE_BINARY=C:/path/to/ffprobe.exeThen run python -m techshorts doctor again.
npx remotion browser ensureIf Windows reports spawn EPERM, run the render from a normal local PowerShell session and check whether security software is blocking node_modules/.remotion or esbuild child processes.
A 360-frame 1080×1920 render can use all available CPU for several minutes. Check Task Manager or docker stats; active Chromium CPU usage normally means rendering is progressing.
The URI in .env must match the platform console exactly, including scheme, hostname, port, path, and trailing slash behavior. Restart the web service after changing it.
TikTok Web Login requires HTTPS. Deploy the application behind HTTPS and register the public /oauth/tiktok/callback URI.
The selected TIKTOK_PRIVACY_LEVEL is not permitted for the connected creator. Change it to one returned by TikTok's creator-information endpoint, restart the worker, and retry.
The topic is too similar to recent history. Use a materially different angle. Explicit series labels such as Part 1 and Part 2 are allowed as separate jobs.
Start the local server on another port and update APP_URL plus OAuth callbacks:
python -m techshorts serve --host 127.0.0.1 --port 8010Non-retryable errors such as missing billing, invalid credentials, failed quality gates, or unsupported privacy settings require operator action. Correct the configuration, restart affected services, and select Retry on the failed job.
- Keep
.env, OAuth client secrets, API keys, andAPP_ENCRYPTION_KEYout of source control. - Use HTTPS before exposing the dashboard or OAuth callbacks publicly.
- Keep
OFFLINEmode active until both social accounts are connected and tested. - Use a unique, long dashboard password in production.
- Preserve
APP_ENCRYPTION_KEYacross deployments and restores. - Review platform permissions and disconnect unused OAuth applications.
- Review this README whenever the pipeline, dashboard controls, environment variables, OAuth scopes, or deployment topology changes.
No YouTube or TikTok post is made during installation or testing. Connecting an account stores authorization, but publishing still requires a non-offline mode and, in approval mode, an explicit operator approval.