Metadata-Version: 2.4
Name: ttclips
Version: 1.0.0
Summary: Fully local TikTok clip reposter: YouTube highlight clips + generated shorts, auto-posted via Playwright.
License-Expression: MIT
Keywords: tiktok,youtube,shorts,clips,automation,yt-dlp,whisper,ffmpeg
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Video
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: yt-dlp>=2024.1.1
Requires-Dist: faster-whisper>=1.0.0
Requires-Dist: google-genai>=1.0.0
Requires-Dist: edge-tts>=6.1.10
Requires-Dist: tiktok-uploader>=1.2.0
Requires-Dist: playwright>=1.58.0
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Dynamic: license-file

# ttclips — clip reposter for TikTok

Fully local, free pipeline: monitors YouTube channels, detects highlights (whisper
+ Gemini), crops clips to vertical 9:16 with **dead-center, word-synced highlight
captions**, and posts to TikTok through Playwright browser automation (cookie auth,
no API approval needed).

## How it works

```
YouTube RSS feeds ─────────▶ monitor.py (queue of new videos)
                                   │
                          yt-dlp + ffmpeg (download, merge + cache reuse)
                                   ▼
                          faster-whisper (transcription + word timestamps)
                                   ▼
                          Gemini (probe → fallback chain) highlight ranking
                                   ▼
                          FFmpeg (crop 9:16 + big centered karaoke captions)
                                   ▼
    2 pipelines:  repost clips        OR        generated original clips
             Gemini highlights  +   Gemini script → edge-tts narration →
             cut_and_crop_clip        stock b-roll + captions
                                      (gradient fallback)
                                   ▼
                          Playwright → TikTok upload (cookies)
```

8 posts/day schedule (`config/posting_window.json`):

| Time | Type |
|------|------|
| 08:00 | repost |
| 10:00 | repost |
| 12:00 | generated |
| 14:00 | repost |
| 16:00 | generated |
| 18:00 | repost |
| 20:00 | generated |
| 22:00 | repost |

## Install as a pip package

```powershell
pip install .            # from a source checkout
ttclips --init           # scaffold config/ + .env.example into the current dir
```

Once published: `pip install ttclips`. The `ttclips` command resolves
`config/` + `data/` from the **current working directory**, or from
`$TTCLIPS_HOME` when that variable is set — so an installed CLI can run from
any empty folder:

```powershell
mkdir D:\ttclips && cd D:\ttclips
ttclips --init
# edit .env (GEMINI_API_KEY), then:
ttclips --monitor
```

Running from a source checkout keeps working unchanged simply by cd-ing to the
repo root. Related console commands: `ttclips-gemini --probe` and
`ttclips-upload` (the internal upload child process).

## Releasing to PyPI (auto via GitHub Actions)

Pushing a tag `vX.Y.Z` automatically builds the wheel + sdist, runs
`twine check`, and publishes to PyPI (`.github/workflows/publish.yml`). A
separate validation workflow (`.github/workflows/ci.yml`) builds + checks the
package on every push to `main` and every PR — no secrets needed.

Release flow:

1. Bump the version in **both** `pyproject.toml` and `ttclips/__init__.py`
   (the release workflow refuses to publish a mismatch).
2. Tag and push:

   ```powershell
   git add . ; git commit -m "Release v0.1.0"
   git tag v0.1.0
   git push origin main --tags
   ```

3. Watch "Publish to PyPI" on GitHub → Actions. Once green, `pip install ttclips` works.

**First-time publisher setup (trusted publishing — no API token needed):**

