@gentbajko/slopify 0.8.3 → 0.8.4
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/SUBTITLES.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
For a completed project, open its final Video or Audio export section, set subtitles, and click **Save subtitles**. This rebuilds only the local export from saved narration and images. Changing font, size or position reuses word timing when the audio and spoken text are unchanged. A paused run saves these choices until Resume; pause an active run before editing. Failed alignment or rendering keeps the previous finished export.
|
|
10
10
|
|
|
11
|
+
If narration omits a short phrase, alignment can skip that transcript span when the surrounding speech is a strong match, including omissions inside an alignment window. The export records the omitted text and audio position for review. Recovery stays bounded and never recreates missing speech.
|
|
12
|
+
|
|
11
13
|
Audio Off disables subtitles. Video Off produces WAV audio with separate subtitle files. Uploaded audio must match the article; substantial mismatches fail with a message to correct the transcript. Review timing and spelling before publishing. English is supported first; unusual pronunciations and non-English passages can fail alignment.
|
|
12
14
|
|
|
13
15
|
Fonts are copied into the completed project's caption assets, so an existing export can reuse its chosen font even after the original system font is removed. Installed fonts and uploaded fonts stay local. SRT/VTT are portable text/timing files and do not embed a font; the chosen font is used in burned video captions.
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { alignWindow, frameSeconds, mismatch } from "./ctc.js";
|
|
2
2
|
import { agreesWithSpeech } from "./quality.js";
|
|
3
3
|
import { letters } from "./vocabulary.js";
|
|
4
|
-
// Recovery
|
|
4
|
+
// Recovery omits bounded transcript spans only after confirming surrounding speech.
|
|
5
5
|
export function alignSpeechWindow(logits, frames, candidate, complete, cutoff, skipBudget) {
|
|
6
6
|
try {
|
|
7
|
-
return {
|
|
7
|
+
return {
|
|
8
|
+
words: accepted(logits, frames, candidate, complete, cutoff),
|
|
9
|
+
skipped: 0,
|
|
10
|
+
omissionStart: 0,
|
|
11
|
+
};
|
|
8
12
|
}
|
|
9
13
|
catch (error) {
|
|
10
14
|
if (!(error instanceof Error) || error.message !== mismatch)
|
|
11
15
|
throw error;
|
|
12
16
|
}
|
|
17
|
+
if (skipBudget <= 0)
|
|
18
|
+
throw new Error(mismatch);
|
|
13
19
|
const heard = greedy(logits, frames).split(/\s+/);
|
|
14
20
|
for (let skipped = 1; skipped <= Math.min(40, skipBudget, candidate.length - 4); skipped += 1) {
|
|
15
21
|
const remaining = candidate.slice(skipped);
|
|
@@ -24,23 +30,58 @@ export function alignSpeechWindow(logits, frames, candidate, complete, cutoff, s
|
|
|
24
30
|
const words = accepted(logits, frames, remaining, complete, cutoff);
|
|
25
31
|
if (words.length < 4 || words.slice(0, 4).some((word) => (word.confidence ?? 0) < 0.75))
|
|
26
32
|
continue;
|
|
27
|
-
return { words, skipped };
|
|
33
|
+
return { words, skipped, omissionStart: 0 };
|
|
28
34
|
}
|
|
29
35
|
catch (error) {
|
|
30
36
|
if (!(error instanceof Error) || error.message !== mismatch)
|
|
31
37
|
throw error;
|
|
32
38
|
}
|
|
33
39
|
}
|
|
40
|
+
const observed = greedy(logits, frames).replace(/[^A-Z']/g, "");
|
|
41
|
+
for (let start = candidate.length - 4; start >= 4; start -= 1) {
|
|
42
|
+
const prefix = candidate
|
|
43
|
+
.slice(0, start)
|
|
44
|
+
.map((word) => word.spoken)
|
|
45
|
+
.join("")
|
|
46
|
+
.replace(/[^A-Z']/g, "");
|
|
47
|
+
if (prefix.length > observed.length)
|
|
48
|
+
continue;
|
|
49
|
+
if (!agreesWithSpeech(prefix, observed.slice(0, prefix.length), 0.2))
|
|
50
|
+
continue;
|
|
51
|
+
for (let skipped = 1; skipped <= Math.min(40, skipBudget, candidate.length - start - 4); skipped += 1) {
|
|
52
|
+
const remaining = [...candidate.slice(0, start), ...candidate.slice(start + skipped)];
|
|
53
|
+
const anchor = remaining
|
|
54
|
+
.slice(start, start + 4)
|
|
55
|
+
.map((word) => word.spoken)
|
|
56
|
+
.join("")
|
|
57
|
+
.replace(/[^A-Z']/g, "");
|
|
58
|
+
if (anchor.length < 20 ||
|
|
59
|
+
!agreesWithSpeech(anchor, observed.slice(prefix.length, prefix.length + anchor.length), 0.2) ||
|
|
60
|
+
!agreesWithSpeech(prefix + anchor, observed.slice(0, prefix.length + anchor.length), 0.15))
|
|
61
|
+
continue;
|
|
62
|
+
try {
|
|
63
|
+
const words = accepted(logits, frames, remaining, complete, cutoff, 0.2);
|
|
64
|
+
if (words.length < start + 4 ||
|
|
65
|
+
words.slice(start - 4, start + 4).some((word) => (word.confidence ?? 0) < 0.75))
|
|
66
|
+
continue;
|
|
67
|
+
return { words, skipped, omissionStart: start };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (!(error instanceof Error) || error.message !== mismatch)
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
34
75
|
throw new Error(mismatch);
|
|
35
76
|
}
|
|
36
|
-
function accepted(logits, frames, candidate, complete, cutoff) {
|
|
77
|
+
function accepted(logits, frames, candidate, complete, cutoff, maximumError = 0.42) {
|
|
37
78
|
const words = alignWindow(logits, frames, candidate, complete).words.filter((word) => word.end <= cutoff);
|
|
38
79
|
const last = words.at(-1);
|
|
39
80
|
if (last === undefined ||
|
|
40
81
|
!agreesWithSpeech(candidate
|
|
41
82
|
.slice(0, words.length)
|
|
42
83
|
.map((word) => word.spoken)
|
|
43
|
-
.join(" "), greedy(logits, Math.min(frames, Math.ceil(last.end / frameSeconds)))))
|
|
84
|
+
.join(" "), greedy(logits, Math.min(frames, Math.ceil(last.end / frameSeconds))), maximumError))
|
|
44
85
|
throw new Error(mismatch);
|
|
45
86
|
return words;
|
|
46
87
|
}
|
|
@@ -73,9 +73,9 @@ async function run(input) {
|
|
|
73
73
|
omitted += recovered.skipped;
|
|
74
74
|
send({
|
|
75
75
|
type: "omission",
|
|
76
|
-
start: sampleAt / sampleRate,
|
|
76
|
+
start: sampleAt / sampleRate + (accepted[recovered.omissionStart - 1]?.end ?? 0),
|
|
77
77
|
text: candidate
|
|
78
|
-
.slice(
|
|
78
|
+
.slice(recovered.omissionStart, recovered.omissionStart + recovered.skipped)
|
|
79
79
|
.map((word) => word.text)
|
|
80
80
|
.join(" "),
|
|
81
81
|
});
|