@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 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 BORDA = 0.12;
512
- var remapWords = (words, spans, borda = BORDA) => {
570
+ var remapWords = (words, spans) => {
513
571
  const out = [];
514
572
  for (const w of words) {
515
- let best;
516
- for (const span2 of spans) {
517
- const overlap = Math.min(w.t1, span2.out + borda) - Math.max(w.t0, span2.in - borda);
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
  };
@@ -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
- export declare const remapWords: (words: CaptionWord[], spans: SourceSpan[], borda?: number) => CaptionWord[];
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.2.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",