- PyPI → [Account settings → Publishing](https://pypi.org/manage/account/publishing/)
  → **Add a pending publisher** with:
  - Owner: `kamzysert3` · Repository: `ttclips` · Workflow name: **`publish.yml`**
- The workflow filename must match **exactly** — it's part of the OIDC claim.
- The first successful run creates the `ttclips` project on PyPI automatically.

## Setup (Windows)

1. **Python 3.10+** — verify with `python --version`.
2. `powershell -ExecutionPolicy Bypass -File scripts\setup.ps1`
   - creates `.venv`, installs deps & Playwright chromium
   - installs ffmpeg via winget if missing and adds its bin dir to your PATH so
     yt-dlp's video+audio merge finds it
   - copies `.env.example` → `.env`
3. Edit **`.env`** → set `GEMINI_API_KEY` (free at https://aistudio.google.com/apikey)
   and `PEXELS_API_KEY` (free at https://www.pexels.com/api/ — powers stock
   backgrounds behind generated clips; leave blank and generated clips keep the
   gradient backplate).
4. **TikTok cookies**: log into tiktok.com in Chrome → use the *"Get cookies.txt
   LOCALLY"* extension → export for tiktok.com → save as `data/cookies.txt`.
   Re-export roughly monthly (sessions drift).

## Verify

```powershell
ttclips --validate-session                 # cookies OK?
ttclips --monitor                          # build watch queue
ttclips --once --dry-run                   # full pipeline, no upload
ttclips --once --slot 12:00                # post one slot now
ttclips --once --slot 12:00 --no-upload    # render only
ttclips --post-file "data\clips\video.mp4" # post an already-generated clip
ttclips-gemini --probe                     # ping Gemini model status
```

Closing flags: `--dry-run` = skip upload, `--no-upload` = render but don't post.
`--post-file <path>` (optional `--caption "..."`) uploads a clip that is already
generated, skipping the entire download/transcribe/rank/encode pipeline — handy
for retrying a stuck upload on a finished clip. On success it records a manual
entry in `data/posted.json`.

## Scheduled runs (Windows Task Scheduler)

| Task | Trigger | Action |
|------|---------|--------|
| ttclips-monitor | Daily 06:00 | refresh the watch queue (`--monitor`) |
| ttclips-poster | Hourly 07:00→23:00 | `--once` (posts only at configured slot times) |

The scheduler posts exactly once per slot per day; run `--catchup` after the
machine was off for a while to fill missed slots for today. `scripts/catchup.ps1`
wraps it. When registering a task, set its *Start in* folder to your ttclips
project dir (or define `TTCLIPS_HOME`) so `config/` and `data/` resolve.

## Configuration

- `config/channels.json` — channels to monitor. Fields per channel:
  `name`, `channel_id`, `enabled`, `keywords` (empty = accept all uploads),
  `max_moments` (how many clips to post per video, capped at 5).
- `config/posting_window.json` — the 8 daily slots (`time`, `type`, and a
  `hint` for generated inspiration).
- `config/tiktok.json` — browser/headless/delay/retry settings, default hashtags,
  caption template. Uploads run in an isolated subprocess with retries; waits grow
  exponentially (`retry_wait_initial_sec`, `retry_wait_backoff`,
  `retry_wait_max_sec`) after every failure. `user_agent` must stay a recent
  desktop Chrome UA (the library default Chrome/58 gets TCP-reset by TikTok's
  WAF); `upload_timeout_sec` gives page loads their budget.
- `.env` — secrets + models + styling:
  - `GEMINI_MODEL` + `GEMINI_FALLBACK_MODELS` (comma list) — tried in order; the
    runtime probes candidates at startup and promotes the healthiest model
  - `GEMINI_RETRY_TIMEOUT` — hard cap on retrying a stuck model
  - `WHISPER_MODEL` / `WHISPER_DEVICE`, `TTS_VOICE`, `COOKIES_FILE`
  - `PEXELS_API_KEY` — stock-video b-roll search for generated clips (free key)
  - caption knobs: `CAPTION_FONT_SIZE` (96), `CAPTION_ALIGNMENT` (5),
    `CAPTION_MARGINV`, `CAPTION_OUTLINE`, `CAPTION_SHADOW`,
    `CAPTION_WORDS_PER_LINE`, `HIGHLIGHT_COLOUR` (yellow), `IDLE_COLOUR` (grey)

## Captions

Captions are big (96pt), dead-centered, and karaoke-style: the word currently
being spoken is bold in the highlight colour while the rest of the line stays
grey. Repost clips use real whisper word timestamps; generated clips re-transcribe
their TTS narration with faster-whisper so the karaoke is word-synced too, in
exactly the same style as reposts.

## State

- `data/queue.json` — discovered videos awaiting processing
- `data/posted.json` — dedup history (never reposts the same moment twice)
- `data/downloads/` — merged mp4s (reused on retry, no re-download)
- `data/clips/` — rendered vertical clips
- `data/broll/` — cached stock backgrounds for generated clips
- `data/logs/ttclips.log` — full log

## Resilience

- **Gemini 503/429 overload**: the probe picks a healthy model at startup and the
  fallback chain walks through remaining models; retries use jittered backoff
  bounded by `GEMINI_RETRY_TIMEOUT`. Older flash models (2.x) return 404 for new
  free-tier keys — use the ones listed in `.env.example`.
- **YouTube bot-checks**: intermittent "sign in to confirm you're not a bot". The
  downloader fails loudly; merging is handled by the winget ffmpeg install.
- **Partial failures**: a failed Gemini post step doesn't waste a 150 MB download —
  the cached merged mp4 is reused.
- **B-roll (generated clips)**: backgrounds come from the free Pexels Videos API
  (`PEXELS_API_KEY`), searched with Gemini-chosen keywords, dimmed and cropped to
  9:16. If the search, download, or render fails the clip falls back to an
  animated gradient backplate, so a slot never dies.

## Notes & risks

- **Copyright**: only repost content you have rights to. Clipping public channels
  without permission can violate YouTube ToS / creators' rights. Prefer channels
  that allow embedding/repurposing, or lean on the generated pipeline.
- **TikTok bot detection**: keep `headless: false`; the human-like delays in
  `config/tiktok.json` help. If TikTok serves a CAPTCHA the run fails loudly.
- **UI changes**: selectors in the TikTok uploader may break; pin versions.
- **Free tier limits**: the Gemini free tier handles dozens of calls/day — fine for
  a few daily clips.
