# Tapir — Architecture (C4 + sequences) Diagrams describe **current assumptions**, not a frozen design. They are diffable Mermaid so they live in version control and render in Gitea. When a decision here changes, record it in `DECISIONS.md` and update the relevant diagram in the same commit. Altitude: C4 Level 1 (Context) and Level 2 (Container), plus sequence diagrams for the two behaviours that carry the most design risk. Clean Architecture layering is described after the diagrams. --- ## C4 L1 — System Context Who and what Tapir talks to. (Brain and external AI are dashed — both optional.) ```mermaid graph TB user["User
(maintainer; later, trusted users)"] tapir["Tapir
watches subscriptions,
summarizes new videos"] yt["YouTube
(Data API + captions)"] vimeo["Vimeo
(API + text tracks)"] local["Local AI stack
(LiteLLM / piguard alias)"] byo["User's BYO AI
(Claude / OpenAI / Gemini)"] brain["brain
(brain-mcp)"] user -->|connects accounts,
reads summaries| tapir tapir -->|subscriptions,
new-video events,
captions| yt tapir -->|subscriptions,
text tracks| vimeo tapir -->|summarize
PRIMARY| local tapir -.->|summarize
FALLBACK, opt-in| byo tapir -.->|optional sink:
brain_ingest| brain classDef opt stroke-dasharray: 5 5; class byo,brain opt; ``` --- ## C4 L2 — Containers Inside Tapir. The **engine** is provider- and sink-agnostic; everything external is an adapter behind an interface (Clean Architecture ports & adapters). ```mermaid graph TB subgraph tapir["Tapir (Go)"] http["HTTP server
OAuth callbacks +
user-facing API"] watcher["Watcher
detects new videos
(WebSub + poll)"] engine["Summarization engine
(use-case core)"] resolver["Transcript resolver
(captions-first)"] router["AI router
(llm.Router:
Primary -> Fallback)"] store[("User store
(Postgres,
per-user isolated)")] subgraph sinks["Sink adapters (Sink interface)"] sink_store["Store sink
(primary)"] sink_brain["Brain sink
(HTTP brain-mcp)"] end subgraph providers["Provider adapters (VideoSource interface)"] p_yt["YouTube adapter"] p_vimeo["Vimeo adapter"] end end http --> store watcher --> p_yt watcher --> p_vimeo watcher -->|NewVideo event| engine engine --> resolver resolver --> p_yt resolver --> p_vimeo engine --> router engine --> sink_store engine -.-> sink_brain sink_store --> store classDef opt stroke-dasharray: 5 5; class sink_brain opt; ``` **Interfaces that keep the engine pure (the ports):** - `VideoSource` — list subscriptions, detect new videos, fetch transcript. Implemented by YouTube and Vimeo adapters. - `Summarizer` — turn a transcript + context into highlights/takeaways. Backed by the `llm.Router` (Primary local, Fallback BYO). - `Sink` — deliver a summary. Implemented by the store sink (primary) and brain sink (optional). New sinks add an implementation, nothing else. - `SecretStore` — fetch/store per-user OAuth tokens and BYO keys. Backed by ESO/1Password. The engine depends only on these interfaces, never on YouTube, brain, or a concrete model. This is what makes "standalone vs homelab" a configuration of which adapters are wired, not two codebases (ADR-003). --- ## Sequence — core use case: new video summarized ```mermaid sequenceDiagram participant Src as VideoSource
(YouTube/Vimeo) participant W as Watcher participant E as Engine participant R as Transcript resolver participant AI as AI router participant S as Sink(s) Src->>W: new upload (WebSub push / poll) W->>E: NewVideo{user, channel, videoID} E->>R: resolve transcript(videoID) R->>Src: fetch captions alt captions available Src-->>R: captions text R-->>E: Transcript{source: captions} E->>AI: summarize(transcript, userContext) AI-->>E: Summary{highlights, takeaways} E->>S: deliver(summary) S-->>E: ok else no transcript Src-->>R: none R-->>E: NoTranscript E->>S: deliver(skipped: no transcript) end ``` --- ## Sequence — AI routing (local-first, BYO fallback) ```mermaid sequenceDiagram participant E as Engine participant R as llm.Router participant P as Primary
(local: LiteLLM/piguard) participant F as Fallback
(user BYO key) E->>R: summarize(transcript) R->>P: complete(prompt) alt local succeeds P-->>R: summary R-->>E: summary (fallback_used = false) else local fails (error/timeout/unavailable) P-->>R: error alt user has BYO configured R->>F: complete(prompt) F-->>R: summary R-->>E: summary (fallback_used = true) else no BYO R-->>E: error (queued for retry) end end ``` > "Reliably" (VISION / the BYO trigger) is operationalized as: **Primary returned without > error within timeout.** Richer quality scoring can layer on later without changing the > interface. `fallback_used` is recorded per summary so "is the local stack good enough?" > becomes a query, not a guess. --- ## Clean Architecture layering ```mermaid graph LR subgraph domain["Domain (entities)"] d["User, Subscription, Video,
Transcript, Summary"] end subgraph usecase["Use cases (engine)"] u["SummarizeNewVideo,
ConnectAccount,
RouteAI"] end subgraph ports["Ports (interfaces)"] po["VideoSource, Summarizer,
Sink, SecretStore"] end subgraph adapters["Adapters (infra)"] a["YouTube, Vimeo, llm.Router,
Store sink, Brain sink,
ESO secret store, HTTP, Postgres"] end a --> po po --> u u --> d ``` Dependencies point inward only. Domain knows nothing of YouTube, brain, Postgres, or any model. Adapters are swappable; tests target the use-case core through fake adapters (see the Gherkin features in `docs/use-cases/`). --- ## Out of scope in these diagrams (deferred per ADRs) - Audio-download + speech-to-text resolver (ADR-007) — would be an additional `VideoSource` fallback path, drawn when built. - Multi-tenant isolation primitives (per-tenant Postgres role, NetworkPolicy, tenant label) — activate at Stage 1 (ADR-002); single-user Stage 0 doesn't exercise them. - Public SaaS surface (sign-up, billing) — Future C, not built (ADR-008).