@editframe/elements 0.11.0-beta.9 → 0.12.0-beta.2
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/dist/EF_FRAMEGEN.d.ts +8 -15
- package/dist/assets/src/MP4File.js +5 -3
- package/dist/elements/EFCaptions.d.ts +50 -6
- package/dist/elements/EFMedia.d.ts +1 -1
- package/dist/elements/EFTimegroup.browsertest.d.ts +4 -0
- package/dist/elements/EFTimegroup.d.ts +23 -2
- package/dist/elements/EFWaveform.d.ts +15 -11
- package/dist/elements/src/EF_FRAMEGEN.js +24 -26
- package/dist/elements/src/elements/EFCaptions.js +295 -42
- package/dist/elements/src/elements/EFImage.js +0 -6
- package/dist/elements/src/elements/EFMedia.js +0 -5
- package/dist/elements/src/elements/EFTemporal.js +13 -10
- package/dist/elements/src/elements/EFTimegroup.js +37 -12
- package/dist/elements/src/elements/EFVideo.js +1 -4
- package/dist/elements/src/elements/EFWaveform.js +250 -143
- package/dist/elements/src/gui/ContextMixin.js +36 -7
- package/dist/elements/src/gui/EFScrubber.js +142 -0
- package/dist/elements/src/gui/EFTimeDisplay.js +81 -0
- package/dist/elements/src/gui/EFTogglePlay.js +14 -14
- package/dist/elements/src/gui/EFWorkbench.js +1 -24
- package/dist/elements/src/gui/TWMixin.css.js +1 -1
- package/dist/elements/src/index.js +8 -1
- package/dist/gui/ContextMixin.d.ts +2 -1
- package/dist/gui/EFScrubber.d.ts +23 -0
- package/dist/gui/EFTimeDisplay.d.ts +17 -0
- package/dist/gui/EFTogglePlay.d.ts +1 -1
- package/dist/gui/EFWorkbench.d.ts +0 -1
- package/dist/index.d.ts +3 -1
- package/dist/style.css +6 -801
- package/package.json +2 -2
- package/src/elements/EFCaptions.browsertest.ts +6 -6
- package/src/elements/EFCaptions.ts +325 -56
- package/src/elements/EFImage.browsertest.ts +4 -17
- package/src/elements/EFImage.ts +0 -6
- package/src/elements/EFMedia.browsertest.ts +8 -19
- package/src/elements/EFMedia.ts +1 -6
- package/src/elements/EFTemporal.browsertest.ts +14 -0
- package/src/elements/EFTemporal.ts +14 -0
- package/src/elements/EFTimegroup.browsertest.ts +37 -0
- package/src/elements/EFTimegroup.ts +42 -17
- package/src/elements/EFVideo.ts +1 -4
- package/src/elements/EFWaveform.ts +339 -314
- package/src/gui/ContextMixin.browsertest.ts +28 -2
- package/src/gui/ContextMixin.ts +41 -9
- package/src/gui/EFScrubber.ts +145 -0
- package/src/gui/EFTimeDisplay.ts +81 -0
- package/src/gui/EFTogglePlay.ts +21 -21
- package/src/gui/EFWorkbench.ts +3 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/elements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0-beta.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"author": "",
|
|
21
21
|
"license": "UNLICENSED",
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@editframe/assets": "0.
|
|
23
|
+
"@editframe/assets": "0.12.0-beta.2",
|
|
24
24
|
"@lit/context": "^1.1.2",
|
|
25
25
|
"@lit/task": "^1.0.1",
|
|
26
26
|
"d3": "^7.9.0",
|
|
@@ -21,14 +21,14 @@ describe("EFCaptions", () => {
|
|
|
21
21
|
|
|
22
22
|
const target = document.createElement("ef-video");
|
|
23
23
|
target.setAttribute("id", id);
|
|
24
|
-
target.assetId = "550e8400-e29b-41d4-a716-446655440000
|
|
24
|
+
target.assetId = "550e8400-e29b-41d4-a716-446655440000";
|
|
25
25
|
document.body.appendChild(target);
|
|
26
26
|
const captions = document.createElement("ef-captions");
|
|
27
27
|
captions.setAttribute("target", id);
|
|
28
28
|
document.body.appendChild(captions);
|
|
29
29
|
workbench.appendChild(captions);
|
|
30
30
|
expect(captions.captionsPath()).toBe(
|
|
31
|
-
"editframe://api/v1/caption_files/550e8400-e29b-41d4-a716-446655440000
|
|
31
|
+
"editframe://api/v1/caption_files/550e8400-e29b-41d4-a716-446655440000",
|
|
32
32
|
);
|
|
33
33
|
});
|
|
34
34
|
});
|
|
@@ -38,13 +38,13 @@ describe("EFCaptions", () => {
|
|
|
38
38
|
const id = v4();
|
|
39
39
|
const target = document.createElement("ef-video");
|
|
40
40
|
target.setAttribute("id", id);
|
|
41
|
-
target.assetId =
|
|
41
|
+
target.assetId = id;
|
|
42
42
|
document.body.appendChild(target);
|
|
43
43
|
const captions = document.createElement("ef-captions");
|
|
44
44
|
captions.setAttribute("target", id);
|
|
45
45
|
document.body.appendChild(captions);
|
|
46
46
|
expect(captions.captionsPath()).toBe(
|
|
47
|
-
`https://editframe.dev/api/v1/caption_files/${id}
|
|
47
|
+
`https://editframe.dev/api/v1/caption_files/${id}`,
|
|
48
48
|
);
|
|
49
49
|
});
|
|
50
50
|
|
|
@@ -54,7 +54,7 @@ describe("EFCaptions", () => {
|
|
|
54
54
|
const id = v4();
|
|
55
55
|
const target = document.createElement("ef-video");
|
|
56
56
|
target.setAttribute("id", id);
|
|
57
|
-
target.assetId =
|
|
57
|
+
target.assetId = id;
|
|
58
58
|
document.body.appendChild(target);
|
|
59
59
|
const captions = document.createElement("ef-captions");
|
|
60
60
|
captions.setAttribute("target", id);
|
|
@@ -62,7 +62,7 @@ describe("EFCaptions", () => {
|
|
|
62
62
|
document.body.appendChild(preview);
|
|
63
63
|
preview.apiHost = "test://";
|
|
64
64
|
expect(captions.captionsPath()).toBe(
|
|
65
|
-
`test:///api/v1/caption_files/${id}
|
|
65
|
+
`test:///api/v1/caption_files/${id}`,
|
|
66
66
|
);
|
|
67
67
|
});
|
|
68
68
|
});
|
|
@@ -1,48 +1,56 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { GetISOBMFFFileTranscriptionResult } from "@editframe/api";
|
|
2
2
|
import { Task } from "@lit/task";
|
|
3
|
+
import { LitElement, type PropertyValueMap, css, html } from "lit";
|
|
3
4
|
import { customElement, property } from "lit/decorators.js";
|
|
4
|
-
import {
|
|
5
|
+
import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
|
|
6
|
+
import { EF_RENDERING } from "../EF_RENDERING.ts";
|
|
7
|
+
import { CrossUpdateController } from "./CrossUpdateController.ts";
|
|
5
8
|
import { EFAudio } from "./EFAudio.ts";
|
|
9
|
+
import { EFSourceMixin } from "./EFSourceMixin.ts";
|
|
6
10
|
import { EFTemporal } from "./EFTemporal.ts";
|
|
7
|
-
import {
|
|
11
|
+
import { EFVideo } from "./EFVideo.ts";
|
|
8
12
|
import { FetchMixin } from "./FetchMixin.ts";
|
|
9
|
-
import { EFSourceMixin } from "./EFSourceMixin.ts";
|
|
10
|
-
import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
|
|
11
|
-
import { EF_RENDERING } from "../EF_RENDERING.ts";
|
|
12
13
|
|
|
13
|
-
interface
|
|
14
|
+
interface WordSegment {
|
|
14
15
|
text: string;
|
|
15
16
|
start: number;
|
|
16
17
|
end: number;
|
|
17
|
-
confidence: number;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
interface Segment {
|
|
21
21
|
start: number;
|
|
22
22
|
end: number;
|
|
23
23
|
text: string;
|
|
24
|
-
confidence: number;
|
|
25
|
-
words: Word[];
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
interface Caption {
|
|
29
|
-
text: string;
|
|
30
27
|
segments: Segment[];
|
|
31
|
-
|
|
28
|
+
word_segments: WordSegment[];
|
|
32
29
|
}
|
|
33
30
|
|
|
31
|
+
const stopWords = new Set(["", ".", "!", "?", ","]);
|
|
32
|
+
|
|
34
33
|
@customElement("ef-captions-active-word")
|
|
35
34
|
export class EFCaptionsActiveWord extends EFTemporal(LitElement) {
|
|
36
35
|
static styles = [
|
|
37
36
|
css`
|
|
38
37
|
:host {
|
|
39
38
|
display: inline-block;
|
|
39
|
+
white-space: pre;
|
|
40
|
+
}
|
|
41
|
+
:host([hidden]) {
|
|
42
|
+
display: none;
|
|
40
43
|
}
|
|
41
44
|
`,
|
|
42
45
|
];
|
|
43
46
|
|
|
44
47
|
render() {
|
|
45
|
-
|
|
48
|
+
if (stopWords.has(this.wordText)) {
|
|
49
|
+
this.hidden = true;
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
this.hidden = false;
|
|
53
|
+
return html` ${this.wordText.trim()} `;
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
@property({ type: Number, attribute: false })
|
|
@@ -54,6 +62,9 @@ export class EFCaptionsActiveWord extends EFTemporal(LitElement) {
|
|
|
54
62
|
@property({ type: String, attribute: false })
|
|
55
63
|
wordText = "";
|
|
56
64
|
|
|
65
|
+
@property({ type: Boolean, reflect: true })
|
|
66
|
+
hidden = false;
|
|
67
|
+
|
|
57
68
|
get startTimeMs() {
|
|
58
69
|
return this.wordStartMs || 0;
|
|
59
70
|
}
|
|
@@ -63,6 +74,137 @@ export class EFCaptionsActiveWord extends EFTemporal(LitElement) {
|
|
|
63
74
|
}
|
|
64
75
|
}
|
|
65
76
|
|
|
77
|
+
@customElement("ef-captions-segment")
|
|
78
|
+
export class EFCaptionsSegment extends EFTemporal(LitElement) {
|
|
79
|
+
static styles = [
|
|
80
|
+
css`
|
|
81
|
+
:host {
|
|
82
|
+
display: block;
|
|
83
|
+
}
|
|
84
|
+
:host([hidden]) {
|
|
85
|
+
display: none;
|
|
86
|
+
}
|
|
87
|
+
`,
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
render() {
|
|
91
|
+
if (stopWords.has(this.segmentText)) {
|
|
92
|
+
this.hidden = true;
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
this.hidden = false;
|
|
96
|
+
return html`${this.segmentText}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@property({ type: Number, attribute: false })
|
|
100
|
+
segmentStartMs = 0;
|
|
101
|
+
|
|
102
|
+
@property({ type: Number, attribute: false })
|
|
103
|
+
segmentEndMs = 0;
|
|
104
|
+
|
|
105
|
+
@property({ type: String, attribute: false })
|
|
106
|
+
segmentText = "";
|
|
107
|
+
|
|
108
|
+
@property({ type: Boolean, reflect: true })
|
|
109
|
+
hidden = false;
|
|
110
|
+
|
|
111
|
+
get startTimeMs() {
|
|
112
|
+
return this.segmentStartMs || 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get durationMs(): number {
|
|
116
|
+
return this.segmentEndMs - this.segmentStartMs;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@customElement("ef-captions-before-active-word")
|
|
121
|
+
export class EFCaptionsBeforeActiveWord extends EFCaptionsSegment {
|
|
122
|
+
static styles = [
|
|
123
|
+
css`
|
|
124
|
+
:host {
|
|
125
|
+
display: inline-block;
|
|
126
|
+
white-space: pre;
|
|
127
|
+
}
|
|
128
|
+
:host([hidden]) {
|
|
129
|
+
display: none;
|
|
130
|
+
}
|
|
131
|
+
`,
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
render() {
|
|
135
|
+
if (stopWords.has(this.segmentText)) {
|
|
136
|
+
this.hidden = true;
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
this.hidden = false;
|
|
140
|
+
return html` ${this.segmentText}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@property({ type: Boolean, reflect: true })
|
|
144
|
+
hidden = false;
|
|
145
|
+
|
|
146
|
+
@property({ type: String, attribute: false })
|
|
147
|
+
segmentText = "";
|
|
148
|
+
|
|
149
|
+
@property({ type: Number, attribute: false })
|
|
150
|
+
segmentStartMs = 0;
|
|
151
|
+
|
|
152
|
+
@property({ type: Number, attribute: false })
|
|
153
|
+
segmentEndMs = 0;
|
|
154
|
+
|
|
155
|
+
get startTimeMs() {
|
|
156
|
+
return this.segmentStartMs || 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get durationMs(): number {
|
|
160
|
+
return this.segmentEndMs - this.segmentStartMs;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@customElement("ef-captions-after-active-word")
|
|
165
|
+
export class EFCaptionsAfterActiveWord extends EFCaptionsSegment {
|
|
166
|
+
static styles = [
|
|
167
|
+
css`
|
|
168
|
+
:host {
|
|
169
|
+
display: inline-block;
|
|
170
|
+
white-space: pre;
|
|
171
|
+
}
|
|
172
|
+
:host([hidden]) {
|
|
173
|
+
display: none;
|
|
174
|
+
}
|
|
175
|
+
`,
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
render() {
|
|
179
|
+
if (stopWords.has(this.segmentText)) {
|
|
180
|
+
this.hidden = true;
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
this.hidden = false;
|
|
184
|
+
return html`${this.segmentText} `;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@property({ type: Boolean, reflect: true })
|
|
188
|
+
hidden = false;
|
|
189
|
+
|
|
190
|
+
@property({ type: String, attribute: false })
|
|
191
|
+
segmentText = "";
|
|
192
|
+
|
|
193
|
+
@property({ type: Number, attribute: false })
|
|
194
|
+
segmentStartMs = 0;
|
|
195
|
+
|
|
196
|
+
@property({ type: Number, attribute: false })
|
|
197
|
+
segmentEndMs = 0;
|
|
198
|
+
|
|
199
|
+
get startTimeMs() {
|
|
200
|
+
return this.segmentStartMs || 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
get durationMs(): number {
|
|
204
|
+
return this.segmentEndMs - this.segmentStartMs;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
66
208
|
@customElement("ef-captions")
|
|
67
209
|
export class EFCaptions extends EFSourceMixin(
|
|
68
210
|
EFTemporal(FetchMixin(LitElement)),
|
|
@@ -71,11 +213,24 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
71
213
|
static styles = [
|
|
72
214
|
css`
|
|
73
215
|
:host {
|
|
74
|
-
display:
|
|
216
|
+
display: flex;
|
|
217
|
+
flex-wrap: wrap;
|
|
218
|
+
align-items: baseline;
|
|
219
|
+
width: fit-content;
|
|
220
|
+
}
|
|
221
|
+
::slotted(*) {
|
|
222
|
+
margin: 0;
|
|
223
|
+
padding: 0;
|
|
75
224
|
}
|
|
76
225
|
`,
|
|
77
226
|
];
|
|
78
227
|
|
|
228
|
+
@property({ type: String, attribute: "display-mode", reflect: true })
|
|
229
|
+
displayMode: "word" | "segment" | "context" = "segment";
|
|
230
|
+
|
|
231
|
+
@property({ type: Number, attribute: "context-words", reflect: true })
|
|
232
|
+
contextWords = 3;
|
|
233
|
+
|
|
79
234
|
@property({ type: String, attribute: "target", reflect: true })
|
|
80
235
|
targetSelector = "";
|
|
81
236
|
|
|
@@ -87,6 +242,27 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
87
242
|
wordStyle = "";
|
|
88
243
|
|
|
89
244
|
activeWordContainers = this.getElementsByTagName("ef-captions-active-word");
|
|
245
|
+
segmentContainers = this.getElementsByTagName("ef-captions-segment");
|
|
246
|
+
beforeActiveWordContainers = this.getElementsByTagName(
|
|
247
|
+
"ef-captions-before-active-word",
|
|
248
|
+
);
|
|
249
|
+
afterActiveWordContainers = this.getElementsByTagName(
|
|
250
|
+
"ef-captions-after-active-word",
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
render() {
|
|
254
|
+
return html`<slot></slot>`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
transcriptionsPath() {
|
|
258
|
+
if (this.targetElement.assetId) {
|
|
259
|
+
if (EF_RENDERING()) {
|
|
260
|
+
return `editframe://api/v1/isobmff_files/${this.targetElement.assetId}/transcription`;
|
|
261
|
+
}
|
|
262
|
+
return `${this.apiHost}/api/v1/isobmff_files/${this.targetElement.assetId}/transcription`;
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
90
266
|
|
|
91
267
|
captionsPath() {
|
|
92
268
|
if (this.targetElement.assetId) {
|
|
@@ -109,20 +285,71 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
109
285
|
},
|
|
110
286
|
});
|
|
111
287
|
|
|
112
|
-
private
|
|
288
|
+
private transcriptionDataTask = new Task(this, {
|
|
113
289
|
autoRun: EF_INTERACTIVE,
|
|
114
|
-
args: () => [this.
|
|
115
|
-
task: async ([
|
|
116
|
-
|
|
290
|
+
args: () => [this.transcriptionsPath(), this.fetch] as const,
|
|
291
|
+
task: async ([transcriptionsPath, fetch], { signal }) => {
|
|
292
|
+
if (!transcriptionsPath) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const response = await fetch(transcriptionsPath, { signal });
|
|
296
|
+
return response.json() as any as GetISOBMFFFileTranscriptionResult;
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
private transcriptionFragmentPath(
|
|
301
|
+
transcriptionId: string,
|
|
302
|
+
fragmentIndex: number,
|
|
303
|
+
) {
|
|
304
|
+
return `${this.apiHost}/api/v1/transcriptions/${transcriptionId}/fragments/${fragmentIndex}`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private fragmentIndexTask = new Task(this, {
|
|
308
|
+
autoRun: EF_INTERACTIVE,
|
|
309
|
+
args: () =>
|
|
310
|
+
[this.transcriptionDataTask.value, this.ownCurrentTimeMs] as const,
|
|
311
|
+
task: async ([transcription, ownCurrentTimeMs]) => {
|
|
312
|
+
if (!transcription) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
const fragmentIndex = Math.floor(
|
|
316
|
+
ownCurrentTimeMs / transcription.work_slice_ms,
|
|
317
|
+
);
|
|
318
|
+
return fragmentIndex;
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
private transcriptionFragmentDataTask = new Task(this, {
|
|
323
|
+
autoRun: EF_INTERACTIVE,
|
|
324
|
+
args: () =>
|
|
325
|
+
[
|
|
326
|
+
this.transcriptionDataTask.value,
|
|
327
|
+
this.fragmentIndexTask.value,
|
|
328
|
+
this.fetch,
|
|
329
|
+
] as const,
|
|
330
|
+
task: async ([transcription, fragmentIndex, fetch], { signal }) => {
|
|
331
|
+
if (
|
|
332
|
+
transcription === null ||
|
|
333
|
+
transcription === undefined ||
|
|
334
|
+
fragmentIndex === null ||
|
|
335
|
+
fragmentIndex === undefined
|
|
336
|
+
) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
const fragmentPath = this.transcriptionFragmentPath(
|
|
340
|
+
transcription.id,
|
|
341
|
+
fragmentIndex,
|
|
342
|
+
);
|
|
343
|
+
const response = await fetch(fragmentPath, { signal });
|
|
117
344
|
return response.json() as any as Caption;
|
|
118
345
|
},
|
|
119
346
|
});
|
|
120
347
|
|
|
121
348
|
frameTask = new Task(this, {
|
|
122
349
|
autoRun: EF_INTERACTIVE,
|
|
123
|
-
args: () => [this.
|
|
350
|
+
args: () => [this.transcriptionFragmentDataTask.status],
|
|
124
351
|
task: async () => {
|
|
125
|
-
await this.
|
|
352
|
+
await this.transcriptionFragmentDataTask.taskComplete;
|
|
126
353
|
},
|
|
127
354
|
});
|
|
128
355
|
|
|
@@ -133,51 +360,90 @@ export class EFCaptions extends EFSourceMixin(
|
|
|
133
360
|
}
|
|
134
361
|
}
|
|
135
362
|
|
|
136
|
-
render() {
|
|
137
|
-
return this.captionsDataTask.render({
|
|
138
|
-
pending: () => html`<div>Generating captions data...</div>`,
|
|
139
|
-
error: () => html`<div>🚫 Error generating captions data</div>`,
|
|
140
|
-
complete: () => html`<slot></slot>`,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
363
|
protected updated(
|
|
145
364
|
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
|
|
146
365
|
): void {
|
|
147
|
-
this.
|
|
366
|
+
this.updateTextContainers();
|
|
148
367
|
}
|
|
149
368
|
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
369
|
+
updateTextContainers() {
|
|
370
|
+
const transcriptionFragment = this.transcriptionFragmentDataTask
|
|
371
|
+
.value as Caption;
|
|
372
|
+
if (!transcriptionFragment) {
|
|
153
373
|
return;
|
|
154
374
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
375
|
+
|
|
376
|
+
const currentTimeMs = this.targetElement.trimAdjustedOwnCurrentTimeMs;
|
|
377
|
+
const currentTimeSec = currentTimeMs / 1000;
|
|
378
|
+
|
|
379
|
+
// Find the current word from word_segments
|
|
380
|
+
const currentWord = transcriptionFragment.word_segments.find(
|
|
381
|
+
(word) => currentTimeSec >= word.start && currentTimeSec <= word.end,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// Find the current segment
|
|
385
|
+
const currentSegment = transcriptionFragment.segments.find(
|
|
386
|
+
(segment) =>
|
|
387
|
+
currentTimeSec >= segment.start && currentTimeSec <= segment.end,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
for (const wordContainer of this.activeWordContainers) {
|
|
391
|
+
if (currentWord) {
|
|
392
|
+
wordContainer.wordText = currentWord.text;
|
|
393
|
+
wordContainer.wordStartMs = currentWord.start * 1000;
|
|
394
|
+
wordContainer.wordEndMs = currentWord.end * 1000;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
for (const segmentContainer of this.segmentContainers) {
|
|
399
|
+
if (currentSegment) {
|
|
400
|
+
segmentContainer.segmentText = currentSegment.text;
|
|
401
|
+
segmentContainer.segmentStartMs = currentSegment.start * 1000;
|
|
402
|
+
segmentContainer.segmentEndMs = currentSegment.end * 1000;
|
|
175
403
|
}
|
|
176
404
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
405
|
+
|
|
406
|
+
// Only process context if we have both a current word and segment
|
|
407
|
+
if (currentWord && currentSegment) {
|
|
408
|
+
// Find all word segments that fall within the current segment's time range
|
|
409
|
+
const segmentWords = transcriptionFragment.word_segments.filter(
|
|
410
|
+
(word) =>
|
|
411
|
+
word.start >= currentSegment.start && word.end <= currentSegment.end,
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// Find the index of the current word within the segment's word segments
|
|
415
|
+
const currentWordIndex = segmentWords.findIndex(
|
|
416
|
+
(word) =>
|
|
417
|
+
word.start === currentWord.start && word.end === currentWord.end,
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
if (currentWordIndex !== -1) {
|
|
421
|
+
// Get words before the current word
|
|
422
|
+
const beforeWords = segmentWords
|
|
423
|
+
.slice(0, currentWordIndex)
|
|
424
|
+
.map((w) => w.text.trim())
|
|
425
|
+
.join(" ");
|
|
426
|
+
|
|
427
|
+
// Get words after the current word
|
|
428
|
+
const afterWords = segmentWords
|
|
429
|
+
.slice(currentWordIndex + 1)
|
|
430
|
+
.map((w) => w.text.trim())
|
|
431
|
+
.join(" ");
|
|
432
|
+
|
|
433
|
+
// Update before containers
|
|
434
|
+
for (const container of this.beforeActiveWordContainers) {
|
|
435
|
+
container.segmentText = beforeWords;
|
|
436
|
+
container.segmentStartMs = currentSegment.start * 1000;
|
|
437
|
+
container.segmentEndMs = currentWord.start * 1000;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Update after containers
|
|
441
|
+
for (const container of this.afterActiveWordContainers) {
|
|
442
|
+
container.segmentText = afterWords;
|
|
443
|
+
container.segmentStartMs = currentWord.end * 1000;
|
|
444
|
+
container.segmentEndMs = currentSegment.end * 1000;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
181
447
|
}
|
|
182
448
|
}
|
|
183
449
|
|
|
@@ -194,5 +460,8 @@ declare global {
|
|
|
194
460
|
interface HTMLElementTagNameMap {
|
|
195
461
|
"ef-captions": EFCaptions;
|
|
196
462
|
"ef-captions-active-word": EFCaptionsActiveWord;
|
|
463
|
+
"ef-captions-segment": EFCaptionsSegment;
|
|
464
|
+
"ef-captions-before-active-word": EFCaptionsBeforeActiveWord;
|
|
465
|
+
"ef-captions-after-active-word": EFCaptionsAfterActiveWord;
|
|
197
466
|
}
|
|
198
467
|
}
|
|
@@ -26,23 +26,12 @@ describe("EFImage", () => {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
describe("attribute: asset-id", () => {
|
|
29
|
-
test("must match :id/basename", () => {
|
|
30
|
-
const image = document.createElement("ef-image");
|
|
31
|
-
expect(() => {
|
|
32
|
-
image.assetId = "1234:example.mp4";
|
|
33
|
-
}).toThrowError(
|
|
34
|
-
new Error(
|
|
35
|
-
"EFMedia: asset-id must match <uuid>:<basename>. (like: 550e8400-e29b-41d4-a716-446655440000:example.mp4)",
|
|
36
|
-
),
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
29
|
test("determines assetPath", () => {
|
|
41
30
|
const id = v4();
|
|
42
31
|
const image = document.createElement("ef-image");
|
|
43
|
-
image.setAttribute("asset-id",
|
|
32
|
+
image.setAttribute("asset-id", id);
|
|
44
33
|
expect(image.assetPath()).toBe(
|
|
45
|
-
`https://editframe.dev/api/v1/image_files/${id}
|
|
34
|
+
`https://editframe.dev/api/v1/image_files/${id}`,
|
|
46
35
|
);
|
|
47
36
|
});
|
|
48
37
|
|
|
@@ -50,13 +39,11 @@ describe("EFImage", () => {
|
|
|
50
39
|
const id = v4();
|
|
51
40
|
const image = document.createElement("ef-image");
|
|
52
41
|
const preview = document.createElement("ef-preview");
|
|
53
|
-
image.setAttribute("asset-id",
|
|
42
|
+
image.setAttribute("asset-id", id);
|
|
54
43
|
preview.appendChild(image);
|
|
55
44
|
preview.apiHost = "test://";
|
|
56
45
|
document.body.appendChild(preview);
|
|
57
|
-
expect(image.assetPath()).toBe(
|
|
58
|
-
`test:///api/v1/image_files/${id}:example.jpg`,
|
|
59
|
-
);
|
|
46
|
+
expect(image.assetPath()).toBe(`test:///api/v1/image_files/${id}`);
|
|
60
47
|
});
|
|
61
48
|
});
|
|
62
49
|
});
|
package/src/elements/EFImage.ts
CHANGED
|
@@ -28,12 +28,6 @@ export class EFImage extends EFSourceMixin(FetchMixin(LitElement), {
|
|
|
28
28
|
#assetId: string | null = null;
|
|
29
29
|
@property({ type: String, attribute: "asset-id", reflect: true })
|
|
30
30
|
set assetId(value: string | null) {
|
|
31
|
-
if (!value?.match(/^.{8}-.{4}-.{4}-.{4}-.{12}:.*$/)) {
|
|
32
|
-
console.error(`EFMedia ${value} is not valid asset-id`);
|
|
33
|
-
throw new Error(
|
|
34
|
-
"EFMedia: asset-id must match <uuid>:<basename>. (like: 550e8400-e29b-41d4-a716-446655440000:example.mp4)",
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
31
|
this.#assetId = value;
|
|
38
32
|
}
|
|
39
33
|
|
|
@@ -57,58 +57,47 @@ describe("EFMedia", () => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
describe("attribute: asset-id", () => {
|
|
60
|
-
test("must match :id/basename", () => {
|
|
61
|
-
const element = document.createElement("test-media");
|
|
62
|
-
expect(() => {
|
|
63
|
-
element.assetId = "1234:example.mp4";
|
|
64
|
-
}).toThrowError(
|
|
65
|
-
new Error(
|
|
66
|
-
"EFMedia: asset-id must match <uuid>:<basename>. (like: 550e8400-e29b-41d4-a716-446655440000:example.mp4)",
|
|
67
|
-
),
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
60
|
test("determines fragmentIndexPath", () => {
|
|
72
61
|
const id = v4();
|
|
73
62
|
const element = document.createElement("test-media");
|
|
74
|
-
element.setAttribute("asset-id",
|
|
63
|
+
element.setAttribute("asset-id", id);
|
|
75
64
|
expect(element.fragmentIndexPath()).toBe(
|
|
76
|
-
`https://editframe.dev/api/v1/isobmff_files/${id}
|
|
65
|
+
`https://editframe.dev/api/v1/isobmff_files/${id}/index`,
|
|
77
66
|
);
|
|
78
67
|
});
|
|
79
68
|
|
|
80
69
|
test("determines fragmentTrackPath", () => {
|
|
81
70
|
const id = v4();
|
|
82
71
|
const element = document.createElement("test-media");
|
|
83
|
-
element.setAttribute("asset-id",
|
|
72
|
+
element.setAttribute("asset-id", id);
|
|
84
73
|
expect(element.fragmentTrackPath("1")).toBe(
|
|
85
|
-
`https://editframe.dev/api/v1/isobmff_tracks/${id}
|
|
74
|
+
`https://editframe.dev/api/v1/isobmff_tracks/${id}/1`,
|
|
86
75
|
);
|
|
87
76
|
});
|
|
88
77
|
|
|
89
78
|
test("honors apiHost in fragmentIndexPath", () => {
|
|
90
79
|
const id = v4();
|
|
91
80
|
const element = document.createElement("test-media");
|
|
92
|
-
element.setAttribute("asset-id",
|
|
81
|
+
element.setAttribute("asset-id", id);
|
|
93
82
|
const preview = document.createElement("ef-preview");
|
|
94
83
|
preview.appendChild(element);
|
|
95
84
|
preview.apiHost = "test://";
|
|
96
85
|
document.body.appendChild(preview);
|
|
97
86
|
expect(element.fragmentIndexPath()).toBe(
|
|
98
|
-
`test:///api/v1/isobmff_files/${id}
|
|
87
|
+
`test:///api/v1/isobmff_files/${id}/index`,
|
|
99
88
|
);
|
|
100
89
|
});
|
|
101
90
|
|
|
102
91
|
test("honors apiHost in fragmentTrackPath", () => {
|
|
103
92
|
const id = v4();
|
|
104
93
|
const element = document.createElement("test-media");
|
|
105
|
-
element.setAttribute("asset-id",
|
|
94
|
+
element.setAttribute("asset-id", id);
|
|
106
95
|
const preview = document.createElement("ef-preview");
|
|
107
96
|
preview.appendChild(element);
|
|
108
97
|
preview.apiHost = "test://";
|
|
109
98
|
document.body.appendChild(preview);
|
|
110
99
|
expect(element.fragmentTrackPath("1")).toBe(
|
|
111
|
-
`test:///api/v1/isobmff_tracks/${id}
|
|
100
|
+
`test:///api/v1/isobmff_tracks/${id}/1`,
|
|
112
101
|
);
|
|
113
102
|
});
|
|
114
103
|
});
|