← Files Pray Production StudioARCHIVED FILE

skills/pps-ads/packages/remotion-kit/src/ad/AdComposition.tsx

3.24 KB · Oct 8, 2026 · 06:29 UTC

↓ Download file

import React from 'react'
import { AbsoluteFill, Sequence, interpolate, useVideoConfig } from 'remotion'
import { Audio } from '@remotion/media'
import { SOUND, type BeatType } from '../grammar'
import { themeFromBrand } from '../theme'
import { frames } from '../motion/timing'
import { bpmFor } from '../motion/grid'
import type { AdCompositionProps } from './schema'
import type { BeatProps, RenderBeat } from './types'
import { footageWindows, voiceoverWindows } from './audio-plan'
import { Open } from './beats/Open'
import { Title } from './beats/Title'
import { Lockup } from './beats/Lockup'
import { Kinetic } from './beats/Kinetic'
import { Live } from './beats/Live'
import { Prompt } from './beats/Prompt'
import { Agent } from './beats/Agent'
import { Demo } from './beats/Demo'
import { Results } from './beats/Results'
import { Device } from './beats/Device'
import { Approve } from './beats/Approve'
import { Proof } from './beats/Proof'
import { End } from './beats/End'

const BEAT_COMPONENTS: Record<BeatType, React.FC<BeatProps>> = {
  open: Open,
  title: Title,
  lockup: Lockup,
  kinetic: Kinetic,
  live: Live,
  prompt: Prompt,
  agent: Agent,
  demo: Demo,
  results: Results,
  device: Device,
  approve: Approve,
  proof: Proof,
  end: End,
}

export const AdComposition: React.FC<AdCompositionProps> = ({ spec }) => {
  const { fps, durationInFrames } = useVideoConfig()
  const theme = themeFromBrand({ color: spec.brand.color, canvas: spec.brand.canvas })
  const brand = { name: spec.brand.name, logoUrl: spec.brand.logoUrl }
  const ducked = voiceoverWindows(spec)
  const footage = footageWindows(spec)
  const duck = Math.pow(10, -SOUND.musicDuckDb / 20)
  const underClip = Math.pow(10, -SOUND.musicUnderClipDb / 20)
  const musicVolume = (f: number) => {
    const inVoice = ducked.some((w) => f >= w.start && f <= w.end)
    const inClip = footage.some((w) => f >= w.start && f < w.end)
    const fadeOut = interpolate(
      f,
      [durationInFrames - frames(SOUND.finalFadeMs / 1000), durationInFrames],
      [1, 0],
      { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' },
    )
    return (inVoice ? duck : inClip ? underClip : 1) * fadeOut
  }
  return (
    <AbsoluteFill style={{ background: '#000' }}>
      {spec.beats.map((beat, index) => {
        const Component = BEAT_COMPONENTS[beat.type]
        const from = frames(beat.start)
        const length = frames(beat.end) - from
        return (
          <Sequence key={index} from={from} durationInFrames={length} premountFor={fps}>
            <Component
              beat={beat as RenderBeat}
              theme={theme}
              brand={brand}
              seconds={length / fps}
              bpm={bpmFor(spec.musicTempo)}
              format={spec.format ?? 'promo'}
            />
          </Sequence>
        )
      })}
      {/* A take starts on its beat but runs its own length: no durationInFrames, or a line is cut mid-word. */}
      {spec.beats.map((beat, index) =>
        beat.voiceoverUrl ? (
          <Sequence key={`vo-${index}`} from={frames(beat.start)} premountFor={fps}>
            <Audio src={beat.voiceoverUrl} />
          </Sequence>
        ) : null,
      )}
      {spec.musicUrl ? <Audio src={spec.musicUrl} volume={musicVolume} /> : null}
    </AbsoluteFill>
  )
}

SHA-256: ceb3c5b048c65772e2789aaba8b46ea902cb0073e68410b348903638f2f875a1