@remixmate/template-core 1.3.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/API.md +167 -0
- package/LICENSE +85 -0
- package/README.md +18 -0
- package/dist/index.cjs +2211 -0
- package/dist/index.d.mts +1293 -0
- package/dist/index.d.ts +1293 -0
- package/dist/index.js +2153 -0
- package/package.json +44 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1293 @@
|
|
|
1
|
+
import React, { FC } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Core type contracts — the stable interface between the DSL, the Remotion
|
|
5
|
+
* pipeline, and every template. All 50+ templates must conform to these types.
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ Evolution rules:
|
|
8
|
+
* 1. Only add optional fields. Never remove or change the meaning of a field.
|
|
9
|
+
* 2. Never add template-specific fields here. Use `entry.props` (opaque
|
|
10
|
+
* Record) instead — each template parses what it needs.
|
|
11
|
+
* 3. Breaking changes must ship as a versioned contract (e.g. V2) so that
|
|
12
|
+
* older templates remain renderable without modification.
|
|
13
|
+
*
|
|
14
|
+
* Migrated from ab-render/src/core/types.ts — field names and semantics must
|
|
15
|
+
* remain 100% identical so existing templates continue to compile unchanged.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
interface Typography {
|
|
19
|
+
titleFont?: string;
|
|
20
|
+
bodyFont?: string;
|
|
21
|
+
titleSize?: number;
|
|
22
|
+
bodySize?: number;
|
|
23
|
+
primaryColor?: string;
|
|
24
|
+
secondaryColor?: string;
|
|
25
|
+
/** Accent for filled chrome (badge background, callout rule). Kept separate
|
|
26
|
+
* from `primaryColor` so templates that render text over full-bleed media
|
|
27
|
+
* can set the text white without turning the badge white-on-white. */
|
|
28
|
+
accentColor?: string;
|
|
29
|
+
}
|
|
30
|
+
interface SubtitleSegment {
|
|
31
|
+
text: string;
|
|
32
|
+
startFrame: number;
|
|
33
|
+
endFrame: number;
|
|
34
|
+
/**
|
|
35
|
+
* 这一段属于原旁白的第几**句**(0-based)。
|
|
36
|
+
*
|
|
37
|
+
* 为什么需要它:一段字幕 **不等于** 一句话。`segment_narration` 先在句末断行,
|
|
38
|
+
* 再把超过 30 字的长句在逗号处继续切 —— 切的目的是让 TTS 按真实停顿返回时间戳,
|
|
39
|
+
* 与"句"无关。于是一段 35 字的句子会变成 3 段字幕。
|
|
40
|
+
*
|
|
41
|
+
* 靠下标把"第 i 个动作"对齐到"第 i 段字幕"的模板(math-principles-expl 的 steps、
|
|
42
|
+
* ppt-to-video 的 beats)因此会整体错位:2026-09-23 线上一条鸡兔同笼的成片里
|
|
43
|
+
* 10 句旁白被切成约 19 段,板面的 10 步在音频播到一半时就走完了,此后画面冻结
|
|
44
|
+
* 而旁白继续 —— 全程零报错、零日志、终态 completed。
|
|
45
|
+
*
|
|
46
|
+
* 有了这个字段,那些模板就能对齐到**句**(每句取其首段的 startFrame),
|
|
47
|
+
* 而字幕分段仍然自由。
|
|
48
|
+
*
|
|
49
|
+
* 老 RenderPlan 没有这个字段,消费方必须能回落到按段下标对齐。
|
|
50
|
+
*/
|
|
51
|
+
sentenceIndex?: number;
|
|
52
|
+
}
|
|
53
|
+
interface Layer {
|
|
54
|
+
layerId: string;
|
|
55
|
+
type: "visual" | "audio" | "text" | "overlay";
|
|
56
|
+
assetId?: string;
|
|
57
|
+
zIndex: number;
|
|
58
|
+
startFrame: number;
|
|
59
|
+
endFrame: number;
|
|
60
|
+
position?: {
|
|
61
|
+
x: number;
|
|
62
|
+
y: number;
|
|
63
|
+
};
|
|
64
|
+
size?: {
|
|
65
|
+
width: number;
|
|
66
|
+
height: number;
|
|
67
|
+
};
|
|
68
|
+
opacity?: number;
|
|
69
|
+
animation?: Record<string, unknown>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A single scene on the timeline. The `props` field is an opaque container —
|
|
73
|
+
* each template parses it into its own strongly-typed interface. Core does
|
|
74
|
+
* NOT know what fields any given template expects.
|
|
75
|
+
*/
|
|
76
|
+
interface TimelineEntry {
|
|
77
|
+
sceneId: string;
|
|
78
|
+
compositionId: string;
|
|
79
|
+
startFrame: number;
|
|
80
|
+
endFrame: number;
|
|
81
|
+
durationFrames: number;
|
|
82
|
+
startTime: number;
|
|
83
|
+
endTime: number;
|
|
84
|
+
/** Opaque props bag — template-specific data. Never add fields here. */
|
|
85
|
+
props: Record<string, unknown>;
|
|
86
|
+
/**
|
|
87
|
+
* @deprecated 历史字段,新产出端不再写入。保留为可选 + 默认空数组,仅为向后
|
|
88
|
+
* 兼容旧的 RenderPlan JSON 反序列化。模板若仍读 entry.layers 应改用 props.*。
|
|
89
|
+
*/
|
|
90
|
+
layers?: Layer[];
|
|
91
|
+
subtitleSegments: SubtitleSegment[];
|
|
92
|
+
transition: {
|
|
93
|
+
type: string;
|
|
94
|
+
durationFrames: number;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
interface ResolvedAsset {
|
|
98
|
+
assetId: string;
|
|
99
|
+
type: "image" | "video" | "audio" | "avatar" | "subtitle" | "bgm";
|
|
100
|
+
source: string;
|
|
101
|
+
status: string;
|
|
102
|
+
url?: string;
|
|
103
|
+
localPath?: string;
|
|
104
|
+
duration?: number;
|
|
105
|
+
width?: number;
|
|
106
|
+
height?: number;
|
|
107
|
+
mimeType?: string;
|
|
108
|
+
/**
|
|
109
|
+
* 结构化旁白(`audio.narration` 的 `{intro, items, outro}` 写法)切出的总行数。
|
|
110
|
+
* 由渲染管线在解析 TTS 时写入;模板据此把字幕行对齐到卡片下标。
|
|
111
|
+
*/
|
|
112
|
+
narrationLineCount?: number;
|
|
113
|
+
/** 同上,其中属于开场铺垫(intro)的行数——这些行不对应任何卡片。 */
|
|
114
|
+
narrationIntroLines?: number;
|
|
115
|
+
}
|
|
116
|
+
interface RenderConfig {
|
|
117
|
+
width: number;
|
|
118
|
+
height: number;
|
|
119
|
+
fps: number;
|
|
120
|
+
totalFrames: number;
|
|
121
|
+
totalDuration: number;
|
|
122
|
+
codec: string;
|
|
123
|
+
crf: number;
|
|
124
|
+
outputFormat: string;
|
|
125
|
+
}
|
|
126
|
+
interface BgmConfig {
|
|
127
|
+
url: string;
|
|
128
|
+
volume?: number;
|
|
129
|
+
}
|
|
130
|
+
interface MainVideoProps {
|
|
131
|
+
timeline: TimelineEntry[];
|
|
132
|
+
renderConfig: RenderConfig;
|
|
133
|
+
resolvedAssets: ResolvedAsset[];
|
|
134
|
+
globalTypography?: Typography;
|
|
135
|
+
motionPreset?: "minimal" | "smooth" | "energetic" | "cinematic";
|
|
136
|
+
colorScheme?: string[];
|
|
137
|
+
variantId?: string;
|
|
138
|
+
bgm?: BgmConfig;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Every template Composition component must implement this function signature.
|
|
142
|
+
*
|
|
143
|
+
* Template-specific data flows through `entry.props` (opaque). Each template
|
|
144
|
+
* should parse it into its own typed interface at the top of its Composition:
|
|
145
|
+
*
|
|
146
|
+
* const props = entry.props as MyTemplateProps;
|
|
147
|
+
*/
|
|
148
|
+
interface TemplateCompositionProps {
|
|
149
|
+
entry: TimelineEntry;
|
|
150
|
+
resolvedAssets: ResolvedAsset[];
|
|
151
|
+
typography?: Typography;
|
|
152
|
+
variantId?: string;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Describes a single entry in the generated COMPOSITION_MANIFEST. The manifest
|
|
156
|
+
* maps a compositionId (e.g. "ImageSlideOpening") to the React component that
|
|
157
|
+
* should render it, along with the template it belongs to and which slot it
|
|
158
|
+
* fills.
|
|
159
|
+
*
|
|
160
|
+
* This type is consumed by:
|
|
161
|
+
* - the built-in template package — its generated manifest.ts uses it as the
|
|
162
|
+
* value type of the COMPOSITION_MANIFEST record.
|
|
163
|
+
* - `ab-render` — `resolveComposition()` looks up entries by compositionId.
|
|
164
|
+
*/
|
|
165
|
+
interface CompositionManifestEntry {
|
|
166
|
+
component: FC<TemplateCompositionProps>;
|
|
167
|
+
templateId: string;
|
|
168
|
+
slot: "opening" | "point" | "ending" | "cover";
|
|
169
|
+
/**
|
|
170
|
+
* The owning template's primary aspect ratio (`supportedAspectRatios[0]`
|
|
171
|
+
* from its template.json), e.g. `"16:9"` / `"9:16"` / `"3:4"`.
|
|
172
|
+
*
|
|
173
|
+
* For **preview hosts only**. Production rendering takes the aspect from
|
|
174
|
+
* the DSL's `renderConfig`, never from here. It exists so a preview host
|
|
175
|
+
* (apps/studio) doesn't have to keep a second, hand-maintained
|
|
176
|
+
* template-id → aspect map: that copy silently defaulted new templates to
|
|
177
|
+
* 16:9 when an author forgot to register them.
|
|
178
|
+
*
|
|
179
|
+
* Optional so that older generated manifests — and any consumer building
|
|
180
|
+
* entries by hand — keep type-checking. Per the evolution rules at the top
|
|
181
|
+
* of this file, this is an additive change.
|
|
182
|
+
*/
|
|
183
|
+
defaultAspectRatio?: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
type BackgroundMotion = "static" | "kenburns-in" | "kenburns-out" | "pan-left" | "pan-right";
|
|
187
|
+
interface BackgroundProps {
|
|
188
|
+
src: string | null;
|
|
189
|
+
type?: "image" | "video" | "color";
|
|
190
|
+
color?: string;
|
|
191
|
+
gradient?: string;
|
|
192
|
+
motion?: BackgroundMotion;
|
|
193
|
+
durationFrames: number;
|
|
194
|
+
/**
|
|
195
|
+
* 缺 src / 加载失败时铺的底色,缺省 `#1a1a2e`。
|
|
196
|
+
*
|
|
197
|
+
* 模板自己在下面垫了底(板面、主题色)时传 `"transparent"`,失败时透出的就是
|
|
198
|
+
* 那层底,而不是一块和主题无关的藏青色。
|
|
199
|
+
*/
|
|
200
|
+
fallbackColor?: string;
|
|
201
|
+
}
|
|
202
|
+
declare const Background: React.FC<BackgroundProps>;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* SceneBackgroundLayer —— 场景级背景层的通用实现。
|
|
206
|
+
*
|
|
207
|
+
* ─── 为什么收在 core ────────────────────────────────────────────────────
|
|
208
|
+
* 这套逻辑此前是 html-slide 与 html-slide-blackboard 各自一份(246 / 228 行)。
|
|
209
|
+
* 两份的结构完全一致,差异只有「签名层」(透视网格 + 景深 ↔ 板面 + 木框)与
|
|
210
|
+
* theme 适配类型 —— 也就是说通用的 5 个 preset、媒体蒙版、图层顺序被复制了
|
|
211
|
+
* 两遍。第三个模板(github-repo-rank)要用同一套能力时,要么再复制一份、把
|
|
212
|
+
* 同源漂移债从 2 份变成 3 份,要么就是现在这样收进 core。
|
|
213
|
+
*
|
|
214
|
+
* 收进来的是**图层顺序与 preset 语义**这两件属于渲染契约的事;各模板的签名
|
|
215
|
+
* 视觉仍留在自己包里,通过 `signature` / `depth` 两个插槽注入。
|
|
216
|
+
*
|
|
217
|
+
* ─── 图层顺序(自底向上)────────────────────────────────────────────────
|
|
218
|
+
* 1. canvasBg 兜底色(`preset: "none"` 不画 —— 那一档的语义就是完全不铺底)
|
|
219
|
+
* 2. preset 对应层(solid / gradient / image / video / noise)
|
|
220
|
+
* 签名 preset(grid / board / static)在这一层**什么都不画**,它由
|
|
221
|
+
* `signature` / `depth` 插槽负责
|
|
222
|
+
* 3. 图片 / 视频的方向性压暗蒙版
|
|
223
|
+
* 4. `signature` 插槽
|
|
224
|
+
* 5. `depth` 插槽(`preset: "none"` 不画)
|
|
225
|
+
*
|
|
226
|
+
* 两个插槽都是**渲染函数**而不是 ReactNode,因为它们需要知道当前 preset 才能
|
|
227
|
+
* 决定画多少:html-slide 的网格在 `preset==="grid"` 时要画底色渐变、作为叠加
|
|
228
|
+
* 层时只画线;blackboard 的 BoardSurface 要按 preset 分别决定画不画板底色、
|
|
229
|
+
* 画不画粉笔质感。把这个判断留给模板,core 只保证顺序。
|
|
230
|
+
*/
|
|
231
|
+
/** `noise` preset 的混合模式。见各模板 theme 里 noiseBlend 的注释。 */
|
|
232
|
+
type SceneBackgroundNoiseBlend = "screen" | "multiply";
|
|
233
|
+
/**
|
|
234
|
+
* 场景背景的声明。
|
|
235
|
+
*
|
|
236
|
+
* `preset` 故意是宽松的 `string`:签名 preset 的名字由模板自己定(html-slide
|
|
237
|
+
* 是 `"grid"`、blackboard 是 `"board"`、github-repo-rank 是 `"static"`),
|
|
238
|
+
* core 不该枚举它们。各模板在自己的 types.ts 里用字面量联合收窄,并在
|
|
239
|
+
* template.json 的 schema 里落成 enum。
|
|
240
|
+
*
|
|
241
|
+
* 模板专属的开关(html-slide 的 `gridOverlay`、blackboard 的 `woodFrame` 等)
|
|
242
|
+
* **不放在这里** —— 它们留在各模板自己的 background 类型里,由包装组件读取后
|
|
243
|
+
* 以 props 形式传给本组件。这样既不污染共享类型,也不改动已有 DSL 的字段名。
|
|
244
|
+
*/
|
|
245
|
+
interface SceneBackground {
|
|
246
|
+
/** 背景样式。缺省由 `signaturePreset` 决定。`none` = 完全不铺底(透明)。 */
|
|
247
|
+
preset?: string;
|
|
248
|
+
/** preset = "solid": 填充色(hex / rgb / rgba / 颜色名)。 */
|
|
249
|
+
color?: string;
|
|
250
|
+
/**
|
|
251
|
+
* preset = "solid" / "gradient": 完整的 CSS `background` 字符串,覆盖
|
|
252
|
+
* `color`。用来写 `linear-gradient(...)` / `radial-gradient(...)`。
|
|
253
|
+
*/
|
|
254
|
+
cssBackground?: string;
|
|
255
|
+
/** preset = "image": 经 resolvedAssets 解析的资产 id。 */
|
|
256
|
+
assetRef?: string;
|
|
257
|
+
/** preset = "image": assetRef 解析不到时的回退直链。 */
|
|
258
|
+
imageUrl?: string;
|
|
259
|
+
/** preset = "image": Ken-Burns / 摇移运镜。缺省由 `defaultMediaMotion` 决定。 */
|
|
260
|
+
motion?: BackgroundMotion;
|
|
261
|
+
/** preset = "image" / "video": 高斯模糊半径(px),让前景文字更清晰。 */
|
|
262
|
+
blurPx?: number;
|
|
263
|
+
/**
|
|
264
|
+
* 图片 / 视频之上的压暗蒙版,0..1。image / video 缺省 0.35,其余为 0;设 0 关闭。
|
|
265
|
+
*
|
|
266
|
+
* 蒙版是**自上而下的斜坡**(顶部约取该值的 45%,下三分之一给足),所以这个
|
|
267
|
+
* 数字表达的是「字幕区的压暗程度」而不是整帧的平铺色。平铺 0.55(早先的
|
|
268
|
+
* 缺省值)在真实视频背景上实测把整帧压到平均亮度 10.4/255 —— 花钱渲的素材
|
|
269
|
+
* 等于看不见。
|
|
270
|
+
*/
|
|
271
|
+
overlayOpacity?: number;
|
|
272
|
+
/** 蒙版颜色,配合 overlayOpacity。缺省由 `defaultOverlayColor` 决定。 */
|
|
273
|
+
overlayColor?: string;
|
|
274
|
+
/** preset = "video": 经 resolvedAssets 解析的资产 id。 */
|
|
275
|
+
videoAssetRef?: string;
|
|
276
|
+
/** preset = "video": videoAssetRef 解析不到时的回退直链。 */
|
|
277
|
+
videoUrl?: string;
|
|
278
|
+
/** preset = "video": 是否静音。缺省 true(背景视频应当让旁白主导音频)。 */
|
|
279
|
+
videoMuted?: boolean;
|
|
280
|
+
/** preset = "video": 是否循环。缺省 true。 */
|
|
281
|
+
videoLoop?: boolean;
|
|
282
|
+
/** preset = "noise": 颗粒透明度 0..1。缺省 0.4。 */
|
|
283
|
+
noiseOpacity?: number;
|
|
284
|
+
}
|
|
285
|
+
/** 传给 `signature` / `depth` 插槽的上下文。 */
|
|
286
|
+
interface SceneBackgroundSlotContext {
|
|
287
|
+
/** 归一后的 preset(已应用 signaturePreset 缺省值)。 */
|
|
288
|
+
preset: string;
|
|
289
|
+
/** 当前是否就是该模板的签名 preset。 */
|
|
290
|
+
isSignaturePreset: boolean;
|
|
291
|
+
/** 当前 preset 是否为图片 / 视频 —— 它们自带压暗蒙版,景深要减半。 */
|
|
292
|
+
isMedia: boolean;
|
|
293
|
+
}
|
|
294
|
+
interface SceneBackgroundLayerProps {
|
|
295
|
+
background?: SceneBackground;
|
|
296
|
+
resolvedAssets: ResolvedAsset[];
|
|
297
|
+
durationFrames: number;
|
|
298
|
+
/** 最底层兜底色。 */
|
|
299
|
+
canvasBg: string;
|
|
300
|
+
/** `noise` preset 的混合模式。 */
|
|
301
|
+
noiseBlend: SceneBackgroundNoiseBlend;
|
|
302
|
+
/** 该模板签名 preset 的名字,同时作为 `background.preset` 的缺省值。 */
|
|
303
|
+
signaturePreset: string;
|
|
304
|
+
/**
|
|
305
|
+
* 裸 `gradient`(没给 color / cssBackground)的回退。
|
|
306
|
+
*
|
|
307
|
+
* 不给它就会退化成两端同色的 linear-gradient —— 一块和 `solid` 分不出来的
|
|
308
|
+
* 纯色。传模板 theme 自己设计的那条渐变。
|
|
309
|
+
*/
|
|
310
|
+
defaultGradient: string;
|
|
311
|
+
/**
|
|
312
|
+
* image / video 缺省运镜。
|
|
313
|
+
*
|
|
314
|
+
* html-slide 系传 `"kenburns-in"`(沿用原行为);对码率敏感、要求「静态优先」
|
|
315
|
+
* 的模板传 `"static"`,让 Ken-Burns 只在 DSL 显式声明 `motion` 时才发生。
|
|
316
|
+
*/
|
|
317
|
+
defaultMediaMotion?: BackgroundMotion;
|
|
318
|
+
/** 媒体蒙版的缺省颜色。传 theme 的暗调,让图片融进画面而不是浮在上面。 */
|
|
319
|
+
defaultOverlayColor?: string;
|
|
320
|
+
/**
|
|
321
|
+
* image / video 的缺省蒙版强度,0..1。默认 0.35。
|
|
322
|
+
*
|
|
323
|
+
* 这个值该多大取决于**前景**:
|
|
324
|
+
* - html-slide 系的正文有自己的底板/描边,0.35 足够,再高就把素材压没了
|
|
325
|
+
* (平铺 0.55 实测把整帧压到平均亮度 10.4/255)。
|
|
326
|
+
* - 前景是「半透明玻璃板 + 白字」的模板(github-repo-rank)必须更高:
|
|
327
|
+
* 玻璃板本身没有不透明底色,背景一亮,白字就直接糊在浅色板上。
|
|
328
|
+
*
|
|
329
|
+
* 所以它是**每模板标定**的量,不是一个全局常数。别在这里改默认值来迁就某个
|
|
330
|
+
* 模板 —— 传参覆盖。
|
|
331
|
+
*/
|
|
332
|
+
defaultOverlayOpacity?: number;
|
|
333
|
+
/** 签名层插槽(图层 4)。 */
|
|
334
|
+
signature?: (ctx: SceneBackgroundSlotContext) => React.ReactNode;
|
|
335
|
+
/** 景深 / 质感插槽(图层 5)。`preset: "none"` 时不调用。 */
|
|
336
|
+
depth?: (ctx: SceneBackgroundSlotContext) => React.ReactNode;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* `noise` preset 的胶片颗粒。
|
|
340
|
+
*
|
|
341
|
+
* 原来写死一张白色颗粒 + `mixBlendMode: overlay`,两处都是错的:overlay 在
|
|
342
|
+
* 近黑底上是数学恒等(实测 `noiseOpacity: 0.55` 时全图色差只有 3.3/255,参数
|
|
343
|
+
* 等于没生效),而白色颗粒在 `multiply` 下同样是恒等 —— 浅色底需要的是**黑色**
|
|
344
|
+
* 颗粒。所以颗粒色必须跟着混合模式走。
|
|
345
|
+
* 颗粒粗细也从 `baseFrequency=0.85` 放到 `0.6`:前者在 1080p 上细到接近编码
|
|
346
|
+
* 噪点,H.264 一压就没了。
|
|
347
|
+
*/
|
|
348
|
+
declare function grainDataUri(blend: SceneBackgroundNoiseBlend): string;
|
|
349
|
+
/**
|
|
350
|
+
* 纯函数版的 preset 解析 —— 把「给定 background 该画哪些层」的判断独立出来,
|
|
351
|
+
* 便于单测,也让组件本体只负责把结果摆成 DOM。
|
|
352
|
+
*/
|
|
353
|
+
declare function resolveSceneBackground(background: SceneBackground | undefined, signaturePreset: string, defaultMediaMotion?: BackgroundMotion, defaultOverlayOpacity?: number): {
|
|
354
|
+
preset: string;
|
|
355
|
+
isSignaturePreset: boolean;
|
|
356
|
+
isMedia: boolean;
|
|
357
|
+
paintCanvas: boolean;
|
|
358
|
+
motion: BackgroundMotion;
|
|
359
|
+
blurPx: number;
|
|
360
|
+
overlayOpacity: number;
|
|
361
|
+
};
|
|
362
|
+
declare const SceneBackgroundLayer: React.FC<SceneBackgroundLayerProps>;
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Subtitle presentation modes.
|
|
366
|
+
*
|
|
367
|
+
* - `dark` 半透明深色底 + 白字。任何背景都能读,是保底选项与默认值。
|
|
368
|
+
* - `light` 半透明浅色底 + 深色字。高调图 / 产品图 / 白底截图。
|
|
369
|
+
* - `stroke` 白字黑描边,无底框。画面本身是重点时用。
|
|
370
|
+
* - `scrim` 底部渐变压暗,无硬边框。对任意背景都鲁棒,观感最干净。
|
|
371
|
+
* - `none` 不渲染字幕。
|
|
372
|
+
* - `auto` 按背景亮度在 dark / light 之间选;拿不到亮度时回退 `scrim`。
|
|
373
|
+
*/
|
|
374
|
+
type SubtitleMode = "auto" | "dark" | "light" | "stroke" | "scrim" | "none";
|
|
375
|
+
/** Resolved modes — everything `auto` can collapse into. */
|
|
376
|
+
type ResolvedSubtitleMode = Exclude<SubtitleMode, "auto">;
|
|
377
|
+
/**
|
|
378
|
+
* Resolve the caption's distance from the bottom edge, in px.
|
|
379
|
+
*
|
|
380
|
+
* ─── Why this normalises on height, not on `useCanvasScale` ───────────────
|
|
381
|
+
* `useCanvasScale` divides by the **long edge**, which is right for type size
|
|
382
|
+
* (a value that should track the frame's overall size) but wrong for a purely
|
|
383
|
+
* vertical offset. In landscape the long edge is the *width*, so a caption
|
|
384
|
+
* authored at 80px drifted upward as the frame got wider: measured on real
|
|
385
|
+
* renders, the caption sat 11.5% of frame height above the bottom at 9:16 /
|
|
386
|
+
* 1:1 / 3:4 but 20.5% at 16:9 and 15.5% at 4:3 — i.e. "the same config" put
|
|
387
|
+
* the captions in visibly different places depending on aspect ratio.
|
|
388
|
+
*
|
|
389
|
+
* Dividing by height instead makes the ratio aspect-independent. Portrait and
|
|
390
|
+
* square are **unaffected**: when height >= width the long edge *is* the
|
|
391
|
+
* height, so the old and new expressions are identical term for term. Only
|
|
392
|
+
* landscape moves (16:9: 80px → 45px; 4:3: 60px → 45px).
|
|
393
|
+
*
|
|
394
|
+
* Split out as a pure function, and kept exported, for the same reason as
|
|
395
|
+
* `backgroundMotionStyle` in `Background.tsx`: the invariant ("one config, one
|
|
396
|
+
* height-relative position, every aspect ratio") is only worth having if it is
|
|
397
|
+
* unit-testable without mounting a Remotion frame.
|
|
398
|
+
*
|
|
399
|
+
* @param mode Already-resolved caption mode.
|
|
400
|
+
* @param canvas Frame dimensions from `useVideoConfig()`.
|
|
401
|
+
* @param bottomRatio Explicit override as a fraction of height; wins outright.
|
|
402
|
+
*/
|
|
403
|
+
declare function resolveSubtitleBottomPx(mode: ResolvedSubtitleMode, canvas: {
|
|
404
|
+
width: number;
|
|
405
|
+
height: number;
|
|
406
|
+
}, bottomRatio?: number): number;
|
|
407
|
+
/**
|
|
408
|
+
* Pick the concrete mode to render.
|
|
409
|
+
*
|
|
410
|
+
* Kept pure and exported so the `auto` thresholds and the legacy
|
|
411
|
+
* `captionStyle` bridge are unit-testable without a Remotion frame.
|
|
412
|
+
*
|
|
413
|
+
* @param mode Explicit request from the template / DSL.
|
|
414
|
+
* @param captionStyle Deprecated two-value prop, honoured when `mode` is absent.
|
|
415
|
+
* @param backgroundLuma Relative luminance (0–1) of the region behind the
|
|
416
|
+
* captions, when the pipeline can supply it. `auto` degrades to `scrim`
|
|
417
|
+
* without it — a gradient needs no knowledge of what it sits on.
|
|
418
|
+
*/
|
|
419
|
+
declare function resolveSubtitleMode(mode?: SubtitleMode, captionStyle?: "pill" | "stroke", backgroundLuma?: number): ResolvedSubtitleMode;
|
|
420
|
+
interface SubtitleBarProps {
|
|
421
|
+
segments: SubtitleSegment[];
|
|
422
|
+
style?: "bottom" | "center" | "karaoke";
|
|
423
|
+
typography?: Typography;
|
|
424
|
+
/**
|
|
425
|
+
* @deprecated Use `mode`. Kept so existing templates render unchanged:
|
|
426
|
+
* `pill` → `dark`, `stroke` → `stroke`.
|
|
427
|
+
*/
|
|
428
|
+
captionStyle?: "pill" | "stroke";
|
|
429
|
+
/** Presentation mode. Wins over `captionStyle` when both are given. */
|
|
430
|
+
mode?: SubtitleMode;
|
|
431
|
+
/**
|
|
432
|
+
* Distance from the bottom edge as a fraction of canvas height. Omit to keep
|
|
433
|
+
* the per-mode default. Exists so templates can nudge captions up off the
|
|
434
|
+
* screen edge without forking this component.
|
|
435
|
+
*/
|
|
436
|
+
bottomRatio?: number;
|
|
437
|
+
/** Multiplier on the resolved font size. */
|
|
438
|
+
fontScale?: number;
|
|
439
|
+
/** Max caption width, in percent of canvas width. */
|
|
440
|
+
maxWidthPct?: number;
|
|
441
|
+
/** Relative luminance (0–1) behind the captions; drives `auto`. */
|
|
442
|
+
backgroundLuma?: number;
|
|
443
|
+
}
|
|
444
|
+
declare const SubtitleBar: React.FC<SubtitleBarProps>;
|
|
445
|
+
|
|
446
|
+
interface TextLayerProps {
|
|
447
|
+
role: "title" | "subtitle" | "badge" | "caption" | "callout";
|
|
448
|
+
content: string;
|
|
449
|
+
animation?: "fade-in" | "slide-up" | "typewriter" | "zoom-in" | "none";
|
|
450
|
+
typography?: Typography;
|
|
451
|
+
startFrame: number;
|
|
452
|
+
endFrame: number;
|
|
453
|
+
/** 与数字人同屏时标题/副标题置顶,避免与人物重叠 */
|
|
454
|
+
verticalLayout?: "center" | "top";
|
|
455
|
+
/** 相对居中位置的垂直偏移(px,向下为正,随画幅缩放)。
|
|
456
|
+
* 每个 role 各自渲染在一层 AbsoluteFill 上,靠 padding 挤位置会算错——
|
|
457
|
+
* 同屏多层时由调用方给出确定偏移,避免标题与副标题重叠。 */
|
|
458
|
+
offsetY?: number;
|
|
459
|
+
/**
|
|
460
|
+
* 覆盖该 role 的默认 `textShadow`。
|
|
461
|
+
*
|
|
462
|
+
* 存在的理由:各 role 的默认阴影是一层模糊投影,压在**亮色**配图上撑不起
|
|
463
|
+
* 对比度 —— image-slide 的白色标题在亮底图上实测只有 1.32:1。调用方需要能
|
|
464
|
+
* 换成描边式阴影(同 SubtitleBar 的 stroke 档,实测 20.62:1),而这属于
|
|
465
|
+
* 「同一 role 在不同底图上的表现」,不是新增一个 role。
|
|
466
|
+
*
|
|
467
|
+
* 传空字符串可显式去掉阴影。
|
|
468
|
+
*/
|
|
469
|
+
textShadow?: string;
|
|
470
|
+
}
|
|
471
|
+
declare const TextLayer: React.FC<TextLayerProps>;
|
|
472
|
+
|
|
473
|
+
interface SceneTransitionProps {
|
|
474
|
+
type: string;
|
|
475
|
+
durationFrames: number;
|
|
476
|
+
direction: "in" | "out";
|
|
477
|
+
children: React.ReactNode;
|
|
478
|
+
}
|
|
479
|
+
declare const SceneTransition: React.FC<SceneTransitionProps>;
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* GridParticleBackground —— 「网格 + 粒子 + 角落光晕」背景,竖屏卡片类模板共用。
|
|
483
|
+
*
|
|
484
|
+
* preset:grid-particles | grid-only | particles-only | gradient | noise | solid | image
|
|
485
|
+
*
|
|
486
|
+
* ─── 为什么收在 core ────────────────────────────────────────────────────
|
|
487
|
+
* 此前 spotlight-card 与 screen-walkthrough 各一份(432 / 443 行,相似 78%),已经
|
|
488
|
+
* 各自演化:spotlight-card 修了「背景图加载失败退回渐变底」,screen-walkthrough 没有;
|
|
489
|
+
* 两边的差异其余全是调参(暗角中心、顶/底柔光带、粒子数)。调参收进 `tuning`,
|
|
490
|
+
* 逻辑只留一份,修一次两边都好。
|
|
491
|
+
*
|
|
492
|
+
* 模板各自的 theme 类型只要满足 `GridParticleTheme` 的字段即可(结构类型),不需要
|
|
493
|
+
* 改名或继承。
|
|
494
|
+
*/
|
|
495
|
+
/** 本组件读取的主题字段。颜色都是 `"r,g,b"` 三元组,便于拼 rgba。 */
|
|
496
|
+
interface GridParticleTheme {
|
|
497
|
+
/** 底色(CSS background,通常是径向渐变)。 */
|
|
498
|
+
bgGradient: string;
|
|
499
|
+
gridCenterRGB: string;
|
|
500
|
+
gridEdgeRGB: string;
|
|
501
|
+
particleRGB: string;
|
|
502
|
+
/** 四角光晕,依次为 左上 / 右上 / 左下 / 右下。 */
|
|
503
|
+
haloColors: readonly string[];
|
|
504
|
+
/** 暗角与柔光带的颜色(深色主题用黑,浅色主题不能吃黑)。 */
|
|
505
|
+
scrimRGB: string;
|
|
506
|
+
vignetteAlpha: number;
|
|
507
|
+
noiseBlend: "screen" | "multiply";
|
|
508
|
+
}
|
|
509
|
+
/** DSL 里 `customPayload.background` 的形状(与模板 schema 同名字段)。 */
|
|
510
|
+
interface GridParticleConfig {
|
|
511
|
+
preset?: string;
|
|
512
|
+
/** 0~1.5,控制亮度 / 密度。不影响 solid / image 的底层颜色或图片本身。 */
|
|
513
|
+
intensity?: number;
|
|
514
|
+
color?: string;
|
|
515
|
+
cssBackground?: string;
|
|
516
|
+
assetRef?: string;
|
|
517
|
+
overlayOpacity?: number;
|
|
518
|
+
}
|
|
519
|
+
/** 按版面调的几何参数。不同模板的文字 / 舞台位置不同,暗角和柔光带要跟着挪。 */
|
|
520
|
+
interface GridParticleTuning {
|
|
521
|
+
particleCount: number;
|
|
522
|
+
/** 暗角椭圆中心的纵向位置,如 `"45%"`。 */
|
|
523
|
+
vignetteCenterY: string;
|
|
524
|
+
topBand: {
|
|
525
|
+
height: string;
|
|
526
|
+
strength: number;
|
|
527
|
+
};
|
|
528
|
+
bottomBand: {
|
|
529
|
+
height: string;
|
|
530
|
+
strength: number;
|
|
531
|
+
midStrength: number;
|
|
532
|
+
};
|
|
533
|
+
/** 颗粒滤镜的 SVG id。 */
|
|
534
|
+
noiseFilterId: string;
|
|
535
|
+
}
|
|
536
|
+
interface GridParticleBackgroundProps {
|
|
537
|
+
theme: GridParticleTheme;
|
|
538
|
+
config?: GridParticleConfig;
|
|
539
|
+
resolvedAssets?: ResolvedAsset[];
|
|
540
|
+
/**
|
|
541
|
+
* 这一屏底部是否真的有文字。false 时不画底部柔光带。
|
|
542
|
+
*
|
|
543
|
+
* 那条带原本恒画,与暗角叠加后把竖屏下三分之一压成死黑(实测底部 90% 处
|
|
544
|
+
* 亮度 2.1/255,网格和粒子在下半屏完全消失)。它是为底部文字铺地板的,
|
|
545
|
+
* 没有文字就没有理由付这个代价 —— 封面就是典型:底部一个字都没有。
|
|
546
|
+
*/
|
|
547
|
+
hasBottomText?: boolean;
|
|
548
|
+
tuning?: Partial<GridParticleTuning>;
|
|
549
|
+
/** 用于降级诊断的模板 id。 */
|
|
550
|
+
templateId?: string;
|
|
551
|
+
}
|
|
552
|
+
declare const GridParticleBackground: React.FC<GridParticleBackgroundProps>;
|
|
553
|
+
|
|
554
|
+
interface AudioLayerProps {
|
|
555
|
+
src: string | null;
|
|
556
|
+
startFrame: number;
|
|
557
|
+
durationFrames: number;
|
|
558
|
+
volume?: number;
|
|
559
|
+
fadeIn?: number;
|
|
560
|
+
fadeOut?: number;
|
|
561
|
+
}
|
|
562
|
+
declare const AudioLayer: React.FC<AudioLayerProps>;
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* 时间轴派发器 —— 把 timeline 的每个 entry 按 compositionId 解析成模板组件,
|
|
566
|
+
* 包进 Remotion Sequence,再叠加可选的全局 BGM。
|
|
567
|
+
*
|
|
568
|
+
* ## 为什么它在 core 里
|
|
569
|
+
*
|
|
570
|
+
* 这段逻辑此前有**两份**实现:
|
|
571
|
+
* - `ab-render/src/core/compositions/MainVideo.tsx`(内置模板,预构建 bundle)
|
|
572
|
+
* - `ab-render/server/draft-renderer.ts` 里拼字符串生成的 `DraftMainVideo`
|
|
573
|
+
* (私有模板,请求时动态 bundle)
|
|
574
|
+
*
|
|
575
|
+
* 动态 bundle 引不到 ab-render 的源码,只能引 `remotion` / `react` /
|
|
576
|
+
* `@ab-templates/core`,所以当初复刻了一份,靠一段 "SYNC CONTRACT" 注释和
|
|
577
|
+
* `ENTRY_GENERATOR_VERSION` 的人肉纪律维持一致。代价是:
|
|
578
|
+
* - 复刻版和生产版实际上已经漂移(见下方 `spreadEntryProps`)
|
|
579
|
+
* - CLI 因此规定「内置模板绝不走 /renderDraft」,两条渲染路径无法统一,
|
|
580
|
+
* 而路径统一是「发模板零部署」的前提
|
|
581
|
+
*
|
|
582
|
+
* 把它下沉到 core,两边各自传自己的 `resolve` 即可复用同一份实现 —— 这正是
|
|
583
|
+
* 原 MainVideo.tsx 注释里写明的终局方案。
|
|
584
|
+
*
|
|
585
|
+
* ## 唯一的行为差异:spreadEntryProps
|
|
586
|
+
*
|
|
587
|
+
* 平台契约是「模板从 `entry.props` 读数据」。但草稿路径额外把 `entry.props`
|
|
588
|
+
* 摊平到组件顶层 props,兼容作者常写的「直接读顶层 prop」。生产路径没有这层兜底。
|
|
589
|
+
*
|
|
590
|
+
* 合并时没有把这个差异藏掉,而是提成显式参数:调用方自己声明要不要兜底。
|
|
591
|
+
* 摊平发生在四个平台 prop **之后**,因此 entry.props 里若有同名键会覆盖它们
|
|
592
|
+
* (视作场景级覆盖,与合并前的草稿行为一致)。
|
|
593
|
+
*/
|
|
594
|
+
interface TimelineSequencerProps {
|
|
595
|
+
timeline: TimelineEntry[];
|
|
596
|
+
resolvedAssets: ResolvedAsset[];
|
|
597
|
+
/** compositionId → 组件;找不到时返回 undefined,该 Sequence 渲染为空。 */
|
|
598
|
+
resolve: (compositionId: string) => React.FC<TemplateCompositionProps> | undefined;
|
|
599
|
+
globalTypography?: Typography;
|
|
600
|
+
variantId?: string;
|
|
601
|
+
bgm?: BgmConfig;
|
|
602
|
+
/**
|
|
603
|
+
* 是否把 `entry.props` 摊平到组件顶层 props(兼容「直接读顶层 prop」的写法)。
|
|
604
|
+
* 默认 false —— 与平台契约一致;草稿路径显式传 true 保持既有兜底。
|
|
605
|
+
*/
|
|
606
|
+
spreadEntryProps?: boolean;
|
|
607
|
+
}
|
|
608
|
+
declare const TimelineSequencer: React.FC<TimelineSequencerProps>;
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Resolve an assetId to a playable URL (or local file path for public/).
|
|
612
|
+
* Returns null when the asset is unknown.
|
|
613
|
+
*
|
|
614
|
+
* This is core infrastructure: the asset pipeline is a platform concern,
|
|
615
|
+
* not a template concern. Do not fork per template.
|
|
616
|
+
*/
|
|
617
|
+
declare function useAssetUrl(assetId: string | undefined, resolvedAssets: ResolvedAsset[]): string | null;
|
|
618
|
+
/** Non-hook variant for use inside loops / .map callbacks. */
|
|
619
|
+
declare function resolveAssetUrl(assetId: string | undefined, resolvedAssets: ResolvedAsset[]): string | null;
|
|
620
|
+
/** Return the full ResolvedAsset (or undefined). Useful when the template
|
|
621
|
+
* needs `type`, `duration`, or other metadata in addition to the URL. */
|
|
622
|
+
declare function resolveAsset(assetId: string | undefined, resolvedAssets: ResolvedAsset[]): ResolvedAsset | undefined;
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Return the subtitle text currently active at the playhead, or an empty
|
|
626
|
+
* string if none. Pure data hook — visual rendering is each template's
|
|
627
|
+
* responsibility (so templates can style the subtitle bar freely).
|
|
628
|
+
*/
|
|
629
|
+
declare function useActiveSubtitle(segments: SubtitleSegment[]): string;
|
|
630
|
+
/**
|
|
631
|
+
* Return the index of the currently active segment (scene-local frames),
|
|
632
|
+
* or -1 if none. Useful when a template wants to link subtitle progress
|
|
633
|
+
* to on-screen element highlighting.
|
|
634
|
+
*/
|
|
635
|
+
declare function useActiveSubtitleIndex(segments: SubtitleSegment[]): number;
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* 把 RenderPlan 里的**全片绝对帧**字幕时间码平移成**场景局部帧**。
|
|
639
|
+
*
|
|
640
|
+
* ─── 为什么必须收在 core ─────────────────────────────────────────────
|
|
641
|
+
* 这五行位移曾经被逐字复制到 9 个模板里,然后长出了三种互不相同的空值
|
|
642
|
+
* 处理:两处写 `subtitleSegments ?? []`、六处裸 `.map()`、一处包在
|
|
643
|
+
* useMemo 里。裸 `.map()` 那几个在 `subtitleSegments` 缺失的 DSL 上会直接
|
|
644
|
+
* 抛 TypeError,而写了防御的两个模板的注释明说这种 DSL 真实存在。
|
|
645
|
+
* 位移规则属于渲染管线契约(帧号基准在哪),不是模板的表达自由。
|
|
646
|
+
*
|
|
647
|
+
* ─── 关于 `subtitleSegments` 的可选性 ───────────────────────────────
|
|
648
|
+
* `TimelineEntry.subtitleSegments` 在类型上是必填,这里却在运行时兜底
|
|
649
|
+
* `?? []`。看着矛盾,但这是刻意的:把字段改成可选是对 core 契约的破坏性
|
|
650
|
+
* 变更(见 types.ts 顶部的演进规则,需要走 V2),而线上确实存在缺这个
|
|
651
|
+
* 字段的旧 RenderPlan。折中做法是类型不动、兜底集中在这一个函数里 ——
|
|
652
|
+
* 于是模板侧不需要再各自判空。
|
|
653
|
+
*/
|
|
654
|
+
interface LocalizeSubtitlesOptions {
|
|
655
|
+
/**
|
|
656
|
+
* 把字幕起点再提前若干帧。TTS 音频开头常有 ~0.3s 静音,字幕按音频时间码
|
|
657
|
+
* 对齐会显得"慢半拍",提前一点点观感更跟得上。
|
|
658
|
+
*
|
|
659
|
+
* 只影响 `startFrame`,`endFrame` 保持不变(提前出字,不提前收字)。
|
|
660
|
+
* 默认 0。
|
|
661
|
+
*/
|
|
662
|
+
leadFrames?: number;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* 纯函数版本 —— 可在循环 / 非组件上下文 / 测试里调用。
|
|
666
|
+
*
|
|
667
|
+
* @param entry 当前场景的 timeline entry(只读 startFrame 与 subtitleSegments)
|
|
668
|
+
* @param options 见 {@link LocalizeSubtitlesOptions}
|
|
669
|
+
* @returns 帧号已平移到场景局部的字幕段(原数组不被修改)
|
|
670
|
+
*/
|
|
671
|
+
declare function localizeSubtitles(entry: Pick<TimelineEntry, "subtitleSegments" | "startFrame">, options?: LocalizeSubtitlesOptions): SubtitleSegment[];
|
|
672
|
+
/**
|
|
673
|
+
* Hook 版本 —— 结果按 (segments, startFrame, leadFrames) 记忆化。
|
|
674
|
+
*
|
|
675
|
+
* 记忆化不是可有可无的优化:`useActiveSubtitle` / `useActiveSubtitleIndex`
|
|
676
|
+
* 的 useMemo 依赖里有 `segments`,每帧新建一个数组会让它们的缓存每帧失效。
|
|
677
|
+
*/
|
|
678
|
+
declare function useLocalSubtitles(entry: Pick<TimelineEntry, "subtitleSegments" | "startFrame">, options?: LocalizeSubtitlesOptions): SubtitleSegment[];
|
|
679
|
+
/**
|
|
680
|
+
* 每**句**旁白的起拍帧 —— 给"第 i 个动作对齐第 i 句"的模板用。
|
|
681
|
+
*
|
|
682
|
+
* ─── 为什么不能直接用 `segments.map(s => s.startFrame)` ──────────────
|
|
683
|
+
* 那是**段**的起点,而段 ≠ 句。`segment_narration` 在句末断行之后,还会把
|
|
684
|
+
* 超过 30 字的长句在逗号处继续切(为了让 TTS 按真实停顿返回时间戳)。于是:
|
|
685
|
+
*
|
|
686
|
+
* 旁白 10 句 ──segment_narration──▶ 字幕 19 段
|
|
687
|
+
* steps 10 步 ──按段下标对齐──────▶ 第 10 步落在第 10 段 ≈ 音频中点
|
|
688
|
+
* 后 9 段没有任何步骤 → 画面冻结
|
|
689
|
+
*
|
|
690
|
+
* 这就是 2026-09-23 线上那条鸡兔同笼成片"配音和画面错位"的全部机制:
|
|
691
|
+
* 作者写了 10 步、旁白 10 句,契约上一一对应,而实现对齐的是段。
|
|
692
|
+
* 零报错、零日志、renderJob completed。
|
|
693
|
+
*
|
|
694
|
+
* ─── 回落 ────────────────────────────────────────────────────────────
|
|
695
|
+
* 老 RenderPlan 的段上没有 `sentenceIndex`。那时无法区分段与句,只能退回
|
|
696
|
+
* 按段下标对齐(即旧行为)——**不要**在这里假装成功,调用方需要知道自己
|
|
697
|
+
* 拿到的是句还是段,所以回落由调用方显式处理,本函数在缺字段时返回 null。
|
|
698
|
+
*
|
|
699
|
+
* @returns 每句的起拍帧(升序,长度 = 句数);段上没有 `sentenceIndex` 时返回 null
|
|
700
|
+
*/
|
|
701
|
+
declare function sentenceStartFrames(segments: readonly SubtitleSegment[]): number[] | null;
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Scale factor for px constants authored against a 1920px long edge.
|
|
705
|
+
*
|
|
706
|
+
* Normalising on the **long** edge keeps a given px value visually equivalent
|
|
707
|
+
* across orientations: 1920×1080 (16:9) and 1080×1920 (9:16) both return 1.0,
|
|
708
|
+
* while 1:1 at 1080×1080 returns 0.5625 so type does not overwhelm the smaller
|
|
709
|
+
* canvas.
|
|
710
|
+
*
|
|
711
|
+
* @example
|
|
712
|
+
* const scale = useCanvasScale();
|
|
713
|
+
* fontSize: 40 * scale
|
|
714
|
+
*/
|
|
715
|
+
declare function useCanvasScale(): number;
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* CJK-safe font stack normalisation.
|
|
719
|
+
*
|
|
720
|
+
* Template metadata historically carried values like `"PingFang SC Semibold"`,
|
|
721
|
+
* which is **not** a CSS family name — the family is `PingFang SC` and
|
|
722
|
+
* `Semibold` is a weight. The browser fails to match it, and because the value
|
|
723
|
+
* is non-empty the usual `font || "fallback"` guard never fires, so the text
|
|
724
|
+
* silently lands on the renderer's default font. On top of that, PingFang only
|
|
725
|
+
* exists on macOS while remote renders run on Linux.
|
|
726
|
+
*
|
|
727
|
+
* `fontStack` normalises whatever the metadata provides into a valid stack that
|
|
728
|
+
* always ends in a CJK-capable fallback.
|
|
729
|
+
*/
|
|
730
|
+
/**
|
|
731
|
+
* Normalise a metadata font value into a valid CSS `font-family` stack.
|
|
732
|
+
*
|
|
733
|
+
* Accepts either a single family (`"PingFang SC Semibold"`) or an
|
|
734
|
+
* already-composed stack (`"PingFang SC Regular, Noto Sans SC, sans-serif"`).
|
|
735
|
+
* Entries are quoted, de-weighted twins are appended behind each original, CJK
|
|
736
|
+
* fallbacks are inserted next, and any generic family closes out the stack.
|
|
737
|
+
*
|
|
738
|
+
* Intended for CJK body / display text. Monospace stacks are written inline by
|
|
739
|
+
* the templates that need them and should not be routed through here.
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* fontStack("PingFang SC Semibold")
|
|
743
|
+
* // '"PingFang SC Semibold", "PingFang SC", "Noto Sans SC", "Noto Sans CJK SC", sans-serif'
|
|
744
|
+
*/
|
|
745
|
+
declare function fontStack(font?: string): string;
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* 模板降级诊断 —— `template.scene.degraded` 的唯一发射点。
|
|
749
|
+
*
|
|
750
|
+
* 协议(ab-platform docs/engineering/logging-standards-and-governance-design.md §3):浏览器里
|
|
751
|
+
* `console.warn("__diagnostic_v1__ " + JSON)`,ab-render 的 browser-diagnostics 只认
|
|
752
|
+
* `template_id / scene_id / slide_id / reason` 四个字段(再补 task_id),其余一律丢弃。
|
|
753
|
+
*
|
|
754
|
+
* 为什么要收进 core:此前 7 个模板各写一份,字段名已经分裂——spotlight-card /
|
|
755
|
+
* ppt-to-video / picture-book-en 写的是 `template` 而不是 `template_id`,于是它们的降级
|
|
756
|
+
* 到服务端全变成 `template_id: "[unknown]"`,按模板统计降级率时这三个模板直接消失。
|
|
757
|
+
* 字段名、去重、URL 脱敏由这里统一保证,模板只说「哪个场景、为什么」。
|
|
758
|
+
*
|
|
759
|
+
* 去重:组件体每帧都会执行,不去重就逐帧刷屏。按 模板 + 场景 + 版式 + 原因 + 去重键
|
|
760
|
+
* 去重;`dedupeKey` 给「同一原因下还要区分对象」的情形(如按图片地址区分加载失败)。
|
|
761
|
+
*/
|
|
762
|
+
interface SceneDegradedReport {
|
|
763
|
+
/**
|
|
764
|
+
* 模板 id,与 template.json 的 templateId 一致。模板调用**必须**传。
|
|
765
|
+
* 只有 core 原语(如 Background)可以省略:它不知道自己跑在哪个模板里,
|
|
766
|
+
* 服务端按 task_id 关联回模板。
|
|
767
|
+
*/
|
|
768
|
+
templateId?: string;
|
|
769
|
+
/** 降级原因,snake_case,如 `image_load_failed`。 */
|
|
770
|
+
reason: string;
|
|
771
|
+
sceneId?: string;
|
|
772
|
+
slideId?: string;
|
|
773
|
+
/**
|
|
774
|
+
* 额外去重维度,不上报。典型用法是图片地址:同一场景两张图都挂了要各报一次。
|
|
775
|
+
* 传 URL 也安全——只参与去重,不会离开浏览器。
|
|
776
|
+
*/
|
|
777
|
+
dedupeKey?: string;
|
|
778
|
+
/**
|
|
779
|
+
* 附加信息,会写进浏览器控制台(本地 studio 排查用),服务端不转发。
|
|
780
|
+
* 其中的 `src` 会被脱敏(去掉 query / hash;data URI 只留 MIME 前缀)。
|
|
781
|
+
*/
|
|
782
|
+
detail?: Record<string, string | number | boolean | undefined>;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* 把一个媒体地址压成可以进日志的形态:去掉签名 query 与 hash,data URI 只保留
|
|
786
|
+
* `data:image/png` 这样的前缀(正文可能有几百 KB)。
|
|
787
|
+
*/
|
|
788
|
+
declare function sanitizeDiagnosticSrc(src: string): string;
|
|
789
|
+
declare function reportSceneDegraded(report: SceneDegradedReport): void;
|
|
790
|
+
/** 测试专用:清空去重表。 */
|
|
791
|
+
declare function resetSceneDegradedReports(): void;
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* 字幕分屏 —— 把一条过长的字幕段切成若干「屏」,每屏都保证一行放得下。
|
|
795
|
+
*
|
|
796
|
+
* ─── 为什么必须在渲染层切 ─────────────────────────────────────────────
|
|
797
|
+
* 字幕段的长度由旁白作者决定:`narration.items` 的每一行会被**原样**喂给
|
|
798
|
+
* Minimax,TTS 返回的一段就等于作者写的那一行,全链路(含 dsl_validator)
|
|
799
|
+
* 没有任何长度约束。一条四五十字的旁白在任何模板里都会糊成两三行。
|
|
800
|
+
*
|
|
801
|
+
* 也不能挪到 CLI 去切:`_auto_highlight_map` 要求
|
|
802
|
+
* `len(subtitleSegments) == narrationLineCount`,在那一层切会让这个 1:1 断掉,
|
|
803
|
+
* 结果是「整屏卡片全部变暗、没有一张点亮」,而且只留一条 warn 日志。在渲染层
|
|
804
|
+
* 切则天然安全 —— 高亮联动消费的仍是未切分的原始 segments,下标语义不变。
|
|
805
|
+
*
|
|
806
|
+
* CLI 层切也拿不到精度补偿:Minimax 只返回**整行**的 timeBegin / timeEnd,
|
|
807
|
+
* 行内没有更细的时间戳,所以哪一层都只能按字符比例估片内时间。
|
|
808
|
+
*
|
|
809
|
+
* ─── 为什么是解析式估宽而不是量出来 ───────────────────────────────────
|
|
810
|
+
* Remotion 渲的是**单帧**,`useEffect` / ResizeObserver 那套在出图时不可靠,
|
|
811
|
+
* 量不到真实宽度。宽度用一张实测的逐字符 em 表估(见 `ASCII_EM`),可单测。
|
|
812
|
+
* 同样的取舍见各模板的 `utils/overflow.ts`。
|
|
813
|
+
*
|
|
814
|
+
* ─── 容量为什么是参数而不是常量 ───────────────────────────────────────
|
|
815
|
+
* 「一行放得下几个字」取决于字号、字距和可用宽度,而这三者逐模板、逐画幅、
|
|
816
|
+
* 逐 caption 档都不一样(实测 1920×1080:html-slide 的胶囊能放 44 个全角字,
|
|
817
|
+
* html-slide-blackboard 的板书字幕只能放 37)。所以这里只接受 `maxUnitsPerLine`
|
|
818
|
+
* 参数,由调用方用 {@link estimateLineCapacity} 从自己的排版参数算出来。
|
|
819
|
+
*/
|
|
820
|
+
/** 一屏字幕。帧号与来源 `SubtitleSegment` 同基准(场景局部帧)。 */
|
|
821
|
+
interface SubtitleChunk {
|
|
822
|
+
text: string;
|
|
823
|
+
startFrame: number;
|
|
824
|
+
endFrame: number;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* 单屏目标上限的默认值。
|
|
828
|
+
*
|
|
829
|
+
* 取 20 而不是「一行容量」(即「只在真会换行时才切」)是刻意的:三四十个字
|
|
830
|
+
* 一屏占满画宽,不换行也读不完。20 与管线里既有的口径同一档
|
|
831
|
+
* (`segment_narration` 的 MAX_SEGMENT_CHARS=16、`_rebalance_subtitle_segments`
|
|
832
|
+
* 的 _SUB_MAX_CHARS=18)。
|
|
833
|
+
*
|
|
834
|
+
* 它是「一屏读得完多少字」的**阅读**口径,与画幅字号无关 —— 随排版变的是
|
|
835
|
+
* `maxUnitsPerLine`。
|
|
836
|
+
*/
|
|
837
|
+
declare const DEFAULT_SPLIT_MAX_UNITS = 20;
|
|
838
|
+
/** 碎片下限的默认值:切出来短于它的片并回相邻片,避免闪过一个「的延迟」。 */
|
|
839
|
+
declare const DEFAULT_MIN_CHUNK_UNITS = 7;
|
|
840
|
+
/**
|
|
841
|
+
* 单屏最多占一行容量的多少。
|
|
842
|
+
*
|
|
843
|
+
* ─── 为什么需要这道封顶 ───────────────────────────────────────────────
|
|
844
|
+
* `DEFAULT_SPLIT_MAX_UNITS` 是个**绝对字数**,而「这条字幕看起来多满」是个
|
|
845
|
+
* **相对值**,两者在窄画幅下会撞车。实测各模板在 1080p 下的单屏占行宽:
|
|
846
|
+
* 16:9 的 ppt-to-video 34%、image-slide 50%、html-slide 45% —— 宽松
|
|
847
|
+
* 9:16 的 image-slide 91%、3:4 的 math-principles-expl 100% —— 顶满
|
|
848
|
+
* 后者意味着**切分阈值正好等于换行阈值,余量为零**。
|
|
849
|
+
*
|
|
850
|
+
* 而 `displayWidth` 终究是解析式估算:字体在渲染机上可能回落成 Noto Sans SC,
|
|
851
|
+
* 拉丁部分会漂。顶满行时一漂就换行,这整套切分的目的也就落空了。
|
|
852
|
+
*
|
|
853
|
+
* ─── 0.9 这个数是怎么来的 ─────────────────────────────────────────────
|
|
854
|
+
* 这里曾经是 0.8,那是按**旧估宽器 ±35% 的误差**定的(半角一律 0.5)。换成
|
|
855
|
+
* 逐字符表之后误差降到 2.5%,而且真实旁白的宽度构成是:
|
|
856
|
+
* CJK 90.7%(advance 恒等于字号,误差 0.0%) / 拉丁数字 9.3%(会随字体漂)
|
|
857
|
+
* 即使拉丁部分整体漂 15%,整句误差上限也只有 **1.4%**。留两成余量去防 1.4%
|
|
858
|
+
* 的误差是 14 倍的过度保守,而代价很实在 —— 它把窄画幅的单屏上限压到 12~17,
|
|
859
|
+
* 逼着切分算法降级到顿号 / 空格 / 硬切上去切。实测同一组旁白:
|
|
860
|
+
* 阈值 20 → 切分点全在冒号与逗号上(零弱切分点)
|
|
861
|
+
* 阈值 16 → 开始出现顿号、空格
|
|
862
|
+
* 阈值 12 → 还要加上硬切
|
|
863
|
+
* 也就是说过度保守的余量会直接把「并列项被拦腰切开」那个缺陷做回来。
|
|
864
|
+
*
|
|
865
|
+
* 0.9 之后窄画幅单屏占行宽 86%~90%,对 1.4% 的误差仍有 10 倍余量。
|
|
866
|
+
* 横屏完全不受影响(容量 40 以上时 0.9×容量 > 20,绝对值仍是更紧的约束)。
|
|
867
|
+
*/
|
|
868
|
+
declare const LINE_FILL_LIMIT = 0.9;
|
|
869
|
+
/**
|
|
870
|
+
* 文本的显示宽度,单位是「全角字」。
|
|
871
|
+
*
|
|
872
|
+
* 0x1100 之前是 ASCII / 拉丁 / 希腊 / 西里尔这些窄字形,之后(谚文字母、CJK、
|
|
873
|
+
* 假名、全角形式、emoji)一律按全角算 —— CJK 的 advance 恒等于字号,这一条
|
|
874
|
+
* 实测误差 0.0%,与最终落到哪个中文字体无关。
|
|
875
|
+
*/
|
|
876
|
+
declare function displayWidth(text: string): number;
|
|
877
|
+
/**
|
|
878
|
+
* 文本的**口播权重**,单位近似「音节」。片内时间按它分配,不用 `displayWidth`。
|
|
879
|
+
*
|
|
880
|
+
* ─── 为什么不能用显示宽度分时间 ───────────────────────────────────────
|
|
881
|
+
* 「占多宽」和「读多久」是两回事,而且方向相反:`Agent Loop` 占 5.3 个字宽、
|
|
882
|
+
* 只有 3 个音节。实测一段 5.0s 的旁白切成两屏:
|
|
883
|
+
* 「支撑这一切的核心只有四个字」 宽 13.0 → 分到 2.67s,实际要读 3.00s
|
|
884
|
+
* 「Agent Loop,智能体循环」 宽 11.5 → 分到 2.33s,实际只读 2.00s
|
|
885
|
+
* 边界漂了 **0.33s**,而且只在含拉丁 / 数字的片上漂。线上实测的换屏同步离散度
|
|
886
|
+
* (P10 −0.24s / P90 +0.20s,10% 的换屏晚于语音 0.2s 以上)主要就来自这里。
|
|
887
|
+
*
|
|
888
|
+
* ─── 权重怎么定的 ─────────────────────────────────────────────────────
|
|
889
|
+
* · CJK:一字一音节,1.0。
|
|
890
|
+
* · 数字:中文数词会膨胀(`768` 读「七百六十八」是 5 个音节 / 3 个字符),也可能
|
|
891
|
+
* 逐位读(「七六八」3 个)。取中间值 1.2。
|
|
892
|
+
* · 拉丁:**全大写的短串按字母读**(`AI`=2、`HNSW`=4 个音节),所以 1.0/字母;
|
|
893
|
+
* 其余按词读,英文平均约 3 个字母一个音节,取 0.33/字母。这一条不是锦上添花 ——
|
|
894
|
+
* 本领域的旁白里 `AI` / `IVF` / `HNSW` 这类缩写极常见,一律按 0.33 算会把它们
|
|
895
|
+
* 的时间砍掉三分之二。
|
|
896
|
+
* · 标点与空格:0。句内逗号确实会带来 ~0.15s 的停顿,但那比这里要修的 0.33s
|
|
897
|
+
* 小一个量级,再加一个拍脑袋的权重只会让模型更难证伪。
|
|
898
|
+
*
|
|
899
|
+
* 导出是为了能脱离 Remotion 单测这套权重本身。
|
|
900
|
+
*/
|
|
901
|
+
declare function spokenWeight(text: string): number;
|
|
902
|
+
/**
|
|
903
|
+
* 从排版参数估算一行放得下多少个全角字。
|
|
904
|
+
*
|
|
905
|
+
* CJK 字形的 advance 恒等于字号,所以这个公式与最终落到哪个中文字体无关 ——
|
|
906
|
+
* 这一点对渲染机尤其要紧:模板声明的手写体 / 特殊字体在 Linux 上常常回落到
|
|
907
|
+
* PingFang / Noto Sans SC,但容量不变。
|
|
908
|
+
*
|
|
909
|
+
* 实测校验(1920×1080):
|
|
910
|
+
* · 可用宽 1632、字号 36、字距 0.8 → 44.3 → 44,实测第 45 个字换行 ✓
|
|
911
|
+
* · 可用宽 1629.8、字号 42、字距 1 → 37.9 → 37,实测第 38 个字换行 ✓
|
|
912
|
+
*
|
|
913
|
+
* @param availableWidthPx 文本可用宽度。注意 `max-width: N%` 默认作用在
|
|
914
|
+
* **content box** 上,所以横向 padding **不**从这里扣(实测:85% 的胶囊
|
|
915
|
+
* 在 1920 画面下内容宽恰为 1632px,连同 88px 内边距总宽 1720px)。
|
|
916
|
+
* @param fontSizePx 已经乘过 canvas scale 的最终字号。
|
|
917
|
+
* @param letterSpacingPx 已经乘过 canvas scale 的最终字距,默认 0。
|
|
918
|
+
*/
|
|
919
|
+
declare function estimateLineCapacity(availableWidthPx: number, fontSizePx: number, letterSpacingPx?: number): number;
|
|
920
|
+
interface SplitSubtitleOptions {
|
|
921
|
+
/**
|
|
922
|
+
* 一行放得下的显示宽度上限(全角=1、半角=0.5)。用
|
|
923
|
+
* {@link estimateLineCapacity} 从自己的排版参数算出来。
|
|
924
|
+
*/
|
|
925
|
+
maxUnitsPerLine: number;
|
|
926
|
+
/** 单屏目标上限。默认 {@link DEFAULT_SPLIT_MAX_UNITS}。 */
|
|
927
|
+
splitMaxUnits?: number;
|
|
928
|
+
/** 碎片下限。默认 {@link DEFAULT_MIN_CHUNK_UNITS}。 */
|
|
929
|
+
minChunkUnits?: number;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* 把一条字幕文本切成若干屏,并去掉每片的行尾标点。
|
|
933
|
+
*
|
|
934
|
+
* 瀑布式降级:`BREAK_LEVELS` 逐级(句末 → 子句 → 逗号 → 顿号 → 空格)→ 硬切。
|
|
935
|
+
* 每一级**只处理上一级没切动的片**,所以有冒号的句子永远轮不到顿号那一级,
|
|
936
|
+
* 也就不会把并列项拦腰切开。
|
|
937
|
+
*
|
|
938
|
+
* 不需要切时返回单元素数组(仍会去掉行尾标点)。
|
|
939
|
+
*/
|
|
940
|
+
declare function splitSubtitleText(text: string, options: SplitSubtitleOptions): string[];
|
|
941
|
+
interface ChunkSubtitleOptions extends SplitSubtitleOptions {
|
|
942
|
+
/**
|
|
943
|
+
* 单屏最短驻留帧数。按比例分下来低于它就**放弃切这一段**:读不完就切走
|
|
944
|
+
* 比多占一行更糟。默认 0(关闭这道阀门)。
|
|
945
|
+
*/
|
|
946
|
+
minFrames?: number;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* 把字幕段数组展开成「屏」数组。不需要切的段原样透传(同一个对象引用)。
|
|
950
|
+
*/
|
|
951
|
+
declare function chunkSubtitleSegments(segments: SubtitleChunk[], options: ChunkSubtitleOptions): SubtitleChunk[];
|
|
952
|
+
/**
|
|
953
|
+
* 整条字幕 run 的淡入淡出不透明度(0–1),头尾各淡 `capFrames` 帧。
|
|
954
|
+
*
|
|
955
|
+
* ─── 为什么不直接写 interpolate ───────────────────────────────────────
|
|
956
|
+
* Remotion 的 `inputRange` 要求**严格**递增,相邻相等就抛
|
|
957
|
+
* `inputRange must be strictly monotonically increasing`。而「头尾各淡 N 帧」
|
|
958
|
+
* 的四点式 `[s, s+fade, e-fade, e]` 在 fade 取 `floor(runLength / 2)` 时,
|
|
959
|
+
* **runLength 为 2..2·cap 之间的偶数**一律退化成相邻相等:
|
|
960
|
+
* runLength=4 → [0,2,2,4] runLength=16 → [0,8,8,16]
|
|
961
|
+
* 也就是说任何字幕 run 恰好是这些长度的场景都会把整条渲染抛崩。这个坑在
|
|
962
|
+
* 本仓库被踩过两次(2026-08-29 修的是 segments 为空那一半),所以收在这里,
|
|
963
|
+
* 让每个需要 run 级淡入淡出的调用方共用同一份、并且可单测。
|
|
964
|
+
*
|
|
965
|
+
* 取 `floor((runLength - 1) / 2)` 保证 `2·fade < runLength`,四点严格递增。
|
|
966
|
+
* fade 不足 1 帧时(run 只有一两帧)直接返回 1:淡入淡出本来也看不出来,
|
|
967
|
+
* 但字幕必须可见 —— 返回 0 会让它整条消失。
|
|
968
|
+
*/
|
|
969
|
+
declare function resolveRunFadeOpacity(frame: number, runStart: number, runEnd: number, capFrames: number): number;
|
|
970
|
+
/**
|
|
971
|
+
* 换屏时文字的淡入不透明度(0–1)。
|
|
972
|
+
*
|
|
973
|
+
* ─── 为什么起点不是 0 ─────────────────────────────────────────────────
|
|
974
|
+
* 底板是 run 级常驻的(见 `resolveRunFadeOpacity`),文字才是逐屏换的。
|
|
975
|
+
* 文字从 0 起跳意味着**每屏的第一帧只剩一条空底板**——2026-09-21 从线上成片
|
|
976
|
+
* 里抽帧,确实抽到了好几张「胶囊在、字不在」。单次只有 33ms,但一条两分钟的
|
|
977
|
+
* 片子有几十屏,就是几十次闪。
|
|
978
|
+
*
|
|
979
|
+
* 从 0.35 起跳既保留了「换了一下」的柔和感,又不会出现空底板那一帧。
|
|
980
|
+
*
|
|
981
|
+
* @param framesIntoChunk 当前帧减去本屏的 startFrame。
|
|
982
|
+
*/
|
|
983
|
+
declare function resolveChunkFadeOpacity(framesIntoChunk: number): number;
|
|
984
|
+
/**
|
|
985
|
+
* 当前帧该显示哪一屏。
|
|
986
|
+
*
|
|
987
|
+
* 两处与「找到落在区间里的那一段」不同:
|
|
988
|
+
* 1. **句间停顿保持**:帧落在两屏之间的空隙里时,只要空隙不超过
|
|
989
|
+
* `holdFrames` 就继续显示上一屏。TTS 的句间停顿通常 100–300ms,
|
|
990
|
+
* 不保持的话整条字幕会消失再弹出,切屏之后这种闪烁会成倍变多。
|
|
991
|
+
* 2. **只在片与片之间保持**:最后一屏播完就是播完,不会赖在屏幕上占住
|
|
992
|
+
* 场景尾部那 1.5s 的静音尾巴。
|
|
993
|
+
*/
|
|
994
|
+
declare function resolveActiveChunk<T extends SubtitleChunk>(chunks: T[], frame: number, holdFrames: number): T | undefined;
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* 数字人(口播 talking-head 视频)叠加层的数据契约。
|
|
998
|
+
*
|
|
999
|
+
* 设计文档:template-library/docs/design/slide-digital-human.md。
|
|
1000
|
+
*
|
|
1001
|
+
* 三类输入来源(优先级见 `resolveDigitalHuman`):
|
|
1002
|
+
* - `digitalHuman`:作者在 `customPayload.digitalHuman` 写的**外观**配置;
|
|
1003
|
+
* - `avatarAssetId / avatarPosition / avatarScale`:propExtractors 从通用 DSL
|
|
1004
|
+
* 的 `visuals.avatar.{assetRef, position, scale}` 抽出;
|
|
1005
|
+
* - `avatarOffsetFrames / avatarSegmentRole`:渲染管线为「连续段」注入的时间信息。
|
|
1006
|
+
*/
|
|
1007
|
+
type DigitalHumanShape = "rect" | "rounded" | "circle";
|
|
1008
|
+
type DigitalHumanAnchor = "top-left" | "top-center" | "top-right" | "center-left" | "center" | "center-right" | "bottom-left" | "bottom-center" | "bottom-right";
|
|
1009
|
+
type DigitalHumanPosition = DigitalHumanAnchor | "custom";
|
|
1010
|
+
type DigitalHumanSizePreset = "small" | "medium" | "large" | "full";
|
|
1011
|
+
type DigitalHumanLayout = "overlay" | "reserve";
|
|
1012
|
+
type DigitalHumanOnEnd = "freeze" | "hide" | "loop";
|
|
1013
|
+
type DigitalHumanEnter = "none" | "fade" | "pop" | "slide";
|
|
1014
|
+
/** 本场景在所属连续段里的位置。段长为 1 时是 `only`。 */
|
|
1015
|
+
type DigitalHumanSegmentRole = "only" | "first" | "middle" | "last";
|
|
1016
|
+
/** `customPayload.digitalHuman` —— 作者可写的外观配置,全部可选。 */
|
|
1017
|
+
interface DigitalHumanConfig {
|
|
1018
|
+
/** 显式关闭(素材存在也不显示)。默认 true。 */
|
|
1019
|
+
enabled?: boolean;
|
|
1020
|
+
/** 锚点预设;`custom` 时用 x / y。缺省取模板默认。 */
|
|
1021
|
+
position?: DigitalHumanPosition;
|
|
1022
|
+
/** position=custom 时盒子**中心点**,占画面宽的比例(0–1)。 */
|
|
1023
|
+
x?: number;
|
|
1024
|
+
/** position=custom 时盒子**中心点**,占画面高的比例(0–1)。 */
|
|
1025
|
+
y?: number;
|
|
1026
|
+
/** 锚点预设时距画面(安全区)边缘的距离,占短边比例。默认 0.03。 */
|
|
1027
|
+
margin?: number;
|
|
1028
|
+
/** 在锚点基础上的水平微调,占画面宽比例,可负。 */
|
|
1029
|
+
offsetX?: number;
|
|
1030
|
+
/** 在锚点基础上的垂直微调,占画面高比例,可负。 */
|
|
1031
|
+
offsetY?: number;
|
|
1032
|
+
/** 盒子高度:预设档位,或占画面短边的比例(0.1–1)。 */
|
|
1033
|
+
size?: DigitalHumanSizePreset | number;
|
|
1034
|
+
/** 盒子宽高比 w/h。circle 强制为 1;其余缺省取视频元数据,再缺省 3/4。 */
|
|
1035
|
+
aspectRatio?: number;
|
|
1036
|
+
/** 形状,默认 rounded。 */
|
|
1037
|
+
shape?: DigitalHumanShape;
|
|
1038
|
+
/** rounded 的圆角,占盒子短边比例。默认 0.08。 */
|
|
1039
|
+
cornerRadius?: number;
|
|
1040
|
+
/** 描边宽度 px@1920 长边,0 关闭。rounded / circle 默认 4,rect 默认 0。 */
|
|
1041
|
+
borderWidth?: number;
|
|
1042
|
+
/** 描边颜色,缺省由模板给(通常是主题强调色)。 */
|
|
1043
|
+
borderColor?: string;
|
|
1044
|
+
/** 投影。默认 true。 */
|
|
1045
|
+
shadow?: boolean;
|
|
1046
|
+
/** 裁切焦点(objectPosition)水平,0–1。默认 0.5。 */
|
|
1047
|
+
focusX?: number;
|
|
1048
|
+
/** 裁切焦点(objectPosition)垂直,0–1。默认 0.3(脸部偏上)。 */
|
|
1049
|
+
focusY?: number;
|
|
1050
|
+
/** 画面在盒子内的放大倍数(1–3)。默认 1。 */
|
|
1051
|
+
zoom?: number;
|
|
1052
|
+
/** overlay=浮在内容之上(默认);reserve=内容区让出数字人所在一侧。 */
|
|
1053
|
+
layout?: DigitalHumanLayout;
|
|
1054
|
+
/** 视频比场景短时的处理。默认 freeze。 */
|
|
1055
|
+
onEnd?: DigitalHumanOnEnd;
|
|
1056
|
+
/** 入场动效,默认 fade。连续段里只有段首场景播放。 */
|
|
1057
|
+
enter?: DigitalHumanEnter;
|
|
1058
|
+
/** 数字人自带音轨是否出声。默认 false —— 口型音轨就是旁白,旁白另有 AudioLayer。 */
|
|
1059
|
+
useVideoAudio?: boolean;
|
|
1060
|
+
/** 直链兜底,avatarAssetId 缺失或解析失败时使用。 */
|
|
1061
|
+
src?: string;
|
|
1062
|
+
/** 手动指定起播秒数;给了就覆盖管线注入的 avatarOffsetFrames。 */
|
|
1063
|
+
startFromSec?: number;
|
|
1064
|
+
/** Cover 用的静态图资产 id。 */
|
|
1065
|
+
posterAssetRef?: string;
|
|
1066
|
+
/** Cover 用的静态图直链。 */
|
|
1067
|
+
posterUrl?: string;
|
|
1068
|
+
}
|
|
1069
|
+
/** 从 props 里收集到的原始输入(尚未校验)。 */
|
|
1070
|
+
interface DigitalHumanInput {
|
|
1071
|
+
digitalHuman?: unknown;
|
|
1072
|
+
avatarAssetId?: unknown;
|
|
1073
|
+
avatarPosition?: unknown;
|
|
1074
|
+
avatarScale?: unknown;
|
|
1075
|
+
avatarOffsetFrames?: unknown;
|
|
1076
|
+
avatarSegmentRole?: unknown;
|
|
1077
|
+
}
|
|
1078
|
+
/** 校验、合并默认值之后的外观配置 —— 每个字段都有确定值。 */
|
|
1079
|
+
interface ResolvedDigitalHumanConfig {
|
|
1080
|
+
position: DigitalHumanPosition;
|
|
1081
|
+
x: number;
|
|
1082
|
+
y: number;
|
|
1083
|
+
margin: number;
|
|
1084
|
+
offsetX: number;
|
|
1085
|
+
offsetY: number;
|
|
1086
|
+
/** 盒高 / 画面短边。 */
|
|
1087
|
+
sizeRatio: number;
|
|
1088
|
+
/** 显式给的宽高比;undefined 表示跟随视频元数据。 */
|
|
1089
|
+
aspectRatio?: number;
|
|
1090
|
+
shape: DigitalHumanShape;
|
|
1091
|
+
cornerRadius: number;
|
|
1092
|
+
borderWidth: number;
|
|
1093
|
+
borderColor?: string;
|
|
1094
|
+
shadow: boolean;
|
|
1095
|
+
focusX: number;
|
|
1096
|
+
focusY: number;
|
|
1097
|
+
zoom: number;
|
|
1098
|
+
layout: DigitalHumanLayout;
|
|
1099
|
+
onEnd: DigitalHumanOnEnd;
|
|
1100
|
+
enter: DigitalHumanEnter;
|
|
1101
|
+
useVideoAudio: boolean;
|
|
1102
|
+
}
|
|
1103
|
+
/** 模板默认值:每个模板可以有自己的默认位置 / 大小 / 形状。 */
|
|
1104
|
+
type DigitalHumanDefaults = Partial<Pick<DigitalHumanConfig, "position" | "size" | "shape" | "margin" | "layout" | "borderColor" | "enter" | "onEnd" | "focusX" | "focusY">>;
|
|
1105
|
+
/** 画面上的像素盒子。 */
|
|
1106
|
+
interface DigitalHumanBox {
|
|
1107
|
+
left: number;
|
|
1108
|
+
top: number;
|
|
1109
|
+
width: number;
|
|
1110
|
+
height: number;
|
|
1111
|
+
borderRadius: number;
|
|
1112
|
+
/** 盒子偏向画面哪一侧;字幕 / 内容让位据此决定。 */
|
|
1113
|
+
side: "left" | "right" | "none";
|
|
1114
|
+
/** 计算结果超出画面、被钳制过。 */
|
|
1115
|
+
clamped: boolean;
|
|
1116
|
+
}
|
|
1117
|
+
/** 安全区内缩(px)。例如黑板模板的木框、字幕带。 */
|
|
1118
|
+
interface DigitalHumanInsets {
|
|
1119
|
+
top?: number;
|
|
1120
|
+
right?: number;
|
|
1121
|
+
bottom?: number;
|
|
1122
|
+
left?: number;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* 数字人配置的纯函数:收集 → 校验 → 合并 → 解析素材 / 起播帧。
|
|
1127
|
+
*
|
|
1128
|
+
* 全部无副作用、不依赖 React,方便单测;降级上报由调用方(模板 / DigitalHumanLayer)做。
|
|
1129
|
+
*/
|
|
1130
|
+
|
|
1131
|
+
declare const DIGITAL_HUMAN_ANCHORS: readonly DigitalHumanAnchor[];
|
|
1132
|
+
/** 尺寸档位:盒高 / 画面短边。 */
|
|
1133
|
+
declare const DIGITAL_HUMAN_SIZE_PRESETS: Record<DigitalHumanSizePreset, number>;
|
|
1134
|
+
/** props 里与数字人有关的全部键。多版式模板据此把它们当 frame 级字段。 */
|
|
1135
|
+
declare const DIGITAL_HUMAN_PROP_KEYS: readonly ["digitalHuman", "avatarAssetId", "avatarPosition", "avatarScale", "avatarOffsetFrames", "avatarSegmentRole"];
|
|
1136
|
+
/**
|
|
1137
|
+
* 从 `entry.props` 收集数字人输入。
|
|
1138
|
+
*
|
|
1139
|
+
* binder 有三种形态:字段摊平在顶层 / 整包塞进 `templateData` / 嵌套多层
|
|
1140
|
+
* `templateData`。按「外层优先」逐层回捞,外层空值不盖住内层真值。
|
|
1141
|
+
*/
|
|
1142
|
+
declare function pickDigitalHumanInput(props: unknown): DigitalHumanInput;
|
|
1143
|
+
/**
|
|
1144
|
+
* 旧版 image-slide 形态:只给了 `avatarAssetId`,没有任何新字段。
|
|
1145
|
+
* image-slide 据此走与改动前逐像素一致的 legacy 版式。
|
|
1146
|
+
*/
|
|
1147
|
+
declare function isLegacyAvatarInput(input: DigitalHumanInput): boolean;
|
|
1148
|
+
interface SanitizedDigitalHumanConfig {
|
|
1149
|
+
config: DigitalHumanConfig;
|
|
1150
|
+
/** 被丢弃或钳制的字段名。 */
|
|
1151
|
+
issues: string[];
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* 校验 `customPayload.digitalHuman`:枚举不合法的丢弃、数值越界的钳制,
|
|
1155
|
+
* 类型不对的丢弃。未知字段忽略(不报)。
|
|
1156
|
+
*/
|
|
1157
|
+
declare function sanitizeDigitalHumanConfig(raw: unknown): SanitizedDigitalHumanConfig;
|
|
1158
|
+
/**
|
|
1159
|
+
* 合并外观配置。优先级:`digitalHuman.*` > `avatarPosition / avatarScale` > 模板默认 > 内置默认。
|
|
1160
|
+
*/
|
|
1161
|
+
declare function mergeDigitalHumanConfig(config: DigitalHumanConfig, defaults: DigitalHumanDefaults, dsl?: {
|
|
1162
|
+
position?: DigitalHumanAnchor;
|
|
1163
|
+
scale?: number;
|
|
1164
|
+
}): ResolvedDigitalHumanConfig;
|
|
1165
|
+
interface ResolveDigitalHumanOptions {
|
|
1166
|
+
fps: number;
|
|
1167
|
+
defaults?: DigitalHumanDefaults;
|
|
1168
|
+
}
|
|
1169
|
+
interface ResolvedDigitalHuman {
|
|
1170
|
+
/** 有可播放的视频且未被关闭。 */
|
|
1171
|
+
active: boolean;
|
|
1172
|
+
/** 作者没有显式 `enabled: false`。 */
|
|
1173
|
+
enabled: boolean;
|
|
1174
|
+
src: string | null;
|
|
1175
|
+
asset?: ResolvedAsset;
|
|
1176
|
+
posterSrc: string | null;
|
|
1177
|
+
config: ResolvedDigitalHumanConfig;
|
|
1178
|
+
/** 视频起播帧(`startFrom`)。 */
|
|
1179
|
+
startFrame: number;
|
|
1180
|
+
role: DigitalHumanSegmentRole;
|
|
1181
|
+
/** 视频总帧数;资产没有 duration 时为 undefined。 */
|
|
1182
|
+
videoFrames?: number;
|
|
1183
|
+
/** 视频像素尺寸(有元数据时)。 */
|
|
1184
|
+
videoSize?: {
|
|
1185
|
+
width: number;
|
|
1186
|
+
height: number;
|
|
1187
|
+
};
|
|
1188
|
+
/** 越界 / 非法字段名。 */
|
|
1189
|
+
issues: string[];
|
|
1190
|
+
/** 给了 assetId 但解析不到 URL。 */
|
|
1191
|
+
unresolvedAssetId?: string;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* 从 props 解析出这一屏的数字人。
|
|
1195
|
+
*
|
|
1196
|
+
* 素材:`avatarAssetId` → `digitalHuman.src`。
|
|
1197
|
+
* 起播帧:`digitalHuman.startFromSec` > `avatarOffsetFrames` > 0。
|
|
1198
|
+
* `ResolvedAsset.duration` 按**毫秒**解释(与 gen-voice 的 audio_length_ms 同单位)。
|
|
1199
|
+
*/
|
|
1200
|
+
declare function resolveDigitalHuman(props: unknown, resolvedAssets: ResolvedAsset[], options: ResolveDigitalHumanOptions): ResolvedDigitalHuman;
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* 数字人盒子的几何计算,以及字幕 / 内容据此让位的辅助函数。纯函数。
|
|
1204
|
+
*/
|
|
1205
|
+
|
|
1206
|
+
/** 没有显式宽高比、也没有视频元数据时的缺省宽高比(竖版半身)。 */
|
|
1207
|
+
declare const DEFAULT_DIGITAL_HUMAN_ASPECT: number;
|
|
1208
|
+
interface Canvas {
|
|
1209
|
+
width: number;
|
|
1210
|
+
height: number;
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* 计算盒子。
|
|
1214
|
+
*
|
|
1215
|
+
* - 盒高 = `sizeRatio × 画面短边`,盒宽 = 盒高 × 宽高比;超出画面时等比缩小。
|
|
1216
|
+
* - 锚点:贴安全区边缘,间距 `margin × 短边`;`*-center` / `center-*` 在另一轴居中。
|
|
1217
|
+
* - custom:`(x, y)` 是盒子中心,占画面比例,不加 margin / insets。
|
|
1218
|
+
* - 最后叠加 offsetX / offsetY,并钳制在画面内。
|
|
1219
|
+
*/
|
|
1220
|
+
declare function resolveDigitalHumanBox(config: ResolvedDigitalHumanConfig, canvas: Canvas, options?: {
|
|
1221
|
+
videoSize?: {
|
|
1222
|
+
width: number;
|
|
1223
|
+
height: number;
|
|
1224
|
+
};
|
|
1225
|
+
insets?: DigitalHumanInsets;
|
|
1226
|
+
}): DigitalHumanBox;
|
|
1227
|
+
/**
|
|
1228
|
+
* 字幕让位:居中的字幕在 [bandTop, 画面底] 这条带子里,数字人与之有交叠且偏在
|
|
1229
|
+
* 一侧时,收窄字幕最大宽度(仍居中),让两侧都不碰到盒子。
|
|
1230
|
+
*
|
|
1231
|
+
* 返回收窄后的百分比;不需要让位时原样返回 `baseMaxWidthPct`。
|
|
1232
|
+
* 收窄结果不会低于 `minMaxWidthPct`(再窄字幕就不可读,宁可小幅重叠)。
|
|
1233
|
+
*/
|
|
1234
|
+
declare function resolveSubtitleMaxWidthPct(box: DigitalHumanBox | null, canvas: Canvas, opts: {
|
|
1235
|
+
bandTop: number;
|
|
1236
|
+
baseMaxWidthPct: number;
|
|
1237
|
+
gap?: number;
|
|
1238
|
+
minMaxWidthPct?: number;
|
|
1239
|
+
}): number;
|
|
1240
|
+
/**
|
|
1241
|
+
* 内容让位(layout=reserve):返回让出数字人一侧后剩余的水平区间(px)。
|
|
1242
|
+
* 盒子居中(side=none)时不让位,返回整幅。
|
|
1243
|
+
*/
|
|
1244
|
+
declare function resolveReservedRegion(box: DigitalHumanBox | null, canvas: Canvas, gap?: number): {
|
|
1245
|
+
left: number;
|
|
1246
|
+
width: number;
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1249
|
+
interface DigitalHumanLayerProps {
|
|
1250
|
+
dh: ResolvedDigitalHuman;
|
|
1251
|
+
box: DigitalHumanBox;
|
|
1252
|
+
/** 图层 zIndex,默认 12(在内容之上、字幕之下)。 */
|
|
1253
|
+
zIndex?: number;
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* 上报解析阶段收集到的问题。模板在决定是否渲染之前调用,
|
|
1257
|
+
* 这样即便数字人最终不显示(素材解析失败)也会留下诊断。
|
|
1258
|
+
*/
|
|
1259
|
+
declare function reportDigitalHumanIssues(dh: ResolvedDigitalHuman, box: DigitalHumanBox | null, templateId: string, sceneId?: string): void;
|
|
1260
|
+
/**
|
|
1261
|
+
* 数字人视频图层。
|
|
1262
|
+
*
|
|
1263
|
+
* 时间:`startFrom = dh.startFrame`(连续段里是本场景在段视频中的偏移)。
|
|
1264
|
+
* 视频播完后按 `onEnd`:freeze 停最后一帧 / hide 消失 / loop 循环剩余部分。
|
|
1265
|
+
* 入场动效只在段首(role = only / first)播放,段中场景直接满不透明度出现,
|
|
1266
|
+
* 这样同一段视频跨场景是连续的。
|
|
1267
|
+
*/
|
|
1268
|
+
declare const DigitalHumanLayer: React.FC<DigitalHumanLayerProps>;
|
|
1269
|
+
/** 封面用的静态数字人:有 posterSrc 时按同一个盒子画图,否则不渲染。 */
|
|
1270
|
+
declare const DigitalHumanPoster: React.FC<{
|
|
1271
|
+
dh: ResolvedDigitalHuman;
|
|
1272
|
+
box: DigitalHumanBox;
|
|
1273
|
+
zIndex?: number;
|
|
1274
|
+
}>;
|
|
1275
|
+
/**
|
|
1276
|
+
* layout=reserve 时把按固定宽度排版的内容整体等比缩进剩余区域。
|
|
1277
|
+
*
|
|
1278
|
+
* 放在一个已经收窄到 `regionWidth` 的容器里使用:内部仍按 `width × height`
|
|
1279
|
+
* 排版(版式组件的 px 常量都按这个尺寸写),再以中心为原点缩放到 regionWidth。
|
|
1280
|
+
* 不需要缩放(regionWidth 缺省或不小于 width)时原样透传、不引入额外 DOM,
|
|
1281
|
+
* 保证不开数字人时画面逐像素不变。
|
|
1282
|
+
*/
|
|
1283
|
+
declare const ReservedContentScaler: React.FC<{
|
|
1284
|
+
/** 可用宽度(px);undefined = 不缩放。 */
|
|
1285
|
+
regionWidth?: number;
|
|
1286
|
+
/** 内容的原始排版宽度。 */
|
|
1287
|
+
width: number;
|
|
1288
|
+
/** 内容区高度。 */
|
|
1289
|
+
height: number;
|
|
1290
|
+
children: React.ReactNode;
|
|
1291
|
+
}>;
|
|
1292
|
+
|
|
1293
|
+
export { AudioLayer, Background, type BackgroundMotion, type BgmConfig, type ChunkSubtitleOptions, type CompositionManifestEntry, DEFAULT_DIGITAL_HUMAN_ASPECT, DEFAULT_MIN_CHUNK_UNITS, DEFAULT_SPLIT_MAX_UNITS, DIGITAL_HUMAN_ANCHORS, DIGITAL_HUMAN_PROP_KEYS, DIGITAL_HUMAN_SIZE_PRESETS, type DigitalHumanAnchor, type DigitalHumanBox, type DigitalHumanConfig, type DigitalHumanDefaults, type DigitalHumanEnter, type DigitalHumanInput, type DigitalHumanInsets, DigitalHumanLayer, type DigitalHumanLayerProps, type DigitalHumanLayout, type DigitalHumanOnEnd, type DigitalHumanPosition, DigitalHumanPoster, type DigitalHumanSegmentRole, type DigitalHumanShape, type DigitalHumanSizePreset, GridParticleBackground, type GridParticleBackgroundProps, type GridParticleConfig, type GridParticleTheme, type GridParticleTuning, LINE_FILL_LIMIT, type Layer, type LocalizeSubtitlesOptions, type MainVideoProps, type RenderConfig, ReservedContentScaler, type ResolveDigitalHumanOptions, type ResolvedAsset, type ResolvedDigitalHuman, type ResolvedDigitalHumanConfig, type ResolvedSubtitleMode, type SanitizedDigitalHumanConfig, type SceneBackground, SceneBackgroundLayer, type SceneBackgroundLayerProps, type SceneBackgroundNoiseBlend, type SceneBackgroundSlotContext, type SceneDegradedReport, SceneTransition, type SplitSubtitleOptions, SubtitleBar, type SubtitleChunk, type SubtitleMode, type SubtitleSegment, type TemplateCompositionProps, TextLayer, type TimelineEntry, TimelineSequencer, type TimelineSequencerProps, type Typography, chunkSubtitleSegments, displayWidth, estimateLineCapacity, fontStack, grainDataUri, isLegacyAvatarInput, localizeSubtitles, mergeDigitalHumanConfig, pickDigitalHumanInput, reportDigitalHumanIssues, reportSceneDegraded, resetSceneDegradedReports, resolveActiveChunk, resolveAsset, resolveAssetUrl, resolveChunkFadeOpacity, resolveDigitalHuman, resolveDigitalHumanBox, resolveReservedRegion, resolveRunFadeOpacity, resolveSceneBackground, resolveSubtitleBottomPx, resolveSubtitleMaxWidthPct, resolveSubtitleMode, sanitizeDiagnosticSrc, sanitizeDigitalHumanConfig, sentenceStartFrames, splitSubtitleText, spokenWeight, useActiveSubtitle, useActiveSubtitleIndex, useAssetUrl, useCanvasScale, useLocalSubtitles };
|