# ADR-109 VideoSmith — Isolated PWA Wrapping an Internal-Only Third-Party Engine, With the AI Chain Owned by the Wrapper

## Status

Accepted, 2026-07-03 (built and live-verified same day).

## Status History

```yaml
status_history:
  - date: 2026-07-03
    status: Accepted
    changed_by: hkl
    reason: >
      VideoSmith Phase 1 was designed, built, deployed, and live-verified
      end-to-end the same day. Recorded as Accepted directly (not Proposed)
      because the decision was already implemented and tested before this
      ADR was written, per user's explicit "start building" instruction and
      the interactive resolution of all open design questions beforehand
      (see MDD_videosmith.md §8).
    changed_via: manual (written after build, documenting a decision already
      implemented and verified — the adr-generator agent referenced in prior
      memory notes was not found on disk in this session; ADR authored
      directly against docs/adr/ADR-template.md instead)
  - date: 2026-07-03
    status: Accepted (amended)
    changed_by: hkl
    reason: >
      Feature parity expansion to match Blogsmith's fuller feature set (ADR-097):
      batch/multi-topic submission, a per-user schedule cursor (direct port of
      Blogsmith's scheduler_worker()/compute_next_slot() pattern), a draft →
      scheduled/published/unpublished distribution phase, and real auto-posting
      via Upload-Post (https://api.upload-post.com), since MoneyPrinterTurbo's
      own upload_post_* config keys turned out to be unimplemented dead config
      (verified by reading its full source). Two intentional deviations from
      Blogsmith's exact shape: (1) publish state lives in a separate
      jobs.publish_status column rather than being folded into jobs.status,
      because VideoSmith's render phase already has more states than
      Blogsmith's and reusing 'drafting' for two different meanings would be
      ambiguous; (2) the multi-topic UI is a removable chip/tag input with
      paste-splitting and a live counter, not Blogsmith's plain one-per-line
      textarea — explicitly requested as an improvement. Upload-Post's API
      contract was verified live (not just from docs): a deliberately-fake API
      key produced a genuine HTTP 401 from api.upload-post.com, confirming the
      request shape is correct. Real auto-posting still requires the user to
      create their own upload-post.com account (account linking happens on
      their dashboard, not via this API) — same shape as the Pexels-key
      dependency from Phase 1; until then every completed video lands in
      publish_status='draft', verified live.
    changed_via: manual (same session as initial build)
  - date: 2026-07-03
    status: Accepted (amended)
    changed_by: hkl
    reason: >
      Single-account Upload-Post design replaced with multi-channel support.
      User caught a real capability gap from a live screenshot: the Settings
      panel showed TikTok/Instagram/YouTube checkboxes as if they were
      independent destinations, but the schema stored exactly ONE
      upload_post_username/upload_post_api_key on `users` — the checkboxes only
      selected which platforms of that single account to post to. Verified
      against Upload-Post's real docs (docs.upload-post.com) that they support
      13 platforms (not just 3) via a "one API key, multiple named profiles"
      architecture; the user explicitly chose full independence instead — each
      VideoSmith "channel" now holds its own separate upload_post_api_key +
      upload_post_username + platform selection, not a shared account. New
      `upload_post_channels` table (one row per channel, FK to `users`,
      `is_default` flag with app-enforced exclusivity); `jobs.channel_id` FK
      records which channel a job used/will use. Old `users.upload_post_*`
      columns dropped after a one-time backfill into a "Default" channel per
      user (migrate_videosmith_v3.sql — no data existed to backfill in
      practice, verified via SQL before dropping). Channel selection scope
      decided as: per single-topic job (dropdown, defaults to the user's
      default channel); batch/scheduled jobs use the default channel
      automatically (no per-topic picker in v1 — user did not respond to that
      specific sub-question, proceeded with the documented-as-recommended,
      most-contained option). The platform checkbox list in Settings is now
      rendered from the server's `available_platforms` API response (backed by
      a Python constant), not hardcoded in HTML, satisfying the user's explicit
      "channel list may grow, can't be hardcoded" requirement.
    changed_via: manual (same session, in response to a user-caught UI/schema
      mismatch)
  - date: 2026-07-06
    status: Accepted (amended)
    changed_by: hkl
    reason: >
      Added a second job type: "highlight video" assembled from the user's own
      event photos/clips, instead of an AI script driving Pexels stock
      footage. Ran /scout first (house convention for non-trivial new
      capability) — found MoneyPrinterTurbo (the same mpt-api container this
      ADR already wraps) has an existing, never-used video_source:"local"
      pipeline: preprocess_video() (photo-to-Ken-Burns-clip conversion,
      material validation) and combine_videos() (subclipping, concatenation,
      transitions, aspect normalization, BGM mixing) — all via moviepy 2.2.1 +
      ffmpeg, already installed in mpt-api. Zero new video-processing
      infrastructure needed; reused as-is. The one real gap MPT doesn't fill
      (no "best moment" awareness, only naive fixed-length chunking) is
      addressed with PySceneDetect (BSD-3, CPU-only, pip-installable) splitting
      raw clips at real scene boundaries first — user explicitly chose to pull
      this into v1 rather than deferring it to v2 as originally scouted.
      Rejected auto-editor (wrong problem — silence-cutting) and ML "best
      moment" scoring tools (VideoHighlighter, AI-Youtube-Shorts-Generator —
      need GPU/CUDA or an external LLM API, incompatible with this 2-core/
      no-GPU box). New infra: videosmith-app gained ffmpeg+scenedetect and a
      new shared volume with mpt-api's storage/ mount (deliberately NOT
      touching resource/ — that's where the Phase-1 font-shadowing bug lives,
      left un-mounted on purpose). Two real constraints inherited from MPT's
      design, documented rather than silently accepted: (1) original clip
      audio is always discarded (MPT's materials stage is video-only by
      design) — output is real visuals + chosen BGM/silence, not the event's
      own ambient sound; (2) combine_videos()'s target duration is driven by a
      "primary audio" track, so a silent placeholder of the computed total
      material duration (capped at 90s) is generated and passed as
      custom_audio_file to control length without adding unwanted narration.
      Full pipeline verified live: a real synthetic 2-scene clip + photo
      rendered correctly (frame-level color check confirmed sequential order,
      portrait aspect, real BGM audio track), and a real browser session
      confirmed both the happy path (UI mechanics) and the graceful-failure
      path (an invalid upload failed cleanly with a clear message, not a
      hang or crash). See MDD_videosmith.md §17 for the full scout writeup
      and verification detail.
    changed_via: manual (same session, /scout-driven — user directly
      requested exploring existing tools/stack before building)
```

