@omelhorsite/video-sdk 0.2.0 → 0.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/README.md +8 -1
- package/dist/index.js +108 -14
- package/dist/types/captions.d.ts +95 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,7 +64,14 @@ export by construction.
|
|
|
64
64
|
- Captions live in the project in source time (`project.captions[].words`,
|
|
65
65
|
`{ t0, t1, text }`), so cutting or reordering never invalidates them.
|
|
66
66
|
`timelineWords(project)` brings the words onto the timeline; `groupWords`
|
|
67
|
-
and `captionStates` do the karaoke grouping.
|
|
67
|
+
and `captionStates` do the karaoke grouping. A word belongs to the piece
|
|
68
|
+
that holds its **centre**: a word said before a cut has it on one side, one
|
|
69
|
+
said after has it on the other, and a few hundredths of drift in a speech
|
|
70
|
+
model's timings cannot change that.
|
|
71
|
+
- `sourceCaptions(project, words)` is the way back, for captioning an edit
|
|
72
|
+
rather than the raw material: give it words in timeline time and it returns
|
|
73
|
+
them in source time, grouped by asset. `audibleSpans(project)` lists the
|
|
74
|
+
pieces whose own sound is in the mix — a silent overlay carries no words.
|
|
68
75
|
- `sliceProject(project, from, to)` cuts a window out of a project for a fast
|
|
69
76
|
partial render.
|
|
70
77
|
|
package/dist/index.js
CHANGED
|
@@ -449,7 +449,40 @@ var DEFAULT_CAPTION_STYLE = {
|
|
|
449
449
|
highlight: "#ffd600",
|
|
450
450
|
stroke: "#000000",
|
|
451
451
|
maxWidth: 0.9,
|
|
452
|
-
fontFamily: "Montserrat, Helvetica, Arial, sans-serif"
|
|
452
|
+
fontFamily: "Montserrat, Helvetica, Arial, sans-serif",
|
|
453
|
+
fontWeight: 800,
|
|
454
|
+
lineHeight: 1.24,
|
|
455
|
+
letterSpacing: 0,
|
|
456
|
+
wordSpacing: 0,
|
|
457
|
+
shadow: "",
|
|
458
|
+
punctuation: true,
|
|
459
|
+
box: false,
|
|
460
|
+
boxPadX: 0.27,
|
|
461
|
+
boxPadY: 0.26,
|
|
462
|
+
boxRadius: 0.12,
|
|
463
|
+
boxPop: 0.95,
|
|
464
|
+
boxPopTime: 0.1
|
|
465
|
+
};
|
|
466
|
+
var CAPTION_PRESETS = {
|
|
467
|
+
karaoke: {},
|
|
468
|
+
box: {
|
|
469
|
+
fontScale: 0.046,
|
|
470
|
+
strokeScale: 0,
|
|
471
|
+
pos: 0.78,
|
|
472
|
+
maxWords: 4,
|
|
473
|
+
maxChars: 24,
|
|
474
|
+
maxWidth: 0.6,
|
|
475
|
+
color: "#ffffff",
|
|
476
|
+
highlight: "#036cf0",
|
|
477
|
+
fontFamily: "Poppins, Montserrat, Helvetica, Arial, sans-serif",
|
|
478
|
+
fontWeight: 700,
|
|
479
|
+
lineHeight: 0.92,
|
|
480
|
+
letterSpacing: -0.02,
|
|
481
|
+
wordSpacing: 0.15,
|
|
482
|
+
shadow: "rgba(0,0,0,0.55)",
|
|
483
|
+
punctuation: false,
|
|
484
|
+
box: true
|
|
485
|
+
}
|
|
453
486
|
};
|
|
454
487
|
var SENT_END = ".?!:;,";
|
|
455
488
|
var groupWords = (words, style) => {
|
|
@@ -492,6 +525,32 @@ var captionStates = (chunks, tail) => {
|
|
|
492
525
|
});
|
|
493
526
|
return states;
|
|
494
527
|
};
|
|
528
|
+
var easeOut = (p) => 1 - (1 - p) * (1 - p);
|
|
529
|
+
var boxScale = (state, t, style, fps) => {
|
|
530
|
+
if (style.boxPopTime <= 0)
|
|
531
|
+
return 1;
|
|
532
|
+
const entrada = easeOut(Math.min(1, Math.max(0, (t - state.t0) / style.boxPopTime)));
|
|
533
|
+
const saida = easeOut(Math.min(1, Math.max(0, (state.t1 - (t + 1 / fps)) / style.boxPopTime)));
|
|
534
|
+
return style.boxPop + (1 - style.boxPop) * Math.min(entrada, saida);
|
|
535
|
+
};
|
|
536
|
+
var captionFrames = (states, style, fps) => {
|
|
537
|
+
if (!style.box)
|
|
538
|
+
return states.map((state) => ({ state, t0: state.t0, t1: state.t1, scale: 1 }));
|
|
539
|
+
const out = [];
|
|
540
|
+
for (const state of states) {
|
|
541
|
+
const n0 = Math.round(state.t0 * fps);
|
|
542
|
+
const n1 = Math.round(state.t1 * fps);
|
|
543
|
+
for (let n2 = n0;n2 < n1; n2++) {
|
|
544
|
+
const scale = Math.round(boxScale(state, n2 / fps, style, fps) * 1000) / 1000;
|
|
545
|
+
const last = out[out.length - 1];
|
|
546
|
+
if (last && last.state === state && last.scale === scale)
|
|
547
|
+
last.t1 = (n2 + 1) / fps;
|
|
548
|
+
else
|
|
549
|
+
out.push({ state, t0: n2 / fps, t1: (n2 + 1) / fps, scale });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return out;
|
|
553
|
+
};
|
|
495
554
|
var sourceSpans = (project, assetId) => {
|
|
496
555
|
const spans = [];
|
|
497
556
|
for (const track of project.tracks) {
|
|
@@ -508,21 +567,13 @@ var sourceSpans = (project, assetId) => {
|
|
|
508
567
|
}
|
|
509
568
|
return spans.sort((a, b) => a.start - b.start);
|
|
510
569
|
};
|
|
511
|
-
var
|
|
512
|
-
var remapWords = (words, spans, borda = BORDA) => {
|
|
570
|
+
var remapWords = (words, spans) => {
|
|
513
571
|
const out = [];
|
|
514
572
|
for (const w of words) {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if (overlap <= 0)
|
|
519
|
-
continue;
|
|
520
|
-
if (!best || overlap > best.overlap)
|
|
521
|
-
best = { span: span2, overlap };
|
|
522
|
-
}
|
|
523
|
-
if (!best)
|
|
573
|
+
const meio = (w.t0 + w.t1) / 2;
|
|
574
|
+
const span = spans.find((s) => meio >= s.in && meio <= s.out);
|
|
575
|
+
if (!span)
|
|
524
576
|
continue;
|
|
525
|
-
const { span } = best;
|
|
526
577
|
const t0 = Math.min(Math.max(w.t0, span.in), span.out) - span.in + span.start;
|
|
527
578
|
const t1 = Math.min(Math.max(w.t1, span.in), span.out) - span.in + span.start;
|
|
528
579
|
if (t1 > t0)
|
|
@@ -536,6 +587,44 @@ var timelineWords = (project) => {
|
|
|
536
587
|
all.push(...remapWords(src.words, sourceSpans(project, src.assetId)));
|
|
537
588
|
return all.sort((a, b) => a.t0 - b.t0);
|
|
538
589
|
};
|
|
590
|
+
var audibleSpans = (project) => {
|
|
591
|
+
const spans = [];
|
|
592
|
+
for (const track of project.tracks) {
|
|
593
|
+
if (track.kind !== "video" || track.muted)
|
|
594
|
+
continue;
|
|
595
|
+
for (const clip of track.clips) {
|
|
596
|
+
if (clip.kind !== "video")
|
|
597
|
+
continue;
|
|
598
|
+
const v = clip;
|
|
599
|
+
if (v.muted)
|
|
600
|
+
continue;
|
|
601
|
+
spans.push({ assetId: v.assetId, start: v.start, end: v.start + v.duration, in: v.in });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return spans.sort((a, b) => a.start - b.start);
|
|
605
|
+
};
|
|
606
|
+
var sourceCaptions = (project, words, language) => {
|
|
607
|
+
const spans = audibleSpans(project);
|
|
608
|
+
const porAsset = new Map;
|
|
609
|
+
for (const w of words) {
|
|
610
|
+
const meio = (w.t0 + w.t1) / 2;
|
|
611
|
+
const s = spans.find((x) => meio >= x.start && meio <= x.end);
|
|
612
|
+
if (!s)
|
|
613
|
+
continue;
|
|
614
|
+
const t0 = Math.min(Math.max(w.t0, s.start), s.end) - s.start + s.in;
|
|
615
|
+
const t1 = Math.min(Math.max(w.t1, s.start), s.end) - s.start + s.in;
|
|
616
|
+
if (t1 <= t0)
|
|
617
|
+
continue;
|
|
618
|
+
const lista = porAsset.get(s.assetId) ?? [];
|
|
619
|
+
lista.push({ t0, t1, text: w.text });
|
|
620
|
+
porAsset.set(s.assetId, lista);
|
|
621
|
+
}
|
|
622
|
+
return [...porAsset.entries()].map(([assetId, ws]) => ({
|
|
623
|
+
assetId,
|
|
624
|
+
words: ws.sort((a, b) => a.t0 - b.t0),
|
|
625
|
+
...language ? { language } : {}
|
|
626
|
+
}));
|
|
627
|
+
};
|
|
539
628
|
// src/slice.ts
|
|
540
629
|
var temSource = (c) => c.kind === "video" || c.kind === "image" || c.kind === "audio";
|
|
541
630
|
var sliceClip = (clip, from, to) => {
|
|
@@ -1139,6 +1228,7 @@ export {
|
|
|
1139
1228
|
timelineWords,
|
|
1140
1229
|
stringTokenStore,
|
|
1141
1230
|
sourceSpans,
|
|
1231
|
+
sourceCaptions,
|
|
1142
1232
|
sliceProject,
|
|
1143
1233
|
sliceClip,
|
|
1144
1234
|
shadowMargin,
|
|
@@ -1170,6 +1260,9 @@ export {
|
|
|
1170
1260
|
compile,
|
|
1171
1261
|
cloudCaptionStyle,
|
|
1172
1262
|
captionStates,
|
|
1263
|
+
captionFrames,
|
|
1264
|
+
boxScale,
|
|
1265
|
+
audibleSpans,
|
|
1173
1266
|
assetEntryName,
|
|
1174
1267
|
SHADOW_OPACITY,
|
|
1175
1268
|
SHADOW_DROP,
|
|
@@ -1193,5 +1286,6 @@ export {
|
|
|
1193
1286
|
DEFAULT_CAPTION_STYLE,
|
|
1194
1287
|
DEFAULT_BASE_URL,
|
|
1195
1288
|
Cloud,
|
|
1196
|
-
CREDENTIALS_VERSION
|
|
1289
|
+
CREDENTIALS_VERSION,
|
|
1290
|
+
CAPTION_PRESETS
|
|
1197
1291
|
};
|
package/dist/types/captions.d.ts
CHANGED
|
@@ -45,8 +45,40 @@ export interface CaptionStyle {
|
|
|
45
45
|
/** Largura maxima do texto como fraccao da largura do projecto. */
|
|
46
46
|
maxWidth: number;
|
|
47
47
|
fontFamily: string;
|
|
48
|
+
fontWeight: number;
|
|
49
|
+
/** Distancia entre linhas, em fraccao da fonte. */
|
|
50
|
+
lineHeight: number;
|
|
51
|
+
/** Espaco extra entre letras e entre palavras, em fraccao da fonte. */
|
|
52
|
+
letterSpacing: number;
|
|
53
|
+
wordSpacing: number;
|
|
54
|
+
/** Sombra suave por baixo do texto (cor CSS); "" = sem sombra. */
|
|
55
|
+
shadow: string;
|
|
56
|
+
/** Mostra a pontuacao colada a`s palavras; o agrupamento corta nela na mesma. */
|
|
57
|
+
punctuation: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Realce em caixa: a palavra activa fica com a cor do texto, sobre um
|
|
60
|
+
* rectangulo arredondado da cor `highlight`, em vez de mudar de cor.
|
|
61
|
+
*/
|
|
62
|
+
box: boolean;
|
|
63
|
+
/** Folga da caixa a` volta das maiusculas e raio dos cantos, em fraccao da fonte. */
|
|
64
|
+
boxPadX: number;
|
|
65
|
+
boxPadY: number;
|
|
66
|
+
boxRadius: number;
|
|
67
|
+
/**
|
|
68
|
+
* A caixa entra a esta escala e cresce ate' 1 em `boxPopTime` segundos, e
|
|
69
|
+
* encolhe de volta no fim da palavra. So' a caixa: o texto nao mexe.
|
|
70
|
+
*/
|
|
71
|
+
boxPop: number;
|
|
72
|
+
boxPopTime: number;
|
|
48
73
|
}
|
|
49
74
|
export declare const DEFAULT_CAPTION_STYLE: CaptionStyle;
|
|
75
|
+
/**
|
|
76
|
+
* Estilos prontos. `box` e' o realce em caixa azul, medido frame a frame de
|
|
77
|
+
* um video de referencia a 1080 de largura: maiusculas com 36 px, linhas a
|
|
78
|
+
* 46 px, caixa 13 px para fora das maiusculas, que entra a 95% do tamanho,
|
|
79
|
+
* assenta em ~0.1 s e sai pelo mesmo caminho.
|
|
80
|
+
*/
|
|
81
|
+
export declare const CAPTION_PRESETS: Record<string, Partial<CaptionStyle>>;
|
|
50
82
|
/** Corta a lista de palavras em grupos curtos (o que aparece de cada vez). */
|
|
51
83
|
export declare const groupWords: (words: CaptionWord[], style: CaptionStyle) => CaptionWord[][];
|
|
52
84
|
/** Um grupo no ecra com uma palavra realcada. */
|
|
@@ -63,6 +95,26 @@ export interface CaptionState {
|
|
|
63
95
|
* entra pelo grupo seguinte adentro.
|
|
64
96
|
*/
|
|
65
97
|
export declare const captionStates: (chunks: CaptionWord[][], tail: number) => CaptionState[];
|
|
98
|
+
/** Um troco de um estado em que a caixa tem uma escala so'. */
|
|
99
|
+
export interface CaptionFrame {
|
|
100
|
+
state: CaptionState;
|
|
101
|
+
t0: number;
|
|
102
|
+
t1: number;
|
|
103
|
+
scale: number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Escala da caixa no frame `[t, t + 1/fps)` de um estado. A entrada mede-se
|
|
107
|
+
* no inicio do frame e a saida no fim, para o primeiro e o ultimo frame de
|
|
108
|
+
* cada palavra ficarem ambos em `boxPop`, como na referencia.
|
|
109
|
+
*/
|
|
110
|
+
export declare const boxScale: (state: CaptionState, t: number, style: CaptionStyle, fps: number) => number;
|
|
111
|
+
/**
|
|
112
|
+
* Os estados partidos em trocos de escala constante, com as fronteiras na
|
|
113
|
+
* grelha de frames do projecto. Sem caixa, um troco por estado, intacto.
|
|
114
|
+
* Escalas iguais ao milesimo juntam-se: nao se distinguem, e cada troco e'
|
|
115
|
+
* mais uma imagem a rasterizar.
|
|
116
|
+
*/
|
|
117
|
+
export declare const captionFrames: (states: CaptionState[], style: CaptionStyle, fps: number) => CaptionFrame[];
|
|
66
118
|
/**
|
|
67
119
|
* Um pedaco do source que sobreviveu a` montagem: `[in, out]` no material
|
|
68
120
|
* bruto, a comecar em `start` na timeline.
|
|
@@ -74,6 +126,48 @@ export interface SourceSpan {
|
|
|
74
126
|
}
|
|
75
127
|
/** Os pedacos de um asset que estao na timeline, por ordem de montagem. */
|
|
76
128
|
export declare const sourceSpans: (project: Project, assetId: string) => SourceSpan[];
|
|
77
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Traz as palavras do tempo do source para o tempo da timeline.
|
|
131
|
+
*
|
|
132
|
+
* Uma palavra pertence ao pedaco que contem o seu CENTRO, e os tempos sao
|
|
133
|
+
* depois apertados a esse pedaco. Palavras cujo centro nao cai em pedaco
|
|
134
|
+
* nenhum desaparecem, que e' o que se quer.
|
|
135
|
+
*
|
|
136
|
+
* O centro em vez da sobreposicao: quem decide por sobreposicao tem de
|
|
137
|
+
* escolher uma tolerancia, e nenhuma serve. Curta, perde a primeira palavra a
|
|
138
|
+
* seguir a um corte, porque os tempos de um modelo de fala andam uns
|
|
139
|
+
* centesimos ao lado. Larga, deixa entrar palavras de takes deitadas fora que
|
|
140
|
+
* encostam a` fronteira pelo lado de fora: ficam com uma lasca de 50 ms e
|
|
141
|
+
* legendam um som que ninguem diz. O centro nao tem esse dilema - uma palavra
|
|
142
|
+
* dita antes do corte tem-no de um lado, uma dita depois tem-no do outro, e
|
|
143
|
+
* uns centesimos de desvio nao mudam o lado.
|
|
144
|
+
*/
|
|
145
|
+
export declare const remapWords: (words: CaptionWord[], spans: SourceSpan[]) => CaptionWord[];
|
|
78
146
|
/** Todas as legendas do projecto, ja em tempo de timeline e por ordem. */
|
|
79
147
|
export declare const timelineWords: (project: Project) => CaptionWord[];
|
|
148
|
+
/** Um pedaco cujo audio proprio entra na mistura, e de onde vem. */
|
|
149
|
+
export interface AudibleSpan {
|
|
150
|
+
assetId: string;
|
|
151
|
+
/** Onde comeca e acaba na timeline. */
|
|
152
|
+
start: number;
|
|
153
|
+
end: number;
|
|
154
|
+
/** Ponto do source a que `start` corresponde. */
|
|
155
|
+
in: number;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Os pedacos que se OUVEM, por ordem de timeline. Um clip sem som proprio
|
|
159
|
+
* (uma ilustracao por cima) nao entra: nada do que ele mostra e' dito.
|
|
160
|
+
*/
|
|
161
|
+
export declare const audibleSpans: (project: Project) => AudibleSpan[];
|
|
162
|
+
/**
|
|
163
|
+
* O caminho inverso de `remapWords`: palavras em tempo de TIMELINE voltam ao
|
|
164
|
+
* tempo do source de onde vieram, agrupadas por asset.
|
|
165
|
+
*
|
|
166
|
+
* Serve para legendar a partir do montado. Transcrever o material bruto e
|
|
167
|
+
* depois esperar que as palavras caiam nos pedacos certos falha assim que uma
|
|
168
|
+
* frase e' dita duas vezes: o modelo funde as tentativas numa so' e os tempos
|
|
169
|
+
* que devolve nao dizem qual delas ficou. Transcrever o que se ouve nao tem
|
|
170
|
+
* esse problema, e isto devolve o resultado ao formato, que guarda as palavras
|
|
171
|
+
* em tempo do source para sobreviverem a` proxima edicao.
|
|
172
|
+
*/
|
|
173
|
+
export declare const sourceCaptions: (project: Project, words: CaptionWord[], language?: string) => CaptionSource[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omelhorsite/video-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The engine of the omelhorsite video editor: the .omsv project model, placement math, effects, karaoke captions, compilation to ffmpeg arguments, and the omelhorsite cloud client. Isolate-safe: no node builtins, no environment access, no stdout.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|