@make-pictures/core 0.0.0-stage → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +50 -2
- package/dist/audio/SmartMusicBed.d.ts +61 -0
- package/dist/audio/SoundCue.d.ts +36 -0
- package/dist/audio/acousticPresets.d.ts +15 -0
- package/dist/audio/index.cjs +311 -0
- package/dist/audio/index.d.ts +3 -0
- package/dist/audio/index.mjs +25 -0
- package/dist/camera/DirectorCamera2D.d.ts +61 -0
- package/dist/camera/DirectorCamera3D.d.ts +56 -0
- package/dist/camera/cameraNoise.d.ts +36 -0
- package/dist/camera/index.cjs +1275 -0
- package/dist/camera/index.d.ts +4 -0
- package/dist/camera/index.mjs +38 -0
- package/dist/camera/shotTypes.d.ts +92 -0
- package/dist/chunk-4BLV45SO.mjs +997 -0
- package/dist/chunk-7DACEC3H.mjs +146 -0
- package/dist/chunk-7H3OMRSU.mjs +274 -0
- package/dist/chunk-IQVH2E54.mjs +94 -0
- package/dist/chunk-NCNZNUWU.mjs +0 -0
- package/dist/chunk-TKRCK7OW.mjs +53 -0
- package/dist/chunk-UL7ZSCA4.mjs +30 -0
- package/dist/chunk-YMFK5NF7.mjs +354 -0
- package/dist/index.cjs +1946 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.mjs +139 -0
- package/dist/production.d.ts +65 -0
- package/dist/theme/AmbientBackground.d.ts +8 -0
- package/dist/theme/VoiceoverCaptionBar.d.ts +8 -0
- package/dist/theme/fonts.d.ts +16 -0
- package/dist/theme/index.cjs +533 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.mjs +25 -0
- package/dist/theme/tokens.d.ts +105 -0
- package/dist/types/index.cjs +18 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.mjs +1 -0
- package/dist/types/scene.d.ts +115 -0
- package/dist/utils/dialogue.d.ts +27 -0
- package/dist/utils/index.cjs +236 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.mjs +29 -0
- package/dist/utils/prng.d.ts +11 -0
- package/dist/utils/timestamps.d.ts +23 -0
- package/package.json +77 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 make.pictures (https://make.pictures)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,51 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://make.pictures"><img src="https://make.pictures/brand/make-pictures-logo.svg" alt="make.pictures — Open-source 60 FPS Video-as-Code Studio" width="420" /></a>
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@make-pictures/core"><img src="https://img.shields.io/npm/v/@make-pictures/core?style=for-the-badge&labelColor=0F172A&color=FF2A85&label=%40make-pictures%2Fcore" alt="@make-pictures/core on npm" /></a>
|
|
7
|
+
<img src="https://img.shields.io/badge/Deterministic_60_FPS_runtime-2563EB?style=for-the-badge&labelColor=0F172A" alt="Deterministic 60 FPS runtime" />
|
|
8
|
+
<a href="https://make.pictures"><img src="https://img.shields.io/badge/make.pictures-Studio-2563EB?style=for-the-badge&labelColor=0F172A" alt="make.pictures Studio" /></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
# `@make-pictures/core`
|
|
12
|
+
|
|
13
|
+
Deterministic **60 FPS Video-as-Code** core runtime for [`make.pictures`](https://make.pictures) — built on React, TypeScript, and Remotion.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Cinematography & Shot DSL (`@make-pictures/core/camera`):** `<DirectorCamera3D />` (`establishing-wide`, `medium`, `close-up`, `low-angle-hero`, `whip`, `glide`, `orbit`, optical `dollyZoomDeltaFov`, automatic `9:16` Golden Safe Box compensation) and `<DirectorCamera2D />`.
|
|
18
|
+
- **Smart Audio Engine (`@make-pictures/core/audio`):** `<SmartMusicBed />` sidechain auto-ducking under narration, `useMusicBeatPulse`, `<SoundCue />` word-timestamp synchronization, and `VOICE_ACOUSTIC_FILTERS`.
|
|
19
|
+
- **Production Manifest & Capsule Contracts (`@make-pictures/core`):** `defineProduction`, `productionAssetPath`, `SPRING_PRESETS` (`damping: 200`), `STUDIO_THEME`, `THEME`, and deterministic Mulberry32 `createSeededRandom`.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @make-pictures/core remotion @remotion/media-utils @remotion/google-fonts zod
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { spring, useCurrentFrame, useVideoConfig } from 'remotion';
|
|
31
|
+
import { SPRING_PRESETS, createSeededRandom } from '@make-pictures/core';
|
|
32
|
+
|
|
33
|
+
export const Hero: React.FC<{ title: string }> = ({ title }) => {
|
|
34
|
+
const frame = useCurrentFrame();
|
|
35
|
+
const { fps } = useVideoConfig();
|
|
36
|
+
// frame 0 stays a clean board; damping: 200 means zero rubbery bounce
|
|
37
|
+
const enter = spring({ frame: frame - 2, fps, config: SPRING_PRESETS.snappy });
|
|
38
|
+
const seed = createSeededRandom('hero-v1')(); // deterministic, never Math.random()
|
|
39
|
+
return <h1 style={{ opacity: enter }} data-seed={seed}>{title}</h1>;
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Links
|
|
44
|
+
|
|
45
|
+
- Website: [make.pictures](https://make.pictures)
|
|
46
|
+
- Source & docs: [github.com/make-pictures/make-pictures-studio](https://github.com/make-pictures/make-pictures-studio)
|
|
47
|
+
- YouTube: [@makepicturestv](https://www.youtube.com/@makepicturestv) (EN) · [@makepicturespl](https://www.youtube.com/@makepicturespl) (PL)
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
MIT © [make.pictures](https://make.pictures)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { SceneVoiceoverData } from '../types/scene';
|
|
3
|
+
export interface DialogueDuckingSegment {
|
|
4
|
+
sceneId: string;
|
|
5
|
+
fromFrame: number;
|
|
6
|
+
durationInFrames: number;
|
|
7
|
+
}
|
|
8
|
+
export interface SmartMusicBedProps {
|
|
9
|
+
src: string;
|
|
10
|
+
dialogueSegments?: DialogueDuckingSegment[];
|
|
11
|
+
voiceoverManifest?: Record<string, SceneVoiceoverData>;
|
|
12
|
+
baseVolume?: number;
|
|
13
|
+
duckedVolume?: number;
|
|
14
|
+
lookaheadFrames?: number;
|
|
15
|
+
releaseFrames?: number;
|
|
16
|
+
fadeInFrames?: number;
|
|
17
|
+
fadeOutFrames?: number;
|
|
18
|
+
loop?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface MusicBeatPulseOptions {
|
|
21
|
+
lowBinCount?: number;
|
|
22
|
+
smoothingFrames?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Computes the continuous speech ducking weight in `0..1` (`0` = no speech / full music,
|
|
26
|
+
* `1` = active speech / ducked music) at `frame`, with smooth lookahead and release ramps.
|
|
27
|
+
*/
|
|
28
|
+
export declare function computeSpeechDuckingEnvelopeAtFrame(params: {
|
|
29
|
+
frame: number;
|
|
30
|
+
dialogueSegments?: DialogueDuckingSegment[];
|
|
31
|
+
voiceoverManifest?: Record<string, SceneVoiceoverData>;
|
|
32
|
+
lookaheadFrames?: number;
|
|
33
|
+
releaseFrames?: number;
|
|
34
|
+
}): number;
|
|
35
|
+
/**
|
|
36
|
+
* Computes the final music bed volume at `frame` combining intro/outro master fades
|
|
37
|
+
* and smooth sidechain speech ducking.
|
|
38
|
+
*/
|
|
39
|
+
export declare function computeMusicBedVolumeAtFrame(params: {
|
|
40
|
+
frame: number;
|
|
41
|
+
durationInFrames: number;
|
|
42
|
+
dialogueSegments?: DialogueDuckingSegment[];
|
|
43
|
+
voiceoverManifest?: Record<string, SceneVoiceoverData>;
|
|
44
|
+
baseVolume?: number;
|
|
45
|
+
duckedVolume?: number;
|
|
46
|
+
lookaheadFrames?: number;
|
|
47
|
+
releaseFrames?: number;
|
|
48
|
+
fadeInFrames?: number;
|
|
49
|
+
fadeOutFrames?: number;
|
|
50
|
+
}): number;
|
|
51
|
+
/**
|
|
52
|
+
* Broadcast-grade background music component with lookahead sidechain auto-ducking.
|
|
53
|
+
* Breathes up in intros, scene transitions, and pauses while ducking smoothly under dialogue.
|
|
54
|
+
*/
|
|
55
|
+
export declare const SmartMusicBed: React.FC<SmartMusicBedProps>;
|
|
56
|
+
/**
|
|
57
|
+
* Extracts a smooth, normalized `0..1` low-frequency kick/bass transient pulse from `audioSrc`
|
|
58
|
+
* using `@remotion/media-utils` (`useAudioData` + `visualizeAudio`).
|
|
59
|
+
* Safely returns `0` when `audioSrc` is `null`, empty, or still loading.
|
|
60
|
+
*/
|
|
61
|
+
export declare function useMusicBeatPulse(audioSrc: string | null, options?: MusicBeatPulseOptions): number;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { SceneVoiceoverData } from '../types/scene';
|
|
3
|
+
export interface SoundCueCommonProps {
|
|
4
|
+
src: string;
|
|
5
|
+
volume?: number;
|
|
6
|
+
playbackRate?: number;
|
|
7
|
+
durationInFrames?: number;
|
|
8
|
+
offsetFrames?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface SoundCueAtFrameProps extends SoundCueCommonProps {
|
|
11
|
+
atFrame: number;
|
|
12
|
+
sceneId?: undefined;
|
|
13
|
+
onWord?: undefined;
|
|
14
|
+
sceneStartFrame?: undefined;
|
|
15
|
+
fallbackFrame?: undefined;
|
|
16
|
+
voiceoverManifest?: undefined;
|
|
17
|
+
}
|
|
18
|
+
export interface SoundCueWordSyncProps extends SoundCueCommonProps {
|
|
19
|
+
atFrame?: undefined;
|
|
20
|
+
sceneId: string;
|
|
21
|
+
onWord: string;
|
|
22
|
+
sceneStartFrame?: number;
|
|
23
|
+
fallbackFrame?: number;
|
|
24
|
+
voiceoverManifest?: Record<string, SceneVoiceoverData>;
|
|
25
|
+
}
|
|
26
|
+
export type SoundCueProps = SoundCueAtFrameProps | SoundCueWordSyncProps;
|
|
27
|
+
/**
|
|
28
|
+
* Resolves the exact composition frame (`>= 0`) at which a `<SoundCue />` fires.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveSoundCueFrame(props: SoundCueProps): number;
|
|
31
|
+
/**
|
|
32
|
+
* Declarative Foley & SFX component supporting either explicit `atFrame` timing
|
|
33
|
+
* or automatic word-level synchronization (`sceneId` + `onWord`).
|
|
34
|
+
* Uses `layout="none"` so it can be mounted safely inside both 2D HTML trees and 3D stages.
|
|
35
|
+
*/
|
|
36
|
+
export declare const SoundCue: React.FC<SoundCueProps>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { VoiceAcousticPreset } from '../types/scene';
|
|
2
|
+
export type { VoiceAcousticPreset };
|
|
3
|
+
/**
|
|
4
|
+
* Declarative ffmpeg `-af` filter chains for character and scene voice acoustics.
|
|
5
|
+
* Every preset preserves a 250ms silent tail (`apad=pad_dur=0.25`) so word endings
|
|
6
|
+
* never clip during playback or MP4 stitching.
|
|
7
|
+
*/
|
|
8
|
+
export declare const VOICE_ACOUSTIC_FILTERS: Record<VoiceAcousticPreset, string>;
|
|
9
|
+
export declare const VOICE_ACOUSTIC_PRESET_NAMES: VoiceAcousticPreset[];
|
|
10
|
+
export declare function isVoiceAcousticPreset(value: unknown): value is VoiceAcousticPreset;
|
|
11
|
+
/**
|
|
12
|
+
* Resolves a declarative `VoiceAcousticPreset` name to an `ffmpeg -af` filter string.
|
|
13
|
+
* Defaults safely to `'clean'` (`apad=pad_dur=0.25`).
|
|
14
|
+
*/
|
|
15
|
+
export declare function getVoiceAcousticFilter(preset?: VoiceAcousticPreset): string;
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// packages/core/src/audio/index.ts
|
|
21
|
+
var audio_exports = {};
|
|
22
|
+
__export(audio_exports, {
|
|
23
|
+
SmartMusicBed: () => SmartMusicBed,
|
|
24
|
+
SoundCue: () => SoundCue,
|
|
25
|
+
VOICE_ACOUSTIC_FILTERS: () => VOICE_ACOUSTIC_FILTERS,
|
|
26
|
+
VOICE_ACOUSTIC_PRESET_NAMES: () => VOICE_ACOUSTIC_PRESET_NAMES,
|
|
27
|
+
computeMusicBedVolumeAtFrame: () => computeMusicBedVolumeAtFrame,
|
|
28
|
+
computeSpeechDuckingEnvelopeAtFrame: () => computeSpeechDuckingEnvelopeAtFrame,
|
|
29
|
+
getVoiceAcousticFilter: () => getVoiceAcousticFilter,
|
|
30
|
+
isVoiceAcousticPreset: () => isVoiceAcousticPreset,
|
|
31
|
+
resolveSoundCueFrame: () => resolveSoundCueFrame,
|
|
32
|
+
useMusicBeatPulse: () => useMusicBeatPulse
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(audio_exports);
|
|
35
|
+
|
|
36
|
+
// packages/core/src/audio/acousticPresets.ts
|
|
37
|
+
var VOICE_ACOUSTIC_FILTERS = {
|
|
38
|
+
clean: "apad=pad_dur=0.25",
|
|
39
|
+
telephone: "highpass=f=340,lowpass=f=3200,acompressor=threshold=-16dB:ratio=3.5:attack=5:release=60,volume=1.18,apad=pad_dur=0.25",
|
|
40
|
+
"radio-walkie": "highpass=f=480,lowpass=f=2700,acrusher=bits=10:mix=0.28,acompressor=threshold=-18dB:ratio=4:attack=4:release=50,volume=1.22,apad=pad_dur=0.25",
|
|
41
|
+
cathedral: "highpass=f=140,aecho=0.82:0.78:48|96:0.32|0.18,lowpass=f=7800,volume=1.08,apad=pad_dur=0.25",
|
|
42
|
+
megaphone: "highpass=f=230,lowpass=f=3100,aecho=0.85:0.75:16:0.22,volume=1.15,apad=pad_dur=0.25",
|
|
43
|
+
"excitement-boost": "highpass=f=85,treble=g=2.0:f=4200,volume=1.32,alimiter=limit=0.96,apad=pad_dur=0.25"
|
|
44
|
+
};
|
|
45
|
+
var VOICE_ACOUSTIC_PRESET_NAMES = Object.keys(
|
|
46
|
+
VOICE_ACOUSTIC_FILTERS
|
|
47
|
+
);
|
|
48
|
+
function isVoiceAcousticPreset(value) {
|
|
49
|
+
return typeof value === "string" && Object.prototype.hasOwnProperty.call(VOICE_ACOUSTIC_FILTERS, value);
|
|
50
|
+
}
|
|
51
|
+
function getVoiceAcousticFilter(preset) {
|
|
52
|
+
if (!preset || !isVoiceAcousticPreset(preset)) {
|
|
53
|
+
return VOICE_ACOUSTIC_FILTERS.clean;
|
|
54
|
+
}
|
|
55
|
+
return VOICE_ACOUSTIC_FILTERS[preset];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// packages/core/src/audio/SmartMusicBed.tsx
|
|
59
|
+
var import_react = require("react");
|
|
60
|
+
var import_media_utils = require("@remotion/media-utils");
|
|
61
|
+
var import_remotion = require("remotion");
|
|
62
|
+
|
|
63
|
+
// packages/core/src/utils/timestamps.ts
|
|
64
|
+
var REGISTERED_VOICEOVER_MANIFEST = {};
|
|
65
|
+
function getSceneVoiceover(sceneId, manifest) {
|
|
66
|
+
if (manifest && manifest[sceneId]) {
|
|
67
|
+
return manifest[sceneId];
|
|
68
|
+
}
|
|
69
|
+
return REGISTERED_VOICEOVER_MANIFEST[sceneId];
|
|
70
|
+
}
|
|
71
|
+
function getWordTriggerFrame(sceneId, targetWord, fallbackFrame, manifest) {
|
|
72
|
+
const voiceover = getSceneVoiceover(sceneId, manifest);
|
|
73
|
+
if (!voiceover || !voiceover.words.length) {
|
|
74
|
+
return fallbackFrame;
|
|
75
|
+
}
|
|
76
|
+
const needle = targetWord.replace(/^[.,!?;:"'()[\]{}—–-]+|[.,!?;:"'()[\]{}—–-]+$/g, "").toLowerCase();
|
|
77
|
+
const exactMatch = voiceover.words.find((w) => w.normalizedWord === needle);
|
|
78
|
+
if (exactMatch) {
|
|
79
|
+
return exactMatch.startFrame;
|
|
80
|
+
}
|
|
81
|
+
const partialMatch = voiceover.words.find(
|
|
82
|
+
(w) => w.normalizedWord.includes(needle)
|
|
83
|
+
);
|
|
84
|
+
return partialMatch ? partialMatch.startFrame : fallbackFrame;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// packages/core/src/audio/SmartMusicBed.tsx
|
|
88
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
89
|
+
var SILENT_WAV_DATA_URI = "data:audio/wav;base64,UklGRiYAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQIAAAAAAA==";
|
|
90
|
+
function smoothstep01(value) {
|
|
91
|
+
const t = Math.max(0, Math.min(1, value));
|
|
92
|
+
return t * t * (3 - 2 * t);
|
|
93
|
+
}
|
|
94
|
+
function buildPhraseSpansForSegment(segment, manifest, mergeGapFrames) {
|
|
95
|
+
const vo = getSceneVoiceover(segment.sceneId, manifest);
|
|
96
|
+
const segStart = segment.fromFrame;
|
|
97
|
+
const segEnd = segment.fromFrame + Math.max(1, segment.durationInFrames);
|
|
98
|
+
if (!vo || !Array.isArray(vo.words) || vo.words.length === 0) {
|
|
99
|
+
return [{ start: segStart, end: segEnd }];
|
|
100
|
+
}
|
|
101
|
+
const spans = [];
|
|
102
|
+
for (const word of vo.words) {
|
|
103
|
+
const absStart = segStart + word.startFrame;
|
|
104
|
+
const absEnd = Math.min(segEnd + 18, segStart + Math.max(word.startFrame + 2, word.endFrame));
|
|
105
|
+
const prev = spans[spans.length - 1];
|
|
106
|
+
if (prev && absStart - prev.end <= mergeGapFrames) {
|
|
107
|
+
prev.end = Math.max(prev.end, absEnd);
|
|
108
|
+
} else {
|
|
109
|
+
spans.push({ start: absStart, end: absEnd });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return spans;
|
|
113
|
+
}
|
|
114
|
+
function computeSpeechDuckingEnvelopeAtFrame(params) {
|
|
115
|
+
const {
|
|
116
|
+
frame,
|
|
117
|
+
dialogueSegments = [],
|
|
118
|
+
voiceoverManifest,
|
|
119
|
+
lookaheadFrames = 12,
|
|
120
|
+
releaseFrames = 28
|
|
121
|
+
} = params;
|
|
122
|
+
if (dialogueSegments.length === 0) {
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
const attack = Math.max(1, lookaheadFrames);
|
|
126
|
+
const release = Math.max(1, releaseFrames);
|
|
127
|
+
const mergeGap = attack + release + 10;
|
|
128
|
+
let maxWeight = 0;
|
|
129
|
+
for (const seg of dialogueSegments) {
|
|
130
|
+
const spans = buildPhraseSpansForSegment(seg, voiceoverManifest, mergeGap);
|
|
131
|
+
for (const span of spans) {
|
|
132
|
+
const rampIn = (0, import_remotion.interpolate)(
|
|
133
|
+
frame,
|
|
134
|
+
[span.start - attack, span.start + 4],
|
|
135
|
+
[0, 1],
|
|
136
|
+
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
137
|
+
);
|
|
138
|
+
const rampOut = (0, import_remotion.interpolate)(
|
|
139
|
+
frame,
|
|
140
|
+
[span.end - 2, span.end + release],
|
|
141
|
+
[1, 0],
|
|
142
|
+
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
143
|
+
);
|
|
144
|
+
const weight = smoothstep01(rampIn) * smoothstep01(rampOut);
|
|
145
|
+
if (weight > maxWeight) {
|
|
146
|
+
maxWeight = weight;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return maxWeight;
|
|
151
|
+
}
|
|
152
|
+
function computeMusicBedVolumeAtFrame(params) {
|
|
153
|
+
const {
|
|
154
|
+
frame,
|
|
155
|
+
durationInFrames,
|
|
156
|
+
dialogueSegments = [],
|
|
157
|
+
voiceoverManifest,
|
|
158
|
+
baseVolume = 0.26,
|
|
159
|
+
duckedVolume = 0.085,
|
|
160
|
+
lookaheadFrames = 12,
|
|
161
|
+
releaseFrames = 28,
|
|
162
|
+
fadeInFrames = 30,
|
|
163
|
+
fadeOutFrames = 45
|
|
164
|
+
} = params;
|
|
165
|
+
const duckWeight = computeSpeechDuckingEnvelopeAtFrame({
|
|
166
|
+
frame,
|
|
167
|
+
dialogueSegments,
|
|
168
|
+
voiceoverManifest,
|
|
169
|
+
lookaheadFrames,
|
|
170
|
+
releaseFrames
|
|
171
|
+
});
|
|
172
|
+
const duckedGain = baseVolume + (duckedVolume - baseVolume) * duckWeight;
|
|
173
|
+
const rawFadeIn = fadeInFrames > 0 ? (0, import_remotion.interpolate)(frame, [0, fadeInFrames], [0, 1], {
|
|
174
|
+
extrapolateLeft: "clamp",
|
|
175
|
+
extrapolateRight: "clamp"
|
|
176
|
+
}) : 1;
|
|
177
|
+
const fadeOutStart = Math.max(0, durationInFrames - Math.max(0, fadeOutFrames));
|
|
178
|
+
const rawFadeOut = fadeOutFrames > 0 ? (0, import_remotion.interpolate)(frame, [fadeOutStart, durationInFrames], [1, 0], {
|
|
179
|
+
extrapolateLeft: "clamp",
|
|
180
|
+
extrapolateRight: "clamp"
|
|
181
|
+
}) : 1;
|
|
182
|
+
const masterEnvelope = smoothstep01(rawFadeIn) * smoothstep01(rawFadeOut);
|
|
183
|
+
return Math.max(0, Number((duckedGain * masterEnvelope).toFixed(5)));
|
|
184
|
+
}
|
|
185
|
+
var SmartMusicBed = ({
|
|
186
|
+
src,
|
|
187
|
+
dialogueSegments = [],
|
|
188
|
+
voiceoverManifest,
|
|
189
|
+
baseVolume = 0.26,
|
|
190
|
+
duckedVolume = 0.085,
|
|
191
|
+
lookaheadFrames = 12,
|
|
192
|
+
releaseFrames = 28,
|
|
193
|
+
fadeInFrames = 30,
|
|
194
|
+
fadeOutFrames = 45,
|
|
195
|
+
loop = true
|
|
196
|
+
}) => {
|
|
197
|
+
const { durationInFrames } = (0, import_remotion.useVideoConfig)();
|
|
198
|
+
const volumeAtFrame = (0, import_react.useCallback)(
|
|
199
|
+
(f) => computeMusicBedVolumeAtFrame({
|
|
200
|
+
frame: f,
|
|
201
|
+
durationInFrames,
|
|
202
|
+
dialogueSegments,
|
|
203
|
+
voiceoverManifest,
|
|
204
|
+
baseVolume,
|
|
205
|
+
duckedVolume,
|
|
206
|
+
lookaheadFrames,
|
|
207
|
+
releaseFrames,
|
|
208
|
+
fadeInFrames,
|
|
209
|
+
fadeOutFrames
|
|
210
|
+
}),
|
|
211
|
+
[
|
|
212
|
+
durationInFrames,
|
|
213
|
+
dialogueSegments,
|
|
214
|
+
voiceoverManifest,
|
|
215
|
+
baseVolume,
|
|
216
|
+
duckedVolume,
|
|
217
|
+
lookaheadFrames,
|
|
218
|
+
releaseFrames,
|
|
219
|
+
fadeInFrames,
|
|
220
|
+
fadeOutFrames
|
|
221
|
+
]
|
|
222
|
+
);
|
|
223
|
+
if (!src) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_remotion.Sequence, { from: 0, durationInFrames, layout: "none", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
227
|
+
import_remotion.Audio,
|
|
228
|
+
{
|
|
229
|
+
src,
|
|
230
|
+
loop,
|
|
231
|
+
loopVolumeCurveBehavior: "extend",
|
|
232
|
+
volume: volumeAtFrame
|
|
233
|
+
}
|
|
234
|
+
) });
|
|
235
|
+
};
|
|
236
|
+
function useMusicBeatPulse(audioSrc, options) {
|
|
237
|
+
const frame = (0, import_remotion.useCurrentFrame)();
|
|
238
|
+
const { fps } = (0, import_remotion.useVideoConfig)();
|
|
239
|
+
const hasSource = Boolean(audioSrc && audioSrc.trim().length > 0);
|
|
240
|
+
const audioData = (0, import_media_utils.useAudioData)(hasSource ? audioSrc : SILENT_WAV_DATA_URI);
|
|
241
|
+
if (!hasSource || !audioData) {
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
const totalAudioFrames = Math.max(
|
|
245
|
+
1,
|
|
246
|
+
Math.floor(audioData.durationInSeconds * fps)
|
|
247
|
+
);
|
|
248
|
+
const binCount = Math.max(1, Math.min(16, options?.lowBinCount ?? 4));
|
|
249
|
+
const windowSize = Math.max(1, Math.min(9, options?.smoothingFrames ?? 3));
|
|
250
|
+
const halfWindow = Math.floor(windowSize / 2);
|
|
251
|
+
let energySum = 0;
|
|
252
|
+
let sampleCount = 0;
|
|
253
|
+
for (let offset = -halfWindow; offset <= halfWindow; offset++) {
|
|
254
|
+
const wrappedFrame = ((frame + offset) % totalAudioFrames + totalAudioFrames) % totalAudioFrames;
|
|
255
|
+
const spectrum = (0, import_media_utils.visualizeAudio)({
|
|
256
|
+
audioData,
|
|
257
|
+
frame: wrappedFrame,
|
|
258
|
+
fps,
|
|
259
|
+
numberOfSamples: 32,
|
|
260
|
+
smoothing: true
|
|
261
|
+
});
|
|
262
|
+
let frameBass = 0;
|
|
263
|
+
for (let b = 0; b < binCount; b++) {
|
|
264
|
+
frameBass += spectrum[b] ?? 0;
|
|
265
|
+
}
|
|
266
|
+
energySum += frameBass / binCount;
|
|
267
|
+
sampleCount += 1;
|
|
268
|
+
}
|
|
269
|
+
const avgBass = sampleCount > 0 ? energySum / sampleCount : 0;
|
|
270
|
+
return Math.max(0, Math.min(1, Math.pow(avgBass * 2.4, 1.15)));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// packages/core/src/audio/SoundCue.tsx
|
|
274
|
+
var import_remotion2 = require("remotion");
|
|
275
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
276
|
+
function resolveSoundCueFrame(props) {
|
|
277
|
+
const offset = props.offsetFrames ?? 0;
|
|
278
|
+
if (typeof props.atFrame === "number") {
|
|
279
|
+
return Math.max(0, Math.round(props.atFrame + offset));
|
|
280
|
+
}
|
|
281
|
+
const sceneStart = props.sceneStartFrame ?? 0;
|
|
282
|
+
const fallback = props.fallbackFrame ?? 0;
|
|
283
|
+
const localWordFrame = getWordTriggerFrame(
|
|
284
|
+
props.sceneId,
|
|
285
|
+
props.onWord,
|
|
286
|
+
fallback,
|
|
287
|
+
props.voiceoverManifest
|
|
288
|
+
);
|
|
289
|
+
return Math.max(0, Math.round(sceneStart + localWordFrame + offset));
|
|
290
|
+
}
|
|
291
|
+
var SoundCue = (props) => {
|
|
292
|
+
const {
|
|
293
|
+
src,
|
|
294
|
+
volume = 0.4,
|
|
295
|
+
playbackRate = 1,
|
|
296
|
+
durationInFrames
|
|
297
|
+
} = props;
|
|
298
|
+
if (!src) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const resolvedFrame = resolveSoundCueFrame(props);
|
|
302
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
303
|
+
import_remotion2.Sequence,
|
|
304
|
+
{
|
|
305
|
+
from: resolvedFrame,
|
|
306
|
+
durationInFrames,
|
|
307
|
+
layout: "none",
|
|
308
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_remotion2.Audio, { src, volume, playbackRate })
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { VOICE_ACOUSTIC_FILTERS, VOICE_ACOUSTIC_PRESET_NAMES, getVoiceAcousticFilter, isVoiceAcousticPreset, type VoiceAcousticPreset, } from './acousticPresets';
|
|
2
|
+
export { SmartMusicBed, computeMusicBedVolumeAtFrame, computeSpeechDuckingEnvelopeAtFrame, useMusicBeatPulse, type DialogueDuckingSegment, type MusicBeatPulseOptions, type SmartMusicBedProps, } from './SmartMusicBed';
|
|
3
|
+
export { SoundCue, resolveSoundCueFrame, type SoundCueAtFrameProps, type SoundCueCommonProps, type SoundCueProps, type SoundCueWordSyncProps, } from './SoundCue';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SmartMusicBed,
|
|
3
|
+
SoundCue,
|
|
4
|
+
VOICE_ACOUSTIC_FILTERS,
|
|
5
|
+
VOICE_ACOUSTIC_PRESET_NAMES,
|
|
6
|
+
computeMusicBedVolumeAtFrame,
|
|
7
|
+
computeSpeechDuckingEnvelopeAtFrame,
|
|
8
|
+
getVoiceAcousticFilter,
|
|
9
|
+
isVoiceAcousticPreset,
|
|
10
|
+
resolveSoundCueFrame,
|
|
11
|
+
useMusicBeatPulse
|
|
12
|
+
} from "../chunk-7H3OMRSU.mjs";
|
|
13
|
+
import "../chunk-TKRCK7OW.mjs";
|
|
14
|
+
export {
|
|
15
|
+
SmartMusicBed,
|
|
16
|
+
SoundCue,
|
|
17
|
+
VOICE_ACOUSTIC_FILTERS,
|
|
18
|
+
VOICE_ACOUSTIC_PRESET_NAMES,
|
|
19
|
+
computeMusicBedVolumeAtFrame,
|
|
20
|
+
computeSpeechDuckingEnvelopeAtFrame,
|
|
21
|
+
getVoiceAcousticFilter,
|
|
22
|
+
isVoiceAcousticPreset,
|
|
23
|
+
resolveSoundCueFrame,
|
|
24
|
+
useMusicBeatPulse
|
|
25
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type CameraImpulse, type CameraKeyframe2D, type DirectorCameraTelemetry, type ShotSpec2D, type StageNode2D } from './shotTypes';
|
|
3
|
+
export interface EvaluateDirectorCamera2DParams {
|
|
4
|
+
frame: number;
|
|
5
|
+
fps: number;
|
|
6
|
+
stageWidth: number;
|
|
7
|
+
stageHeight: number;
|
|
8
|
+
keyframes?: ReadonlyArray<CameraKeyframe2D>;
|
|
9
|
+
nodes?: Record<string, StageNode2D>;
|
|
10
|
+
shots?: ReadonlyArray<ShotSpec2D>;
|
|
11
|
+
safeBox9x16Compensation?: boolean;
|
|
12
|
+
handheldIntensity?: number;
|
|
13
|
+
impulses?: ReadonlyArray<CameraImpulse>;
|
|
14
|
+
seed?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ResolvedCameraState2D {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
zoom: number;
|
|
20
|
+
rotateZ: number;
|
|
21
|
+
telemetry: DirectorCameraTelemetry;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Converts declarative `nodes + shots` into exact `CameraKeyframe2D[]`,
|
|
25
|
+
* automatically fitting single or multi-node bounding boxes into either `16:9`
|
|
26
|
+
* or the `9:16 Golden Safe Box` (`x ∈ [40, 910], y ∈ [220, 1380]`).
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveShotsToKeyframes2D(nodes: Record<string, StageNode2D>, shots: ReadonlyArray<ShotSpec2D>, stageWidth: number, stageHeight: number, safeBox9x16Compensation?: boolean): CameraKeyframe2D[];
|
|
29
|
+
/**
|
|
30
|
+
* Pure deterministic 2D infinite-canvas camera solver (`f(frame, fps, ...)`).
|
|
31
|
+
*/
|
|
32
|
+
export declare function evaluateDirectorCamera2D(params: EvaluateDirectorCamera2DParams): ResolvedCameraState2D;
|
|
33
|
+
export interface DirectorCamera2DProps {
|
|
34
|
+
/** Raw camera keyframes (compatible with `CameraRig`) */
|
|
35
|
+
keyframes?: CameraKeyframe2D[];
|
|
36
|
+
/** Named 2D `NodeBox` dictionary for declarative Shot-DSL auto-framing */
|
|
37
|
+
nodes?: Record<string, StageNode2D>;
|
|
38
|
+
/** Declarative Shot-DSL target sequence resolved against `nodes` */
|
|
39
|
+
shots?: ShotSpec2D[];
|
|
40
|
+
stageWidth?: number;
|
|
41
|
+
stageHeight?: number;
|
|
42
|
+
variant?: 'light' | 'dark';
|
|
43
|
+
/** Automatically centers bounded targets inside the `9:16 Golden Safe Box` when `stageHeight > stageWidth` */
|
|
44
|
+
safeBox9x16Compensation?: boolean;
|
|
45
|
+
/** Subtle low-frequency handheld float intensity (default `0.15`) */
|
|
46
|
+
handheldIntensity?: number;
|
|
47
|
+
/** Damped harmonic camera shake impulses */
|
|
48
|
+
impulses?: CameraImpulse[];
|
|
49
|
+
seed?: number;
|
|
50
|
+
/** Toggle technical blueprint backdrop & grid (default `true`) */
|
|
51
|
+
showGrid?: boolean;
|
|
52
|
+
/** Optional viewport-pinned HUD / VFX layer (`<CinematicLensOverlay />`, etc.) */
|
|
53
|
+
overlay?: React.ReactNode;
|
|
54
|
+
children: React.ReactNode;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Shot-DSL Infinite-Canvas 2D Camera Rig with zero-clipping viewport-pinned
|
|
58
|
+
* blueprint grid, `9:16 Golden Safe Box` auto-framing, deterministic shake,
|
|
59
|
+
* and live `DirectorCameraTelemetryContext`.
|
|
60
|
+
*/
|
|
61
|
+
export declare const DirectorCamera2D: React.FC<DirectorCamera2DProps>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type CameraImpulse, type DirectorCameraTelemetry, type ShotSpec3D, type StageAnchor3D } from './shotTypes';
|
|
3
|
+
export interface ResolvedCameraPose3D {
|
|
4
|
+
position: [number, number, number];
|
|
5
|
+
lookAt: [number, number, number];
|
|
6
|
+
fov: number;
|
|
7
|
+
rollDeg: number;
|
|
8
|
+
telemetry: DirectorCameraTelemetry;
|
|
9
|
+
}
|
|
10
|
+
export interface EvaluateDirectorCamera3DParams {
|
|
11
|
+
frame: number;
|
|
12
|
+
fps: number;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
durationInFrames?: number;
|
|
16
|
+
anchors: Record<string, StageAnchor3D>;
|
|
17
|
+
shots: ReadonlyArray<ShotSpec3D>;
|
|
18
|
+
handheldIntensity?: number;
|
|
19
|
+
impulses?: ReadonlyArray<CameraImpulse>;
|
|
20
|
+
safeBox9x16Compensation?: boolean;
|
|
21
|
+
fovOffset?: number;
|
|
22
|
+
distanceScale?: number;
|
|
23
|
+
seed?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Reads live camera telemetry (`velocity`, `focusDistance`, `activeShotIndex`)
|
|
27
|
+
* from either an enclosing `<DirectorCamera2D>` / `<DirectorCameraTelemetryProvider>`
|
|
28
|
+
* OR a sibling `<DirectorCamera3D>` inside `<ThreeCanvas>` at the exact current `frame`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useDirectorCameraTelemetry(): DirectorCameraTelemetry;
|
|
31
|
+
/**
|
|
32
|
+
* Pure deterministic 3D Shot DSL evaluator (`f(frame, fps, width, height, props)`).
|
|
33
|
+
*/
|
|
34
|
+
export declare function evaluateDirectorCamera3D(params: EvaluateDirectorCamera3DParams): ResolvedCameraPose3D;
|
|
35
|
+
export interface DirectorCameraTelemetryProviderProps {
|
|
36
|
+
telemetry: DirectorCameraTelemetry;
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
}
|
|
39
|
+
export declare const DirectorCameraTelemetryProvider: React.FC<DirectorCameraTelemetryProviderProps>;
|
|
40
|
+
export interface DirectorCamera3DProps {
|
|
41
|
+
anchors: Record<string, StageAnchor3D>;
|
|
42
|
+
shots: ShotSpec3D[];
|
|
43
|
+
handheldIntensity?: number;
|
|
44
|
+
impulses?: CameraImpulse[];
|
|
45
|
+
safeBox9x16Compensation?: boolean;
|
|
46
|
+
fovOffset?: number;
|
|
47
|
+
distanceScale?: number;
|
|
48
|
+
seed?: number;
|
|
49
|
+
children?: React.ReactNode;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Declarative 3D Shot-DSL Camera Rig for `@remotion/three` (`<ThreeCanvas>`).
|
|
53
|
+
* Resolves named `StageAnchor3D` targets and cinematic shot presets, automatically
|
|
54
|
+
* re-framing vertical `9:16` Shorts compositions into the Golden Safe Box (`y ∈ [220, 1380]`).
|
|
55
|
+
*/
|
|
56
|
+
export declare const DirectorCamera3D: React.FC<DirectorCamera3DProps>;
|