## Context

360LM wanted a repeatable topic → short-form-video generation capability, explicitly framed by the user as "similar to BlogSmith, another automation PWA" — i.e., wrap an existing GitHub tool (here, [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo), MIT-licensed, script→TTS→stock-footage→subtitle→render pipeline) the same way Blogsmith wraps WordPress's REST API.

Two prior decisions already govern parts of this shape:
- **ADR-086** (Isolated PWA Architecture) — defines when an app must be standalone (own DB, own auth, own domain) vs. ERP-integrated.
- **ADR-062** (Live AI Pipeline Contract) — mandates every live AI feature use a 3-tier auth/fallback chain (Claude OAuth → OpenRouter → Anthropic REST) with a `need_api_permission` escape hatch.

Neither ADR anticipated wrapping a third-party FastAPI service that has **its own** LLM-calling logic built in. MoneyPrinterTurbo (MPT) supports 25+ LLM providers natively, but only as a single configured provider — it does not implement ADR-062's tiered fallback, and its own auth (an `x-api-key` header check) is present in source but disabled by default (`app/controllers/v1/video.py`, dependency commented out).

Resolved via interactive Q&A with the user (2026-07-03, one question at a time, see MDD_videosmith.md §8):
1. Users are an isolated content/automation team, not hub-authenticated 360LM employees → ADR-086 isolated pattern applies, not ERP-integrated.
2. ADR-062's full tiered chain is required for script generation, not MPT's native single-provider config.
3. Output is manual MP4 download only for now; auto-publish to social platforms is explicitly parked.
4. Single-concurrency (1 job at a time) is accepted given the VPS's 2-core constraint.

## Decision

**VideoSmith is built as an isolated PWA (per ADR-086) whose backend embeds the ADR-062 three-tier AI chain directly, and treats MoneyPrinterTurbo purely as an internal, unauthenticated rendering engine — never exposed publicly, never trusted with LLM provider selection.**

