Skip to content

feat(media): personalised video as a rendition of an approved draft - #28

Merged
ralyodio merged 1 commit into
mainfrom
feat/personalized-video
Aug 13, 2026
Merged

ralyodio merged 1 commit into
mainfrom
feat/personalized-video

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Why

Personalised video is the highest-lift-per-send format in cold outreach, and OutreachGraph already holds the thing that makes a clip worth watching: a specific, evidence-backed reason to be in touch. This adds the format without loosening any of the non-negotiables.

The design point

A video is never an independent generation. It is a rendition of a draft that already passed the §14.2 grounding gates and was approved by a human.

That framing is what makes it safe. Video is a medium where a reviewer cannot skim the claims the way they can in text, so the rule here is that nothing gets spoken that a person did not already read and approve.

How each non-negotiable is held

Non-negotiable How
Every personalised claim is grounded in stored evidence Script is assembled deterministically from the approved draft's own sentences — no model runs in this package — then re-checked with the same findUnsupportedClaims gate as an independent second pass, on both the hook and the whole body. Only the hook segment may reference the prospect, so grounding lands on a bounded, checkable span.
Human approval is the default Mandatory, read from the approvals table. There is no trusted_automation path to a render.
trusted_automation is additionally gated per capability capability.personalized_video must be explicitly true. A missing flag means disabled — deliberately the inverse of the network flags, because this one costs money and puts a synthetic likeness in front of someone who did not ask for it.
Policy is re-checked at execution time evaluatePolicy runs at render time against current flags and rules. The snapshot on the recommendation is never read.
Vendor objects never leak past an adapter Renderers sit behind VideoRenderer. GenMediaVideoRenderer declares what it needs structurally, so the package takes no dependency on any specific library (@profullstack/transcoder's genmedia layer satisfies it).
The pipeline stays runnable with no API keys FixtureVideoRenderer produces a deterministic descriptor. Same script → same URL, so fixture-backed tests can assert on it.

Other decisions worth flagging

  • 45s ceiling, estimated at a deliberately slow 130wpm. Over-estimating length makes the cap bite early, which is the safe direction — a clip that runs long is worse than one rejected before it costs anything to render.
  • One video per draft, enforced by a unique index. Rendering is billable; a retried worker run must not produce a second clip for the same approved message.
  • The row is written before the render starts, so a crash mid-render leaves a rendering/failed row rather than no trace at all.
  • grounded_signal_ids is stored on the asset, not joined from the draft. The audit question is "what evidence backed the thing we actually sent", and a later edit to the draft must not rewrite that answer.

Contents

  • packages/media — script builder, renderer boundary, render orchestration
  • packages/domain/src/video.ts — segment/script/asset types, duration estimation
  • migrations/0007_video_assets.sql
  • docs/prd-implementation-map.md — §14.3 row added

Testing

36 new tests. bun run check (format, typecheck, tests) is clean: 537 pass, 0 fail across the monorepo.

Refusal paths are covered explicitly: unapproved draft, rejected draft, missing flag, flag off, policy denial at render time, ungrounded hook, unsupported claim outside the hook, over-length script, and a failing renderer.

🤖 Generated with Claude Code

Adds @outreachgraph/media: a personalised outreach clip built from a draft
that has already passed the §14.2 grounding gates and been approved by a
human.

The design point is that a video is never an independent generation. Video
is a medium where a reviewer cannot skim the claims the way they can in
text, so nothing gets spoken that a person did not already read and
approve:

- The script is assembled deterministically from the approved draft's own
  sentences. No model runs here. It is then re-checked with the same
  findUnsupportedClaims gate as a second, independent pass, on both the
  hook and the whole body.
- Only the hook may reference the prospect's activity, so the grounding
  requirement lands on a bounded, checkable span.
- Human approval is mandatory and is read from the approvals table. There
  is no trusted_automation path: the capability is additionally gated by
  an explicit opt-in flag, and a missing flag means disabled — the inverse
  of the network flags, because this one costs money and puts a synthetic
  likeness in front of someone who did not ask for it.
- Policy is re-evaluated at render time. The snapshot on the
  recommendation is never trusted.
- A 45s ceiling, estimated at a deliberately slow speaking rate so the cap
  bites early.

Renderers sit behind an interface, so vendor objects do not leak:
FixtureVideoRenderer keeps the pipeline runnable with no API keys, and
GenMediaVideoRenderer adapts a generative video library structurally
(satisfied by @profullstack/transcoder's genmedia layer) without taking a
dependency on it.

One video per draft, enforced by a unique index — rendering is billable and
a retried worker run must not produce a second clip. A row is written
before the render starts so a crash leaves a trace rather than nothing.

36 new tests. Full suite: 537 pass, 0 fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ralyodio
ralyodio merged commit c8d85d0 into main Aug 13, 2026
4 checks passed
@ralyodio
ralyodio deleted the feat/personalized-video branch August 13, 2026 15:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant