* Adopt uv for Python dependency management Replace manual pip/venv setup with `uv sync` (pyproject.toml + uv.lock), and switch all tool invocations across docs, skills, commands, and examples to `uv run`. Optional extras: --extra whisper, --extra modal. tools/requirements.txt is kept as a pip fallback. * FIX: bring uv migration current with main — post-June deps and docs Adds websocket-client (60db websocket transport) to core deps and a 'youtube' extra for the google-* packages (tools/youtube_upload.py), regenerates uv.lock, bumps pyproject version to 0.18.0, and converts the 51 'python3 tools/…' invocations added since the PR was opened (60db, YouTube, Kiro docs, publish command) to 'uv run'. Verified with uv sync --frozen --extra youtube; voiceover, sixtydb_tts, youtube_upload and migrate_to_kiro all run through the project environment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Asharib Ali <asharibarain4@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
11 KiB
Uploading to YouTube
tools/youtube_upload.py uploads a rendered video to YouTube using the YouTube Data API
v3. The /publish command wraps it with metadata auto-filled from a project's project.json.
The one thing to know up front: YouTube uploads act on behalf of a channel, so they require OAuth 2.0, not an API key. There is no key-only path. The setup below is a one-time ~10-minute click-through in Google Cloud Console.
Fast path with Claude Code
Most of this setup is wiring Claude can do for you — the only parts that require a human are the Google Cloud Console clicks and the one browser consent. Split the work like this:
You do (in the browser, ~5 min) — three things, in this order:
- Enable the API. Pick/create a Cloud project, then APIs & Services → Library →
"YouTube Data API v3" → Enable. ⚠️ Don't skip this — creating the OAuth client does
not enable the API, and a missing enable surfaces only at upload time as
HTTP 403 accessNotConfigured. After enabling, give it 1–2 minutes to propagate. - Add yourself as a Test user. APIs & Services → OAuth consent screen (Google's newer UI calls this "Google Auth Platform" → Audience tab). Set User type: External, keep Publishing status: Testing, then under Test users → Add users add the exact Google account you'll log in with, and Save. ⚠️ If you skip this you get a hard "Access blocked: … has not completed the Google verification process" with no way through. (When youare a test user you instead get a softer "Google hasn't verified this app → Advanced → Continue" screen — that one is expected; click through it.)
- Create a Desktop OAuth client and download it. APIs & Services → Credentials → Create Credentials → OAuth client ID → Application type: Desktop app → Create → Download JSON.
Then hand the rest to Claude. Tell it where the downloaded client_secret_*.json is, and
ask it to finish YouTube setup. Claude will: move the secret somewhere stable out of the repo,
add YOUTUBE_CLIENT_SECRETS_FILE to .env, install the google-* deps into .venv, launch
the --auth browser login (you click "Allow" once), confirm with a --dry-run, and run a
private test upload to verify the round-trip.
Two account gotchas worth stating up front:
- Brand Accounts: if your channel is a Brand Account, log in with the personal Google account that manages it (add that email as the Test user). The upload still lands on the brand channel.
- Exact-match: the email you add as a Test user must be byte-for-byte the account you pick in the consent screen.
The detailed manual version of all of this follows.
One-time setup
1. Create a Google Cloud project + enable the API
- Go to console.cloud.google.com and create (or pick) a project. Confirm the project selector in the top bar shows the project you mean — operating in the wrong project is the most common source of confusing failures here.
- APIs & Services → Library → "YouTube Data API v3" → Enable.
- This is mandatory and easy to forget. If it's not enabled, auth and
--dry-runboth succeed, but the real upload fails withHTTP 403 accessNotConfigurednaming this exact project. After enabling, wait 1–2 minutes for it to propagate before retrying.
- This is mandatory and easy to forget. If it's not enabled, auth and
2. Configure the OAuth consent screen
- APIs & Services → OAuth consent screen (newer UI: "Google Auth Platform" → Audience).
- User type External → fill in app name + your email. Leave Publishing status: Testing.
- On Test users → Add users, add the Google account that owns the YouTube channel (a Brand
Account's managing personal account if applicable), and Save.
- While the app is in "Testing", only listed test users can authorize it — a non-listed account gets a hard "Access blocked … verification process" with no override. Refresh tokens also expire after ~7 days in Testing (see Gotchas).
3. Create the OAuth client
- APIs & Services → Credentials → Create Credentials → OAuth client ID.
- Application type: Desktop app.
- Download the
client_secret_*.json.
4. Point the toolkit at it
echo 'YOUTUBE_CLIENT_SECRETS_FILE=/absolute/path/to/client_secret.json' >> .env
5. Install dependencies
The three google-* packages are opt-in — they're commented out in
tools/requirements.txt so base installs stay lean, so install them explicitly:
pip install google-api-python-client google-auth-oauthlib google-auth-httplib2
# (or uncomment the YouTube block in tools/requirements.txt and re-run the -r install)
The tool prints this exact command if you run it before the deps are present.
6. Log in once
uv run tools/youtube_upload.py --auth
A browser opens for consent. On success the refresh token is cached at
_internal/.youtube/token_default.json (chmod 600, gitignored). Every later upload reuses it
silently — no browser needed.
Usage
# Upload privately (the safe default)
uv run tools/youtube_upload.py \
--video out/video.mp4 \
--title "My video title" \
--description-file DESCRIPTION.md \
--tags "ai,agents,explainer" \
--category 28 \
--json-out
# Schedule a public go-live (works for first-party uploads — see Gotchas re: the audit)
uv run tools/youtube_upload.py --video out/video.mp4 --title "My video" \
--publish-at 2026-06-10T09:00:00Z --thumbnail out/thumb.png
# Validate everything without uploading (also reports whether auth is ready)
uv run tools/youtube_upload.py --video out/video.mp4 --title "Test" --dry-run --json-out
Prefer /publish for finished projects — it derives the title/description/tags from
project.json, runs a dry-run, then uploads and writes the resulting video URL back.
Key flags
| Flag | Notes |
|---|---|
--video, --input |
The file to upload (required) |
--title |
Required, ≤100 chars |
--description / --description-file |
Inline text, or a file (- = stdin); ≤5000 chars |
--tags |
Comma-separated; combined length ≤500 chars (overflow is dropped with a warning) |
--category |
Numeric ID string. 22 = People & Blogs, 28 = Science & Tech |
--privacy |
private (default) / unlisted / public |
--publish-at |
ISO8601 UTC, e.g. 2026-06-10T09:00:00Z. Forces private at insert, flips public at the time. |
--thumbnail |
Custom thumbnail (≤2MB, 1280×720). Requires a phone-verified channel. |
--captions + --captions-language |
Caption file (.srt/.vtt) + language code |
--playlist |
Playlist ID to add the video to |
--account |
Namespaces the cached token — use for multiple channels (e.g. --account work) |
--auth |
Interactive login only; caches a token and exits |
--dry-run |
Builds + prints the request body and checks auth, but uploads nothing |
--json-out |
Emits one machine-readable JSON line on stdout |
JSON output (--json-out)
Success:
{"success": true, "videoId": "abc123", "url": "https://www.youtube.com/watch?v=abc123",
"privacyStatus": "private", "requestedPrivacy": "public", "publishAt": "2026-06-10T09:00:00Z",
"account": "default", "thumbnailSet": true, "captionsAdded": false, "playlistId": null,
"uploadStatus": "uploaded"}
Failure:
{"success": false, "error": "...", "errorType": "config", "errorReason": "accessNotConfigured", "videoId": null}
errorType ∈ auth | quota | config | forbidden | validation | upload | http | thumbnail | captions.
For HTTP errors, errorReason carries YouTube's machine reason (e.g. accessNotConfigured,
quotaExceeded, forbidden) so a disabled API (config) is distinguishable from a real quota
wall (quota) or a permission denial (forbidden) — all three are HTTP 403.
Compare requestedPrivacy vs privacyStatus to detect the unverified-app force-to-private case.
Quota
- Default quota: 10,000 units/day per Google Cloud project.
videos.insert≈ 1,600 units → about 6 uploads/day.thumbnails.set≈ 50,captions.insert≈ 400,playlistItems.insert≈ 50.- Exceeding it returns HTTP 403
quotaExceeded(errorType: "quota"). Request more via the Google Cloud quota form if you need higher volume.
Gotchas
- Unaudited-project private lock (often a non-issue for first-party uploads). Google's
docs say videos uploaded via
videos.insertfrom unaudited API projects (created after 2020-07-28) are restricted toprivate. In practice this targets third-party apps uploading to other people's channels — when the Cloud project owner, the channel, and the authenticated account are all you (a first-party upload), it generally isn't enforced and public/scheduled uploads go live fine (verified empirically on this toolkit). If it does bite (e.g. uploading to a different channel via--account), lift it by completing the YouTube API compliance audit (publish the app to production, then submit the audit form). Either way the tool reports the actual returned privacy — treat that as the source of truth. - 7-day refresh-token expiry in "Testing". Consent screens left in Testing mode expire
refresh tokens after ~7 days. If an unattended run fails with
errorType: "auth", just re-runuv run tools/youtube_upload.py --auth. Publishing the consent screen removes this (but keeps the verification private-lock until verified). - Headless servers.
--authneeds a browser + reachable localhost. Run it once on a machine with a browser, then copy_internal/.youtube/token_<account>.jsonto the server. publishAtrequires private at insert — the tool enforces this automatically.- Custom thumbnails require the channel to have a verified phone number, else a 403 (non-fatal — the video still uploads; the tool logs a warning).
- The token file is a secret.
_internal/.youtube/is gitignored; don't commit it.
Troubleshooting
| Symptom | Fix |
|---|---|
Missing dependency: ... |
pip install google-api-python-client google-auth-oauthlib google-auth-httplib2 |
OAuth client secrets file not found |
Set YOUTUBE_CLIENT_SECRETS_FILE in .env (absolute path) or pass --client-secrets |
No cached credentials |
Run uv run tools/youtube_upload.py --auth once |
Refresh token ... is invalid |
Re-run --auth (7-day Testing expiry or revoked token) |
errorType: "quota" |
Daily quota hit — wait or request more quota |
Video uploaded but stays private |
Unverified-app lock — verify your OAuth app, or publish manually |
access_denied in the browser |
The Google account isn't a Test user on the consent screen — add it |