@gentbajko/slopify 2.3.1 → 2.5.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/dist/adapter-registry.js +9 -2
- package/dist/adapters/host-cli/index.js +11 -0
- package/dist/adapters/host-cli/transport.js +6 -1
- package/dist/adapters/image/codex-output.js +59 -17
- package/dist/adapters/image/codex.js +92 -16
- package/dist/adapters/image/fal.js +99 -8
- package/dist/adapters/image/google.js +13 -1
- package/dist/adapters/image/openai.js +37 -15
- package/dist/adapters/image/reference.js +7 -0
- package/dist/adapters/image/replicate.js +48 -2
- package/dist/adapters/image/video.js +28 -0
- package/dist/adapters/llm/codex-models.js +2 -1
- package/dist/adapters/llm/codex-server-models.js +2 -1
- package/dist/adapters/llm/run-cli.js +14 -2
- package/dist/assets/fonts/Barlow-Bold.ttf +0 -0
- package/dist/assets/fonts/SOURCE.txt +6 -0
- package/dist/assets/models.yaml +89 -1
- package/dist/catalog/registry.js +24 -3
- package/dist/catalog/runtime-models.js +1 -1
- package/dist/catalog/schema.js +20 -1
- package/dist/catalog/store.js +2 -2
- package/dist/catalog/validate.js +47 -4
- package/dist/edge/docker-launch.js +4 -0
- package/dist/edge/docker-projects/engine.js +57 -1
- package/dist/edge/docker-projects/install.js +10 -2
- package/dist/edge/docker-projects/prune.js +66 -0
- package/dist/edge/events/hub.js +11 -0
- package/dist/edge/http/app.js +2 -0
- package/dist/edge/http/backups.js +40 -0
- package/dist/edge/http/host-cli.js +17 -2
- package/dist/edge/http/problem.js +1 -0
- package/dist/edge/http/providers.js +8 -0
- package/dist/edge/http/schedules.js +1 -1
- package/dist/edge/http/settings.js +44 -0
- package/dist/edge/http/timeouts.js +38 -0
- package/dist/host-cli/runtime.js +3 -2
- package/dist/kernel/db/migrations/0019-shorts-prompts.sql +16 -0
- package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
- package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
- package/dist/kernel/ports/host-cli.js +17 -2
- package/dist/kernel/ports/image.js +3 -1
- package/dist/kernel/ports/llm.js +3 -1
- package/dist/kernel/runner/attempt.js +5 -2
- package/dist/kernel/runner/providers.js +42 -2
- package/dist/main.js +55 -7
- package/dist/slices/admission/model.js +8 -0
- package/dist/slices/admission/rules.js +91 -2
- package/dist/slices/admission/schema.js +42 -1
- package/dist/slices/admission/start.js +53 -15
- package/dist/slices/backups/files.js +135 -0
- package/dist/slices/backups/folder.js +35 -0
- package/dist/slices/backups/model.js +65 -0
- package/dist/slices/backups/repo.js +28 -0
- package/dist/slices/backups/schedule.js +60 -0
- package/dist/slices/backups/service.js +311 -0
- package/dist/slices/batch/index.js +2 -0
- package/dist/slices/estimate/index.js +65 -2
- package/dist/slices/fonts/catalog.js +23 -0
- package/dist/slices/fonts/index.js +1 -1
- package/dist/slices/library/model.js +8 -1
- package/dist/slices/library/slots.js +13 -2
- package/dist/slices/library/snapshot.js +8 -2
- package/dist/slices/notifications/notifier.js +76 -0
- package/dist/slices/notifications/rules.js +81 -0
- package/dist/slices/notifications/send.js +40 -0
- package/dist/slices/notifications/settings.js +34 -0
- package/dist/slices/play-drafts/convert.js +72 -4
- package/dist/slices/play-drafts/readiness.js +7 -3
- package/dist/slices/play-drafts/repo.js +7 -1
- package/dist/slices/play-drafts/review-inputs.js +2 -0
- package/dist/slices/play-drafts/schema.js +44 -3
- package/dist/slices/play-drafts/service.js +6 -0
- package/dist/slices/play-drafts/uploads.js +2 -1
- package/dist/slices/project-templates/from-project.js +42 -0
- package/dist/slices/project-templates/setup.js +13 -0
- package/dist/slices/rebuild/preview-details.js +53 -3
- package/dist/slices/rebuild/preview-plan.js +2 -2
- package/dist/slices/rebuild/recipe-build.js +13 -3
- package/dist/slices/rebuild/recipe-edit.js +178 -0
- package/dist/slices/rebuild/recipe-exports.js +9 -4
- package/dist/slices/rebuild/recipe-input-schema.js +9 -1
- package/dist/slices/rebuild/recipe-legacy.js +12 -0
- package/dist/slices/rebuild/recipe-model.js +7 -1
- package/dist/slices/rebuild/recipe-provider-choice.js +15 -0
- package/dist/slices/rebuild/recipe-reference.js +44 -0
- package/dist/slices/rebuild/recipe-save.js +25 -1
- package/dist/slices/rebuild/recipe-shorts.js +186 -0
- package/dist/slices/rebuild/recipe-validation.js +33 -2
- package/dist/slices/rebuild/recipe-visual.js +29 -8
- package/dist/slices/rebuild/recipe-work.js +83 -1
- package/dist/slices/rebuild/runtime-actions.js +9 -2
- package/dist/slices/rebuild/runtime-animate.js +110 -0
- package/dist/slices/rebuild/runtime-export-edit.js +119 -0
- package/dist/slices/rebuild/runtime-export.js +13 -1
- package/dist/slices/rebuild/runtime-image.js +50 -0
- package/dist/slices/rebuild/runtime-local.js +6 -3
- package/dist/slices/rebuild/runtime-materialize.js +48 -0
- package/dist/slices/rebuild/runtime-piece-label.js +17 -0
- package/dist/slices/rebuild/runtime-provider.js +17 -10
- package/dist/slices/rebuild/runtime-publication.js +25 -4
- package/dist/slices/rebuild/runtime-run.js +11 -5
- package/dist/slices/rebuild/runtime-shorts.js +328 -0
- package/dist/slices/rebuild/runtime-store.js +7 -3
- package/dist/slices/rebuild/service-readiness.js +15 -1
- package/dist/slices/rebuild/transition-repo.js +40 -1
- package/dist/slices/revisions/adopt-content.js +7 -0
- package/dist/slices/revisions/adopt.js +7 -2
- package/dist/slices/revisions/mutation-assets.js +51 -9
- package/dist/slices/revisions/mutation-prepare.js +14 -14
- package/dist/slices/revisions/mutations.js +2 -1
- package/dist/slices/revisions/publish.js +20 -2
- package/dist/slices/revisions/schema.js +16 -1
- package/dist/slices/schedules/repo.js +1 -1
- package/dist/slices/schedules/service.js +6 -1
- package/dist/slices/settings/model.js +5 -0
- package/dist/slices/shorts/captions.js +272 -0
- package/dist/slices/shorts/clips.js +89 -0
- package/dist/slices/shorts/model.js +186 -0
- package/dist/slices/shorts/pick.js +232 -0
- package/dist/slices/shorts/prompts.js +68 -0
- package/dist/slices/shorts/render.js +163 -0
- package/dist/slices/storage/asset-name.js +9 -2
- package/dist/slices/storage/layout.js +18 -0
- package/dist/slices/storage/model.js +9 -0
- package/dist/slices/storage/portable.js +6 -3
- package/dist/slices/storage/reconcile.js +5 -1
- package/dist/slices/storage/schema.js +3 -0
- package/dist/slices/telemetry/model.js +3 -0
- package/dist/slices/video/cards.js +72 -0
- package/dist/slices/video/chapters.js +89 -0
- package/dist/slices/video/cuts.js +141 -0
- package/dist/slices/video/edit-list.js +36 -6
- package/dist/slices/video/edit-settings.js +191 -0
- package/dist/slices/video/ffmpeg.js +155 -29
- package/dist/slices/video/look.js +164 -0
- package/dist/slices/video/plan.js +68 -11
- package/dist/slices/video/slideshow.js +22 -6
- package/dist/slices/video/transitions.js +51 -0
- package/dist/slices/youtube/transcript.js +54 -0
- package/dist/web/assets/index-CktpxlJd.js +171 -0
- package/dist/web/assets/index-DB-vOnGU.css +1 -0
- package/dist/web/assets/{pdf-DR97Byps.js → pdf-BLF2i8c2.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DfXmBzCk.js +0 -168
- package/dist/web/assets/index-Y3RhfTEh.css +0 -1
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { chapterCuts, narrationShotFrames } from "./cuts.js";
|
|
1
2
|
import { editListVersion, } from "./edit-list.js";
|
|
3
|
+
import { chapterCardSeconds } from "./edit-settings.js";
|
|
4
|
+
import { hasLook } from "./look.js";
|
|
2
5
|
import { motionFor } from "./motion.js";
|
|
6
|
+
import { withTransitions } from "./transitions.js";
|
|
3
7
|
// 30 fps, 1920×1080 for 16:9 and 1080×1920 for 9:16.
|
|
4
8
|
export const fps = 30;
|
|
5
9
|
const frames = {
|
|
@@ -39,6 +43,22 @@ export function planRender(input) {
|
|
|
39
43
|
? input.images.length * input.imageSeconds
|
|
40
44
|
: audio.reduce((sum, segment) => sum + segment.seconds, 0);
|
|
41
45
|
const totalFrames = Math.max(1, Math.round(totalSeconds * fps));
|
|
46
|
+
const edit = input.edit;
|
|
47
|
+
const narration = input.body === undefined ? undefined : edit?.narration;
|
|
48
|
+
const lengths = narration === undefined
|
|
49
|
+
? everyLengths(Math.max(1, Math.round(input.imageSeconds * fps)), totalFrames)
|
|
50
|
+
: narrationShotFrames({
|
|
51
|
+
totalFrames,
|
|
52
|
+
fps,
|
|
53
|
+
imageSeconds: input.imageSeconds,
|
|
54
|
+
cutPoints: narration.cutPoints,
|
|
55
|
+
chapterStarts: narration.chapterStarts,
|
|
56
|
+
});
|
|
57
|
+
const planned = shots(input, lengths);
|
|
58
|
+
const transition = edit?.transition;
|
|
59
|
+
const cards = edit?.cards === undefined
|
|
60
|
+
? []
|
|
61
|
+
: chapterCards(edit.cards.chapters, narration?.cutPoints ?? [], totalFrames);
|
|
42
62
|
return {
|
|
43
63
|
gapSeconds: input.gapSeconds,
|
|
44
64
|
edgeSeconds: input.edgeSeconds,
|
|
@@ -51,13 +71,39 @@ export function planRender(input) {
|
|
|
51
71
|
height: frame.height,
|
|
52
72
|
fps,
|
|
53
73
|
audio,
|
|
54
|
-
shots:
|
|
74
|
+
shots: transition === undefined
|
|
75
|
+
? planned
|
|
76
|
+
: withTransitions(planned, transition.kind, Math.round(transition.seconds * fps)),
|
|
77
|
+
...(edit?.look !== undefined && hasLook(edit.look) ? { look: edit.look } : {}),
|
|
78
|
+
...(cards.length > 0 && edit?.cards !== undefined
|
|
79
|
+
? { cards, cardFont: edit.cards.font }
|
|
80
|
+
: {}),
|
|
55
81
|
},
|
|
56
82
|
totalFrames,
|
|
57
83
|
totalSeconds,
|
|
58
84
|
output: input.output,
|
|
59
85
|
};
|
|
60
86
|
}
|
|
87
|
+
// A card at each chapter start, on the cut the chapter's shot starts with (a chapter at the
|
|
88
|
+
// very start stays there), for 2.5 s or what is left of the video.
|
|
89
|
+
function chapterCards(chapters, cutPoints, totalFrames) {
|
|
90
|
+
const length = Math.round(chapterCardSeconds * fps);
|
|
91
|
+
const cards = [];
|
|
92
|
+
for (const chapter of chapters) {
|
|
93
|
+
const title = chapter.title.trim();
|
|
94
|
+
const moved = chapter.start < 1 / fps ? [] : chapterCuts([chapter.start], cutPoints, Infinity);
|
|
95
|
+
const startFrame = Math.max(0, Math.round((moved[0] ?? chapter.start) * fps));
|
|
96
|
+
const frames = Math.min(length, totalFrames - startFrame);
|
|
97
|
+
if (title === "" || frames < 1)
|
|
98
|
+
continue;
|
|
99
|
+
// Two chapters inside 2.5 s would stack their cards; the second is left out.
|
|
100
|
+
const previous = cards.at(-1);
|
|
101
|
+
if (previous !== undefined && startFrame < previous.startFrame + previous.frames)
|
|
102
|
+
continue;
|
|
103
|
+
cards.push({ title, startFrame, frames });
|
|
104
|
+
}
|
|
105
|
+
return cards;
|
|
106
|
+
}
|
|
61
107
|
export function spoken(kind) {
|
|
62
108
|
return kind !== "gap" && kind !== "edge";
|
|
63
109
|
}
|
|
@@ -92,15 +138,26 @@ export function audioTimeline(input, minimumGap = 1 / fps) {
|
|
|
92
138
|
}
|
|
93
139
|
return segments;
|
|
94
140
|
}
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
// (`motion.ts`), so an image that comes round again may move another way.
|
|
99
|
-
function shots(input, each, totalFrames) {
|
|
141
|
+
// Every N seconds: each shot `each` frames until the timeline is full; the last shot is cut
|
|
142
|
+
// to what is left. A timeline shorter than one shot is a single shot.
|
|
143
|
+
function everyLengths(each, totalFrames) {
|
|
100
144
|
const count = Math.ceil(totalFrames / each);
|
|
101
|
-
return Array.from({ length: count }, (_value, at) => (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
145
|
+
return Array.from({ length: count }, (_value, at) => Math.min(each, totalFrames - each * at));
|
|
146
|
+
}
|
|
147
|
+
// The images take turns in slideshow order, one per shot, starting over after the last until
|
|
148
|
+
// the timeline is full. The motion goes by the shot's place, not the image (`motion.ts`), so
|
|
149
|
+
// an image that comes round again may move another way. A moving clip in an image's place
|
|
150
|
+
// plays as it is, with no motion of its own.
|
|
151
|
+
function shots(input, lengths) {
|
|
152
|
+
return lengths.map((frames, at) => {
|
|
153
|
+
const index = at % input.images.length;
|
|
154
|
+
const clip = input.edit?.clips?.[index];
|
|
155
|
+
return clip === undefined
|
|
156
|
+
? {
|
|
157
|
+
source: { kind: "image", path: input.images[index] ?? "" },
|
|
158
|
+
frames,
|
|
159
|
+
motion: motionFor(input.motionStyle, at, input.zoomPercent),
|
|
160
|
+
}
|
|
161
|
+
: { source: clip, frames, motion: { kind: "still" } };
|
|
162
|
+
});
|
|
106
163
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import {
|
|
1
|
+
import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { extname, join } from "node:path";
|
|
3
|
+
import { cardsAss } from "./cards.js";
|
|
4
|
+
import { concatList, joinArgs, runFfmpeg, segmentArgs, slideshowClips } from "./ffmpeg.js";
|
|
5
|
+
import { cardsIn } from "./look.js";
|
|
4
6
|
// estimate: a zoompan frame from a 4x still costs several plain encoded frames, and a
|
|
5
7
|
// copying join is nearly free, so progress is weighted by that rather than by clock time.
|
|
6
8
|
const clipWeight = 4;
|
|
@@ -12,24 +14,38 @@ export async function renderSlideshow(run) {
|
|
|
12
14
|
const joinMs = msOf(edit.shots.reduce((sum, shot) => sum + shot.frames, 0));
|
|
13
15
|
// Progress is reported against the video's length, the longer of picture and sound.
|
|
14
16
|
const totalMs = Math.max(joinMs, edit.audio.reduce((sum, segment) => sum + segment.seconds * 1000, 0));
|
|
15
|
-
const clipMs = clips.reduce((sum, clip) => sum + msOf(clip.
|
|
17
|
+
const clipMs = clips.reduce((sum, clip) => sum + msOf(clip.frames), 0);
|
|
16
18
|
const work = clipMs * clipWeight + joinMs * (run.burnSubtitles ? 1 : copyWeight);
|
|
17
19
|
const report = (done) => {
|
|
18
20
|
run.onProgress(work > 0 ? Math.round((Math.min(done, work) / work) * totalMs) : 0);
|
|
19
21
|
};
|
|
20
22
|
const workspace = mkdtempSync(join(run.scratch, "render-"));
|
|
21
23
|
try {
|
|
24
|
+
// Chapter cards are drawn by the clips from a script and a font beside them.
|
|
25
|
+
const cards = edit.cards ?? [];
|
|
26
|
+
const font = edit.cardFont;
|
|
27
|
+
if (cards.length > 0) {
|
|
28
|
+
if (font === undefined)
|
|
29
|
+
throw new Error("Slopify hit an internal error (the chapter cards have no font). Use Re-run section on Video; if it happens again, use Download diagnostics in Settings and report it.");
|
|
30
|
+
mkdirSync(join(workspace, "fonts"), { mode: 0o700 });
|
|
31
|
+
copyFileSync(font.path, join(workspace, "fonts", `card${extname(font.path)}`));
|
|
32
|
+
}
|
|
22
33
|
let rendered = 0;
|
|
23
34
|
for (const clip of clips) {
|
|
24
35
|
const before = rendered;
|
|
36
|
+
const script = clip.name.replace(/\.mp4$/, ".ass");
|
|
37
|
+
const shown = cardsIn(cards, clip.segment.start, clip.segment.count);
|
|
38
|
+
if (shown.length > 0 && font !== undefined)
|
|
39
|
+
writeFileSync(join(workspace, script), cardsAss(edit, shown, font.name, clip.segment.start), { mode: 0o600 });
|
|
25
40
|
await runFfmpeg({
|
|
26
41
|
bin: run.bin,
|
|
27
|
-
|
|
42
|
+
...(cards.length > 0 ? { cwd: workspace } : {}),
|
|
43
|
+
args: segmentArgs(edit, clip.segment, join(workspace, clip.name), run.burnSubtitles, script),
|
|
28
44
|
signal: run.signal,
|
|
29
45
|
log: run.log,
|
|
30
46
|
onProgress: (elapsedMs) => report(before + elapsedMs * clipWeight),
|
|
31
47
|
});
|
|
32
|
-
rendered += msOf(clip.
|
|
48
|
+
rendered += msOf(clip.frames) * clipWeight;
|
|
33
49
|
}
|
|
34
50
|
const list = join(workspace, "slides.ffconcat");
|
|
35
51
|
writeFileSync(list, concatList(order), { mode: 0o600 });
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Transitions without a longer render. The slideshow is still encoded clip by clip and
|
|
2
|
+
// joined by the concat demuxer (`ffmpeg.ts`); a transition is one more short clip between
|
|
3
|
+
// two shots. The shot before gives up its last floor(T / 2) frames and the shot after its
|
|
4
|
+
// first ceil(T / 2), and the T-frame transition clip plays the first shot's picture from
|
|
5
|
+
// where it was cut, running on past its end, blended into the second's from half a
|
|
6
|
+
// transition before its start. Every frame of the timeline is still played exactly once, so
|
|
7
|
+
// the video stays as long as its sound.
|
|
8
|
+
// The frames a transition of `frames` takes from the shot before and the shot after.
|
|
9
|
+
export function transitionHalves(frames) {
|
|
10
|
+
const tail = Math.floor(frames / 2);
|
|
11
|
+
return { tail, head: frames - tail };
|
|
12
|
+
}
|
|
13
|
+
// Gives every shot after the first a transition of `frames` frames, shortened where a shot is
|
|
14
|
+
// too short to give up its share and still show at least one frame of its own, and left a
|
|
15
|
+
// hard cut where under two frames would remain.
|
|
16
|
+
export function withTransitions(shots, kind, frames) {
|
|
17
|
+
return shots.map((shot, index) => {
|
|
18
|
+
const before = shots[index - 1];
|
|
19
|
+
if (before === undefined)
|
|
20
|
+
return shot;
|
|
21
|
+
const room = Math.floor((Math.min(before.frames, shot.frames) - 1) / 2);
|
|
22
|
+
const length = Math.min(Math.round(frames), room);
|
|
23
|
+
return length < 2 ? shot : { ...shot, transition: { kind, frames: length } };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
// The clips in timeline order, adding up to the shots' frames exactly.
|
|
27
|
+
export function segments(shots) {
|
|
28
|
+
const out = [];
|
|
29
|
+
let start = 0;
|
|
30
|
+
for (const [index, shot] of shots.entries()) {
|
|
31
|
+
const entering = shot.transition;
|
|
32
|
+
const leaving = shots[index + 1]?.transition;
|
|
33
|
+
const head = entering === undefined ? 0 : transitionHalves(entering.frames).head;
|
|
34
|
+
const tail = leaving === undefined ? 0 : transitionHalves(leaving.frames).tail;
|
|
35
|
+
if (entering !== undefined) {
|
|
36
|
+
const before = transitionHalves(entering.frames).tail;
|
|
37
|
+
out.push({
|
|
38
|
+
kind: "transition",
|
|
39
|
+
from: index - 1,
|
|
40
|
+
to: index,
|
|
41
|
+
count: entering.frames,
|
|
42
|
+
start: start - before,
|
|
43
|
+
});
|
|
44
|
+
start += head;
|
|
45
|
+
}
|
|
46
|
+
const count = shot.frames - head - tail;
|
|
47
|
+
out.push({ kind: "shot", shot: index, from: head, count, start });
|
|
48
|
+
start += count + tail;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
@@ -45,3 +45,57 @@ export function transcriptText(passages) {
|
|
|
45
45
|
.map((passage) => `[${youtubeTimestamp(passage.start)}] ${passage.text}`)
|
|
46
46
|
.join("\n");
|
|
47
47
|
}
|
|
48
|
+
// ceiling: a sentence with no full stop in 30 s (a list read out, a transcript without
|
|
49
|
+
// punctuation) is closed there, so no single number spans more than a short can hold.
|
|
50
|
+
const sentenceLongestSeconds = 30;
|
|
51
|
+
// The narration as numbered sentences, for the Shorts step: the model picks clips by
|
|
52
|
+
// sentence number and Slopify cuts at the sentences' own word times, so a clip never starts
|
|
53
|
+
// or ends mid-word. A sentence closes at its full stop, question or exclamation mark, at a
|
|
54
|
+
// pause of 1.5 s (the gap between intro, body and outro), or after 30 s.
|
|
55
|
+
export function transcriptSentences(words) {
|
|
56
|
+
const sentences = [];
|
|
57
|
+
let first = -1;
|
|
58
|
+
const flush = (last) => {
|
|
59
|
+
if (first < 0 || last < first)
|
|
60
|
+
return;
|
|
61
|
+
const span = words.slice(first, last + 1);
|
|
62
|
+
const text = span
|
|
63
|
+
.map((word) => word.text.replace(/\s+/g, " ").trim())
|
|
64
|
+
.filter((one) => one !== "")
|
|
65
|
+
.join(" ");
|
|
66
|
+
const opening = span[0];
|
|
67
|
+
const closing = span.at(-1);
|
|
68
|
+
if (text !== "" && opening !== undefined && closing !== undefined)
|
|
69
|
+
sentences.push({
|
|
70
|
+
number: sentences.length + 1,
|
|
71
|
+
start: opening.start,
|
|
72
|
+
end: closing.end,
|
|
73
|
+
text,
|
|
74
|
+
firstWord: first,
|
|
75
|
+
lastWord: last,
|
|
76
|
+
});
|
|
77
|
+
first = -1;
|
|
78
|
+
};
|
|
79
|
+
for (const [index, word] of words.entries()) {
|
|
80
|
+
const previous = words[index - 1];
|
|
81
|
+
const opening = first < 0 ? undefined : words[first];
|
|
82
|
+
if (opening !== undefined &&
|
|
83
|
+
previous !== undefined &&
|
|
84
|
+
(word.start - previous.end >= pauseSeconds ||
|
|
85
|
+
word.end - opening.start > sentenceLongestSeconds))
|
|
86
|
+
flush(index - 1);
|
|
87
|
+
if (first < 0)
|
|
88
|
+
first = index;
|
|
89
|
+
if (/[.!?]["'’”)]*$/.test(word.text.trim()))
|
|
90
|
+
flush(index);
|
|
91
|
+
}
|
|
92
|
+
flush(words.length - 1);
|
|
93
|
+
return sentences;
|
|
94
|
+
}
|
|
95
|
+
// One sentence per line, led by its number and its span in the video, which is what the
|
|
96
|
+
// model picks from.
|
|
97
|
+
export function sentencesText(sentences) {
|
|
98
|
+
return sentences
|
|
99
|
+
.map((sentence) => `[${String(sentence.number)}] (${youtubeTimestamp(sentence.start)}-${youtubeTimestamp(sentence.end)}) ${sentence.text}`)
|
|
100
|
+
.join("\n");
|
|
101
|
+
}
|