Concretely:
1. **Own DB, own auth, own domain (live).** `videosmith` Postgres database, `videosmith_app` role (zero access to `lm360`/`lm360_prod`), PIN-based bcrypt auth (`pgcrypto`), Bearer session tokens (30-day expiry) — mirrors Blogsmith/Health Tracker exactly. Traefik label routes `videosmith.srv1111289.hstgr.cloud` (`zerossl` resolver) — this subdomain resolves via Hostinger's own wildcard DNS for the VPS hostname (same mechanism Blogsmith uses), so it has been publicly reachable since the container started; no DNS action was ever required. (Distinct from `360dlm.in`, a separate custom domain from an earlier abandoned direct-deployment plan, which genuinely has zero DNS records and remains on hold per the user.)
2. **ADR-062 chain is embedded in the wrapper process, not a separate proxy container.** Unlike `counter-ai`/`print-ai-proxy` (standalone sidecars), `videosmith_app.py` implements Tier 1 (`claude -p` subprocess, OAuth) → Tier 2 (OpenRouter, 4-model cascade) → Tier 3 (Anthropic REST) directly, matching Blogsmith's own precedent of embedding rather than sidecar-ing AI logic for a single-purpose isolated app. This avoids running a second container for one caller.
3. **MoneyPrinterTurbo never generates the script or the search terms itself.** VideoSmith generates both via its own chain and populates MPT's `video_script`/`video_terms` request fields — verified live (2026-07-03) that MPT's `generate_script`/`generate_terms` steps then use the supplied text verbatim instead of calling out to any LLM provider. MPT's own `llm_provider` config setting is therefore inert for this pipeline by design, not by accident.
4. **MPT runs internal-only.** `mpt-api` container has no Traefik label, is reachable only via Docker DNS (`http://mpt-api:8080`) from `videosmith-app` on the shared `root_default` network. Its own disabled auth is not relied upon — network isolation is the actual security boundary (per audit finding, see References).
5. **VideoSmith, not MPT, owns failure detection for stuck renders.** Verified live: MPT does not flip a task's state to failed when an exception is raised inside its own background render thread (e.g., missing Pexels key) — the task sits at `state: 4` (processing) forever. VideoSmith's own 20-minute job timeout in `process_job()` is therefore the only mechanism that will ever mark such a job `failed`. This is treated as a permanent property of wrapping MPT, not a bug to fix upstream.

**Decision Maker:** hkl (via interactive Q&A, 2026-07-03)

## Implementation Notes

- **Files:** `/var/www/Others/Automation/videosmith/videosmith_app.py` (auth + embedded AI chain + MPT integration + HTTP server), `public/index.html` (PWA), `migrations/migrate_videosmith_v1.sql` (schema), `docker-compose.yml` (both containers), `.env` (mode 600 — reuses platform's existing `ANTHROPIC_API_KEY`/`OPENROUTER_API_KEY`, no new secret provisioned).
- **Key identifiers:** `JOB_LOCK` (threading.Lock, enforces single-concurrency alongside MPT's own `max_concurrent_tasks=1` in `config.toml`); `generate_script()` (the 3-tier chain entry point); `MPT_STATE_FAILED=-1`/`MPT_STATE_COMPLETE=1`/`MPT_STATE_PROCESSING=4` (MPT's numeric task states, from its `app/models/const.py`); `recover_orphaned_jobs()` (fails in-flight jobs on container restart — verified live).
- **MPT API gotchas found only by hitting the live service** (not discoverable from its docstrings alone): real routes are under `/api/v1/...` (e.g. `POST /api/v1/videos`, `GET /api/v1/tasks/{id}`) — a plain `/tasks/{id}/...` path exists but is a *different* thing, a static-file mount for serving finished videos. `video_aspect` requires literal `'9:16'`/`'16:9'`/`'1:1'`, not `portrait`/`landscape`. Full detail: MDD_videosmith.md §13.
- **Find all sites:** `grep -n "MPT_API_BASE\|api/v1" /var/www/Others/Automation/videosmith/videosmith_app.py`

## Alternatives Considered

- **Expose MoneyPrinterTurbo's own Streamlit WebUI + FastAPI directly to users (original plan before this ADR).** Rejected: MPT's auth is disabled by default and its UI is a dev tool, not a 360LM-quality client experience; also the user explicitly asked for a Blogsmith-style wrapper instead once it was framed that way.
- **Run the ADR-062 chain as a separate `videosmith-ai-proxy` sidecar, matching `counter-ai`/`print-ai-proxy`.** Rejected in favor of embedding directly in the wrapper process: those proxies serve multiple independent PWA callers, justifying a standalone service; VideoSmith has exactly one caller (itself), so a sidecar would just be an extra container and network hop with no reuse benefit — Blogsmith's own precedent (embedded, not sidecar) was followed instead.
- **Let MPT use its own configured LLM provider (`openai` in `config.toml`) instead of bypassing it.** Rejected: the user explicitly chose full ADR-062 compliance (resilient 3-tier fallback) over MPT's native single-provider model, and MPT has no such fallback built in.
- **ERP-integrated (hub PIN auth) instead of isolated.** Rejected per ADR-086 criteria and explicit user confirmation that VideoSmith's users are the automation/content team, matching Blogsmith's existing precedent, not hub-authenticated employees.

## Consequences

**Positive:**
- VideoSmith gets ADR-062's resilience (3-tier fallback, `need_api_permission` escape hatch pattern available if needed) that MoneyPrinterTurbo could never provide natively.
- MPT's disabled-by-default auth is a non-issue because it's never network-reachable from outside `root_default`.
- Single embedded process (no sidecar) keeps deployment as simple as Blogsmith's, easy for a future maintainer to reason about.

**Negative / Trade-offs:**
- MPT's own 25-provider LLM configuration surface (`config.toml`'s `llm_provider` and 25 provider blocks) is entirely dead weight for this pipeline — a future reader of `config.toml` could reasonably assume it's active and be wrong. Mitigated by the explicit note in `config.toml`'s `max_concurrent_tasks` line and this ADR.
- VideoSmith must carry MPT's failure-detection burden itself (the 20-minute timeout) because MPT will never self-report certain failure classes. If MPT's upstream behavior changes (e.g., adds proper exception-to-failed-state handling), this becomes redundant but harmless.
- Embedding the AI chain in the wrapper (rather than a shared sidecar) means any future second caller of the same chain would need its own copy or a refactor into a shared proxy — acceptable now (one caller), revisit if a second caller appears.

