Turn a long YouTube workshop into chapters, YouTube description timestamps, per-chapter transcript JSON, social posts, optional ffmpeg split commands, and optional Typefully drafts with video attached.
Each video is stored in a folder at the project root named after the 11-character YouTube video ID (for example 5eMytPBgmVs/).
- Python 3.10+ (uses type hints and standard library patterns from recent 3.x)
ffmpegon your PATH if you plan to run the generated split commands (not required for transcript/chapter/social steps)- OpenAI API key for chapter detection and social copy (
scripts/create_chapters.py,scripts/create_social_post.py) - YouTube Data API is not used; transcripts come from
youtube-transcript-api - Optional: Oxylabs proxy env vars if YouTube blocks plain transcript requests
- Optional: Typefully API key and matching social set ID in code for
scripts/create_typefully_drafts.py
cd short-video-automationpython3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txtCopy the example env file and edit it:
cp .env.example .env| Variable | Required for | Purpose |
|---|---|---|
OPENAI_API_KEY |
create_chapters.py, create_social_post.py |
OpenAI API |
TYPEFULLY_API_KEY |
create_typefully_drafts.py |
Typefully API |
OXYLABS_USER, OXYLABS_ENDPOINT, OXYLABS_PASSWORD |
youtube.py only, optional |
Proxy for transcript fetches |
scripts/youtube.py and the other scripts load .env from the project root (the parent of scripts/).
Run all commands from the project root so relative paths like VIDEO_ID/ resolve correctly.
Script: scripts/youtube.py
Input: YouTube URL or bare video ID
Output: <VIDEO_ID>/full_transcript.json (Whisper-like shape: text + segments with start, end, text)
python scripts/youtube.py "https://www.youtube.com/watch?v=VIDEO_ID"
# or
python scripts/youtube.py VIDEO_IDScript: scripts/create_chapters.py
Input: Folder containing full_transcript.json (or path to that file)
Output: <VIDEO_ID>/chapters.json (starts, titles, summaries, standalone_post_candidate)
python scripts/create_chapters.py VIDEO_IDScript: scripts/split_transcript_into_chapters.py
Input: full_transcript.json + chapters.json in the same folder
Output:
<VIDEO_ID>/chapters/chapter_0001.json, …<VIDEO_ID>/chapters-table.csv(columns:chapter,start,end,title)
python scripts/split_transcript_into_chapters.py VIDEO_IDScript: scripts/write_youtube_chapters.py
Input: chapters.json
Output: youtube-chapters.txt next to it (each line: HH:MM:SS Title)
python scripts/write_youtube_chapters.py VIDEO_ID/chapters.json
# custom output:
python scripts/write_youtube_chapters.py VIDEO_ID/chapters.json -o VIDEO_ID/my-chapters.txtPaste the contents of youtube-chapters.txt into the video description. YouTube expects the first chapter to start at 00:00:00.
Script: scripts/create_social_post.py
- Batch mode (directory =
chapters/): processes everychapter_*.jsonwherestandalone_post_candidateis"yes", calls OpenAI, and writes the post into thesocial_postcolumn ofchapters-table.csv(column is created if missing).
python scripts/create_social_post.py VIDEO_ID/chapters- Single chapter (one file): prints the post to stdout; optional
--outputfile.
python scripts/create_social_post.py VIDEO_ID/chapters/chapter_0003.json --output /tmp/post.txtOptional: --model (default in script: gpt-5.4-mini), --table in batch mode to point at a non-default CSV path.
Script: scripts/generate_ffmpeg_commands.py
Requirements:
chapters-table.csvmust include asocial_postcolumn (added when you run batchcreate_social_post.py; you can also fill cells manually for chapters you want to clip).youtube-chapters.txtmust already exist in the same folder (create it with Step 4 first).
The script appends to youtube-chapters.txt: a watch URL derived from the parent folder name (treated as video ID), a Commands: heading, and one stream-copy command per row with non-empty social_post:
ffmpeg -ss START -to END -i input.mp4 -c copy chapter_XXXX.mp4
Examples:
# Defaults: ./chapters-table.csv and ./youtube-chapters.txt (run inside VIDEO_ID/)
cd VIDEO_ID
python ../scripts/generate_ffmpeg_commands.py . --input-file ../downloads/my_video.mp4
# From project root:
python scripts/generate_ffmpeg_commands.py VIDEO_ID --input-file /path/to/source.mp4Adjust --input-file to your real source file name/path as printed inside the commands.
Script: scripts/create_typefully_drafts.py
For each CSV row with non-empty social_post, expects chapter_XXXX.mp4 in the same folder as the CSV (or --folder). Uploads media, waits for processing, attaches to an X (Twitter) draft.
export TYPEFULLY_API_KEY=... # or set in .env
python scripts/create_typefully_drafts.py VIDEO_ID/chapters-table.csv --folder VIDEO_ID
python scripts/create_typefully_drafts.py VIDEO_ID/chapters-table.csv --dry-runNote: SOCIAL_SET_ID in the script is set for a specific Typefully social set; change it in scripts/create_typefully_drafts.py to match your account.
VIDEO_ID/
full_transcript.json # from youtube.py
chapters.json # from create_chapters.py
chapters-table.csv # from split + social_post from create_social_post
youtube-chapters.txt # from write_youtube_chapters (+ optional ffmpeg block)
chapters/
chapter_0001.json
chapter_0002.json
...
- Transcript errors / IP blocks: set Oxylabs variables in
.envand retryyoutube.py. generate_ffmpeg_commandssays output file not found: runwrite_youtube_chapters.pyfirst soyoutube-chapters.txtexists.No chapters with non-empty social_post: run batchcreate_social_post.pyonVIDEO_ID/chapters, or addsocial_posttext manually for the rows you need.- Typefully skips uploads: ensure each
chapter_XXXX.mp4exists next to the CSV or in--folder.