**Risks and mitigations:**

| Risk | Mitigation |
|---|---|
| A future edit to MPT's upstream image changes its route prefix or `video_aspect` enum silently | `docker-compose.yml` pins `ghcr.io/harry0703/moneyprinterturbo:latest` (not a fixed tag) — a silent breaking change is possible on next pull. Mitigation: any future `docker compose pull` for this service should be followed by the exact live smoke test done during this build (register→login→submit→poll to completion) before trusting it. |
| MPT's disabled auth gets accidentally enabled or the container gets a Traefik label added later without re-reviewing this ADR | This ADR's Decision section states network isolation is the real boundary — any future change adding a public route for `mpt-api` should re-open this ADR, not silently patch compose. |
| The embedded 20-minute timeout masks a render that would have succeeded slightly later | Accepted trade-off; 20 minutes is far beyond MPT's expected 5-15 min render time on this 2-core box (MDD §8 Q4), so a false-timeout on a genuinely still-progressing render is unlikely. |

## Related Decisions

- **ADR-086** — Isolated PWA Architecture. VideoSmith is a direct instance of this pattern (own DB/role/auth/domain).
- **ADR-062** — Live AI Pipeline Contract. VideoSmith's embedded chain implements this contract's Tier 1/2/3 structure, but as an in-process implementation rather than a standalone proxy — the first ADR-062 pipeline built this way.
- **ADR-097** — Blogsmith AI Content Pipeline. The direct precedent VideoSmith's architecture mirrors (isolated PWA, embedded AI chain, external-service integration at the end of the pipeline).
- **ADR-073** — Traefik Label Conventions. VideoSmith's `zerossl` label (live, publicly reachable) follows this ADR's guidance for non-360lm-core, automation-tool services.

## References

- `/var/www/360lm/docs/MDD_videosmith.md` — full design + §13 Build Log (live-verified facts, bugs found/fixed)
- `/var/www/360lm/docs/plan_videosmith_2026-07-03.md` — phased build plan
- `/var/www/Others/Automation/videosmith/videosmith_app.py` — the implementation this ADR documents
- `/var/www/Others/Automation/blogsmith/blogsmith_ai.py` — the precedent this design mirrors
- MoneyPrinterTurbo source, `app/controllers/v1/video.py` (disabled auth dependency), `app/models/schema.py` (`VideoParams.video_script`/`video_terms` fields), `app/models/const.py` (task state constants) — reviewed directly via a shallow clone during the design phase
