@gentbajko/slopify 2.0.3 → 2.1.1
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 +1 -1
- package/dist/adapters/alignment/threads.js +11 -0
- package/dist/adapters/alignment/worker.js +38 -4
- package/dist/adapters/host-cli/index.js +25 -1
- package/dist/assets/document/SOURCE.txt +7 -0
- package/dist/assets/document/background.jpg +0 -0
- package/dist/assets/document/fonts/Cinzel-Black.ttf +0 -0
- package/dist/assets/document/fonts/Cinzel-Bold.ttf +0 -0
- package/dist/assets/document/fonts/Cinzel-Medium.ttf +0 -0
- package/dist/assets/document/fonts/Cinzel-Regular.ttf +0 -0
- package/dist/assets/document/fonts/OFL.txt +93 -0
- package/dist/assets/document/fonts/SOURCE.txt +11 -0
- package/dist/catalog/validate.js +3 -2
- package/dist/edge/docker-projects/state.js +5 -1
- package/dist/edge/host-cli.js +14 -0
- package/dist/edge/http/folder-location-schema.js +5 -8
- package/dist/edge/http/folder-location.js +10 -5
- package/dist/edge/http/host-cli.js +27 -2
- package/dist/edge/http/revision-files.js +5 -1
- package/dist/host-cli/open-folder.js +95 -0
- package/dist/host-cli/runtime.js +2 -0
- package/dist/kernel/db/migrate.js +25 -1
- package/dist/kernel/db/migrations/0014-document-stage.sql +44 -0
- package/dist/kernel/db/migrations/0015-description-prompts.sql +13 -0
- package/dist/kernel/pipeline.js +11 -1
- package/dist/kernel/ports/host-cli.js +14 -0
- package/dist/kernel/runner/graph.js +6 -1
- package/dist/main.js +6 -2
- package/dist/slices/admission/model.js +7 -1
- package/dist/slices/admission/rules.js +67 -3
- package/dist/slices/admission/schema.js +16 -3
- package/dist/slices/admission/start.js +3 -2
- package/dist/slices/document/blocks.js +129 -0
- package/dist/slices/document/flow.js +117 -0
- package/dist/slices/document/fonts.js +54 -0
- package/dist/slices/document/model.js +11 -0
- package/dist/slices/document/pages.js +198 -0
- package/dist/slices/document/render.js +242 -0
- package/dist/slices/document/sources.js +35 -0
- package/dist/slices/document/theme.js +158 -0
- package/dist/slices/document/writer.js +66 -0
- package/dist/slices/estimate/index.js +14 -2
- package/dist/slices/library/model.js +1 -1
- package/dist/slices/library/slots.js +4 -1
- package/dist/slices/play-drafts/convert.js +30 -1
- package/dist/slices/play-drafts/readiness.js +3 -2
- package/dist/slices/play-drafts/schema.js +22 -5
- package/dist/slices/project-templates/from-project.js +10 -0
- package/dist/slices/project-templates/setup.js +1 -0
- package/dist/slices/rebuild/preview-details.js +11 -1
- package/dist/slices/rebuild/recipe-audio.js +3 -0
- package/dist/slices/rebuild/recipe-build.js +6 -1
- package/dist/slices/rebuild/recipe-document.js +36 -0
- package/dist/slices/rebuild/recipe-exports.js +14 -2
- package/dist/slices/rebuild/recipe-fixture.js +4 -0
- package/dist/slices/rebuild/recipe-legacy.js +5 -0
- package/dist/slices/rebuild/recipe-model.js +2 -0
- package/dist/slices/rebuild/recipe-provider-choice.js +3 -0
- package/dist/slices/rebuild/recipe-save.js +15 -3
- package/dist/slices/rebuild/recipe-validation.js +15 -2
- package/dist/slices/rebuild/recipe-visual.js +10 -1
- package/dist/slices/rebuild/recipe-work.js +15 -0
- package/dist/slices/rebuild/recipe-youtube.js +33 -0
- package/dist/slices/rebuild/recovery-selection.js +2 -0
- package/dist/slices/rebuild/recovery.js +10 -5
- package/dist/slices/rebuild/runtime-actions.js +2 -0
- package/dist/slices/rebuild/runtime-document.js +85 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +1 -0
- package/dist/slices/rebuild/runtime-export.js +35 -19
- package/dist/slices/rebuild/runtime-piece-label.js +4 -0
- package/dist/slices/rebuild/runtime-publication.js +1 -0
- package/dist/slices/rebuild/runtime-run.js +11 -3
- package/dist/slices/rebuild/runtime-store.js +7 -2
- package/dist/slices/rebuild/runtime-subtitles.js +6 -3
- package/dist/slices/rebuild/runtime-youtube.js +91 -0
- package/dist/slices/rebuild/transition-repo.js +2 -0
- package/dist/slices/reruns/cascade.js +6 -4
- package/dist/slices/revisions/mutation-cues.js +3 -1
- package/dist/slices/revisions/projection.js +2 -1
- package/dist/slices/storage/downloads.js +1 -0
- package/dist/slices/storage/layout.js +6 -0
- package/dist/slices/storage/model.js +3 -0
- package/dist/slices/subtitles/prepare.js +2 -1
- package/dist/slices/telemetry/model.js +4 -1
- package/dist/slices/video/audio-export.js +11 -3
- package/dist/slices/video/edit-list.js +73 -0
- package/dist/slices/video/ffmpeg.js +123 -57
- package/dist/slices/video/motion.js +51 -0
- package/dist/slices/video/plan.js +61 -27
- package/dist/slices/video/reuse-audio.js +2 -0
- package/dist/slices/video/run.js +21 -10
- package/dist/slices/video/slideshow.js +48 -0
- package/dist/slices/video/write-export.js +17 -25
- package/dist/slices/youtube/answer.js +199 -0
- package/dist/slices/youtube/model.js +27 -0
- package/dist/slices/youtube/timestamps.js +24 -0
- package/dist/slices/youtube/transcript.js +47 -0
- package/dist/web/assets/index-B6NbhpjD.css +1 -0
- package/dist/web/assets/index-DV01Cyjx.js +142 -0
- package/dist/web/index.html +2 -2
- package/package.json +4 -2
- package/dist/web/assets/index-Bwtmu9ei.css +0 -1
- package/dist/web/assets/index-DjhMJqMN.js +0 -141
package/SUBTITLES.md
CHANGED
|
@@ -16,7 +16,7 @@ Fonts are copied into the completed project's caption assets, so an existing exp
|
|
|
16
16
|
|
|
17
17
|
## Local model and licenses
|
|
18
18
|
|
|
19
|
-
Speech inference uses MIT-licensed `onnxruntime-
|
|
19
|
+
Speech inference uses MIT-licensed `onnxruntime-node@1.30.0` (native CPU) in a separate process, with `min(8, CPUs − 1)` threads; set `SLOPIFY_SUBTITLE_THREADS` to override. The npm package bundles CPU builds for Linux x64/arm64, Windows x64/arm64 and Apple silicon macOS (about 220 MB unpacked); Intel Macs have no build, so subtitle timing falls back to the slower single-threaded `onnxruntime-web` there. Its postinstall script only fetches optional CUDA files on Linux x64 and is not needed (the Docker image installs with `--ignore-scripts`); model weights are downloaded lazily into `<data-dir>/models/english-subtitles/`.
|
|
20
20
|
|
|
21
21
|
Model: [Xenova/wav2vec2-base-960h](https://huggingface.co/Xenova/wav2vec2-base-960h/tree/a19f851b3d42865797e410752b4c570c871e4825), an ONNX conversion of [facebook/wav2vec2-base-960h](https://huggingface.co/facebook/wav2vec2-base-960h), licensed Apache-2.0. Pinned revision: `a19f851b3d42865797e410752b4c570c871e4825`. The quantized model is 95,286,046 bytes, SHA256 `cd5040c147381580ed73258143dd8e0c28e800a09e74ee42ee2b3e8cb4d760a3`; every cached/downloaded model is verified before use.
|
|
22
22
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { availableParallelism } from "node:os";
|
|
2
|
+
// Measured on a 32-thread machine, 5 min of narration in 12 s windows: one native CPU session with
|
|
3
|
+
// 8 intra-op threads ran 37× realtime; 16 threads 31×, 32 threads 20×, 1 thread 20×, and the old
|
|
4
|
+
// single-threaded WASM runtime 4.8×. More threads in one session gets slower, so 8 is the ceiling.
|
|
5
|
+
const maxThreads = 8;
|
|
6
|
+
export function subtitleThreads(env = process.env, cpus = availableParallelism()) {
|
|
7
|
+
const override = env.SLOPIFY_SUBTITLE_THREADS?.trim();
|
|
8
|
+
if (override !== undefined && /^[1-9]\d*$/.test(override))
|
|
9
|
+
return Number(override);
|
|
10
|
+
return Math.min(maxThreads, Math.max(1, cpus - 1));
|
|
11
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { open, readFile, stat } from "node:fs/promises";
|
|
2
|
-
import * as ort from "onnxruntime-web/wasm";
|
|
3
2
|
import { SubtitleMismatch } from "../../kernel/ports/subtitles.js";
|
|
4
3
|
import { mismatch } from "./ctc.js";
|
|
5
4
|
import { workerInput } from "./protocol.js";
|
|
6
5
|
import { speechWords } from "./text.js";
|
|
6
|
+
import { subtitleThreads } from "./threads.js";
|
|
7
7
|
import { alignSpeechWindow, greedy } from "./window.js";
|
|
8
8
|
const sampleRate = 16000;
|
|
9
9
|
const windowSeconds = 12;
|
|
@@ -26,9 +26,7 @@ function send(message) {
|
|
|
26
26
|
process.send?.(message);
|
|
27
27
|
}
|
|
28
28
|
async function run(input) {
|
|
29
|
-
ort
|
|
30
|
-
const model = new Uint8Array(await readFile(input.modelPath));
|
|
31
|
-
const session = await ort.InferenceSession.create(model, { executionProviders: ["wasm"] });
|
|
29
|
+
const { ort, session } = await openSession(input.modelPath);
|
|
32
30
|
const file = await open(input.pcmPath, "r");
|
|
33
31
|
try {
|
|
34
32
|
const totalSamples = (await stat(input.pcmPath)).size / 4;
|
|
@@ -139,6 +137,42 @@ async function run(input) {
|
|
|
139
137
|
await session.release();
|
|
140
138
|
}
|
|
141
139
|
}
|
|
140
|
+
// One native CPU session per job, as benchmarked; threads.ts records the measured speeds. The
|
|
141
|
+
// native runtime loads its binary on import, so a missing or foreign binary fails there; that
|
|
142
|
+
// system (an Intel Mac has no 1.24 build) falls back to the single-threaded WebAssembly runtime,
|
|
143
|
+
// about 7× slower but the same model, rather than losing captions.
|
|
144
|
+
async function openSession(modelPath) {
|
|
145
|
+
let native;
|
|
146
|
+
try {
|
|
147
|
+
native = await import("onnxruntime-node");
|
|
148
|
+
}
|
|
149
|
+
catch (nativeError) {
|
|
150
|
+
try {
|
|
151
|
+
const web = await import("onnxruntime-web/wasm");
|
|
152
|
+
web.env.wasm.numThreads = 1;
|
|
153
|
+
const session = await web.InferenceSession.create(new Uint8Array(await readFile(modelPath)), {
|
|
154
|
+
executionProviders: ["wasm"],
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
ort: web,
|
|
158
|
+
session: session,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
const system = `${process.platform} ${process.arch}`;
|
|
163
|
+
const reason = nativeError instanceof Error
|
|
164
|
+
? (nativeError.message.split("\n")[0] ?? "")
|
|
165
|
+
: String(nativeError);
|
|
166
|
+
throw new Error(`Subtitle timing could not start: neither of its speech engines loaded on this computer (${system}): ${reason}. Reinstall Slopify so npm installs the engine for this system, then try again; if it keeps failing, turn subtitles off for this project.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const session = await native.InferenceSession.create(modelPath, {
|
|
170
|
+
executionProviders: ["cpu"],
|
|
171
|
+
intraOpNumThreads: subtitleThreads(),
|
|
172
|
+
interOpNumThreads: 1,
|
|
173
|
+
});
|
|
174
|
+
return { ort: native, session };
|
|
175
|
+
}
|
|
142
176
|
function candidates(source, cursor, observed) {
|
|
143
177
|
const selected = [];
|
|
144
178
|
let length = 0;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { redact } from "../../kernel/log.js";
|
|
2
|
-
import { bridgeLimits, hostFaultSchema, hostFrameSchema, hostHealthSchema, hostImageSchema, hostLlmSchema, hostModelsSchema, hostStatusSchema, } from "../../kernel/ports/host-cli.js";
|
|
2
|
+
import { bridgeLimits, hostFaultSchema, hostFrameSchema, hostHealthSchema, hostImageSchema, hostLlmSchema, hostModelsSchema, hostOpenedSchema, hostOpenFolderSchema, hostStatusSchema, } from "../../kernel/ports/host-cli.js";
|
|
3
3
|
import { isProviderError, providerError } from "../../kernel/ports/model.js";
|
|
4
4
|
import { sniffImage } from "../image/bytes.js";
|
|
5
5
|
import { hostRequest, hostUnavailable, readHostBytes, } from "./transport.js";
|
|
@@ -50,6 +50,30 @@ export function createHostCliClient(options) {
|
|
|
50
50
|
...(model.thinkingModes === undefined ? {} : { thinkingModes: model.thinkingModes }),
|
|
51
51
|
}));
|
|
52
52
|
return {
|
|
53
|
+
openFolder: async (path, signal) => {
|
|
54
|
+
const parsed = hostOpenFolderSchema.safeParse({ path });
|
|
55
|
+
if (!parsed.success || !options.directory)
|
|
56
|
+
return false;
|
|
57
|
+
// An older helper answers 404 and a refused folder 403; either way the caller shows the path.
|
|
58
|
+
try {
|
|
59
|
+
const response = await hostRequest({
|
|
60
|
+
directory: options.directory,
|
|
61
|
+
path: "/v1/open-folder",
|
|
62
|
+
method: "POST",
|
|
63
|
+
kind: "metadata",
|
|
64
|
+
body: Buffer.from(JSON.stringify(parsed.data)),
|
|
65
|
+
signal,
|
|
66
|
+
});
|
|
67
|
+
if (response.statusCode !== 200) {
|
|
68
|
+
response.destroy();
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return hostOpenedSchema.safeParse(JSON.parse((await readHostBytes(response, bridgeLimits.status)).toString("utf8"))).success;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
53
77
|
status: async (id) => {
|
|
54
78
|
try {
|
|
55
79
|
const status = await metadata(`/v1/status/${id}`, hostStatusSchema);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
background.jpg
|
|
2
|
+
The parchment page texture of the DiceMaster document theme, from the lore2script2 PDF
|
|
3
|
+
generator (same owner).
|
|
4
|
+
SHA-256: 18f9701b476f299e942cbb99c05ba3267d8341df2b6136e3aebbafad83b03b31
|
|
5
|
+
|
|
6
|
+
fonts/
|
|
7
|
+
Cinzel, SIL Open Font License 1.1: see fonts/SOURCE.txt and fonts/OFL.txt.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2020 The Cinzel Project Authors (https://github.com/NDISCOVER/Cinzel)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Cinzel-Regular.ttf, Cinzel-Medium.ttf, Cinzel-Bold.ttf, Cinzel-Black.ttf
|
|
2
|
+
Cinzel 2.000 by Natanael Gama, from the embedded font data of the lore2script2 PDF generator
|
|
3
|
+
(same owner). Copyright and licence are read from each file's own name table.
|
|
4
|
+
Upstream: https://github.com/NDISCOVER/Cinzel
|
|
5
|
+
|
|
6
|
+
OFL.txt
|
|
7
|
+
SIL Open Font License 1.1, with the copyright line from the fonts' name table.
|
|
8
|
+
7445469e9f302618200b15b1645d6aa3565b93a6634a0f9e5cd069c7fc2343fe Cinzel-Black.ttf
|
|
9
|
+
cbe8f6a665cc58d4540b6ce2a29e6f1b7e096fa6835aea1d2ffb40c088c5cdf6 Cinzel-Bold.ttf
|
|
10
|
+
3d50001a451ba6cfd3ef07a2dfb0c564980a5229f70decdfa23f08f4a30e1bed Cinzel-Medium.ttf
|
|
11
|
+
540b628614b728fc87e50b883189a524dac4d8163ca4a0f8e49b499910762e86 Cinzel-Regular.ttf
|
package/dist/catalog/validate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { usesNarrationPreparation } from "../slices/admission/rules.js";
|
|
1
|
+
import { usesNarrationPreparation, usesYoutubeDescription, } from "../slices/admission/rules.js";
|
|
2
2
|
import { isLocalCliProvider } from "../slices/settings/model.js";
|
|
3
3
|
export function modelFields(draft, catalogue) {
|
|
4
4
|
if (!catalogue)
|
|
@@ -9,7 +9,8 @@ export function modelFields(draft, catalogue) {
|
|
|
9
9
|
draft.sources.thumbnail === "prompt_by_llm" ||
|
|
10
10
|
draft.intro?.mode === "llm" ||
|
|
11
11
|
draft.outro?.mode === "llm" ||
|
|
12
|
-
usesNarrationPreparation(draft)
|
|
12
|
+
usesNarrationPreparation(draft) ||
|
|
13
|
+
usesYoutubeDescription(draft);
|
|
13
14
|
const checks = [
|
|
14
15
|
{ field: "llm", family: "llm", choice: draft.llm, needed: needLlm },
|
|
15
16
|
{
|
|
@@ -104,10 +104,14 @@ export const journalSchema = z
|
|
|
104
104
|
token: z.string().regex(/^[a-f0-9]{64}$/),
|
|
105
105
|
})
|
|
106
106
|
.strict();
|
|
107
|
+
/** Where the Docker launcher keeps one folder (and receipt) per installation. */
|
|
108
|
+
export function dockerRoot(env, home) {
|
|
109
|
+
return join(absolute.parse(env.XDG_DATA_HOME || join(home, ".local/share")), "slopify/docker");
|
|
110
|
+
}
|
|
107
111
|
export function dockerConfig(env, home, cwd, uid, gid) {
|
|
108
112
|
const name = identifier.parse(env.SLOPIFY_DOCKER_NAME || "slopify");
|
|
109
113
|
const volume = identifier.parse(env.SLOPIFY_DOCKER_VOLUME || "slopify-data");
|
|
110
|
-
const root =
|
|
114
|
+
const root = dockerRoot(env, home);
|
|
111
115
|
const raw = env.SLOPIFY_DOCKER_PROJECTS_DIR || null;
|
|
112
116
|
const projectsOverride = raw === null ? null : resolve(cwd, raw.startsWith("~/") ? join(home, raw.slice(2)) : raw);
|
|
113
117
|
const portText = env.SLOPIFY_DOCKER_HOST_PORT || "6969";
|
package/dist/edge/host-cli.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
2
|
import { open } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { parseArgs } from "node:util";
|
|
5
6
|
import { nodeRunCli } from "../adapters/llm/run-cli.js";
|
|
6
7
|
import { hostEnvironmentSchema } from "../host-cli/environment.js";
|
|
8
|
+
import { launchXdgOpen } from "../host-cli/open-folder.js";
|
|
7
9
|
import { ensureBridgeToken, prepareHostPaths } from "../host-cli/paths.js";
|
|
8
10
|
import { createHostRuntime } from "../host-cli/runtime.js";
|
|
9
11
|
import { startHostServer } from "../host-cli/server.js";
|
|
10
12
|
import { resolveHostCommand } from "../host-cli/status.js";
|
|
11
13
|
import { readVersion } from "../kernel/version.js";
|
|
12
14
|
import { nodeCliProbe } from "../slices/settings/cli-status.js";
|
|
15
|
+
import { dockerRoot } from "./docker-projects/state.js";
|
|
13
16
|
const { values } = parseArgs({ options: { "state-dir": { type: "string" } }, strict: true });
|
|
14
17
|
if (!values["state-dir"])
|
|
15
18
|
throw new Error("A host helper state directory is required.");
|
|
@@ -32,6 +35,14 @@ try {
|
|
|
32
35
|
finally {
|
|
33
36
|
await file.close();
|
|
34
37
|
}
|
|
38
|
+
// Without a usable data folder there are no launcher receipts, so Open folder stays unsupported.
|
|
39
|
+
let folderRoot;
|
|
40
|
+
try {
|
|
41
|
+
folderRoot = dockerRoot(process.env, process.env.HOME || homedir());
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
folderRoot = undefined;
|
|
45
|
+
}
|
|
35
46
|
const server = await startHostServer({
|
|
36
47
|
directory: paths.share,
|
|
37
48
|
token: await ensureBridgeToken(paths.tokenFile),
|
|
@@ -42,6 +53,9 @@ const server = await startHostServer({
|
|
|
42
53
|
env: process.env,
|
|
43
54
|
now: Date.now,
|
|
44
55
|
resolve: (id) => resolveHostCommand(id, process.env),
|
|
56
|
+
...(process.getuid === undefined || folderRoot === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: { folders: { root: folderRoot, uid: process.getuid(), launch: launchXdgOpen } }),
|
|
45
59
|
}),
|
|
46
60
|
});
|
|
47
61
|
process.on("SIGHUP", server.pauseAdmissions);
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
|
|
2
|
+
const dockerHost = { location: z.literal("docker-host"), path: z.string().min(1) };
|
|
3
|
+
// In Docker the host helper may open the folder itself; the host path is sent either way.
|
|
4
|
+
export const folderReplySchema = z.union([
|
|
3
5
|
z.object({ opened: z.literal(true) }).strict(),
|
|
4
|
-
z
|
|
5
|
-
|
|
6
|
-
opened: z.literal(false),
|
|
7
|
-
location: z.literal("docker-host"),
|
|
8
|
-
path: z.string().min(1),
|
|
9
|
-
})
|
|
10
|
-
.strict(),
|
|
6
|
+
z.object({ opened: z.literal(true), ...dockerHost }).strict(),
|
|
7
|
+
z.object({ opened: z.literal(false), ...dockerHost }).strict(),
|
|
11
8
|
]);
|
|
@@ -27,11 +27,16 @@ export async function replyForFolder(c, deps, projectId, file) {
|
|
|
27
27
|
if (s.isSymbolicLink() || (i === parts.length - 1 ? !s.isFile() : !s.isDirectory()))
|
|
28
28
|
throw new Error("Unavailable output");
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
const hostPath = join(host, dirname(r));
|
|
31
|
+
const opened = (await deps.folderLocation
|
|
32
|
+
.openOnHost?.(hostPath, AbortSignal.timeout(10_000))
|
|
33
|
+
.catch(() => false)) === true;
|
|
34
|
+
if (!opened && deps.folderLocation.openOnHost)
|
|
35
|
+
deps.log.write("warn", "project.open-folder", {
|
|
36
|
+
projectId,
|
|
37
|
+
detail: "The host helper did not open the folder (not installed, too old, or it refused), so the host path was shown instead.",
|
|
38
|
+
});
|
|
39
|
+
return c.json(folderReplySchema.parse({ opened, location: "docker-host", path: hostPath }));
|
|
35
40
|
}
|
|
36
41
|
catch {
|
|
37
42
|
deps.log.write("warn", "project.locate-folder", {
|
|
@@ -5,7 +5,7 @@ import { stream } from "hono/streaming";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { sniffImage } from "../../adapters/image/bytes.js";
|
|
7
7
|
import { redact } from "../../kernel/log.js";
|
|
8
|
-
import { bridgeLimits, hostCliIds, hostCliProtocol, hostFrameSchema, hostImageSchema, hostLlmIds, hostLlmSchema, hostModelsSchema, hostStatusSchema, } from "../../kernel/ports/host-cli.js";
|
|
8
|
+
import { bridgeLimits, HostFolderRefused, hostCliIds, hostCliProtocol, hostFrameSchema, hostImageSchema, hostLlmIds, hostLlmSchema, hostModelsSchema, hostOpenFolderSchema, hostStatusSchema, } from "../../kernel/ports/host-cli.js";
|
|
9
9
|
import { isProviderError, providerError } from "../../kernel/ports/model.js";
|
|
10
10
|
export function hostGate() {
|
|
11
11
|
let accepting = true;
|
|
@@ -82,7 +82,8 @@ export function hostCliRoutes(options) {
|
|
|
82
82
|
accepting: gate.accepting,
|
|
83
83
|
}));
|
|
84
84
|
app.use("/v1/*", async (c, next) => {
|
|
85
|
-
|
|
85
|
+
// Opening a folder is quick and must not look like running work to the launcher's upgrade check.
|
|
86
|
+
const kind = c.req.method === "POST" && c.req.path !== "/v1/open-folder" ? "generation" : "metadata";
|
|
86
87
|
const job = gate.acquire(kind);
|
|
87
88
|
if (!job)
|
|
88
89
|
return c.json({
|
|
@@ -244,6 +245,30 @@ export function hostCliRoutes(options) {
|
|
|
244
245
|
controller.abort();
|
|
245
246
|
}
|
|
246
247
|
});
|
|
248
|
+
app.post("/v1/open-folder", async (c) => {
|
|
249
|
+
const open = ports.openFolder;
|
|
250
|
+
if (!open)
|
|
251
|
+
return c.notFound();
|
|
252
|
+
const body = hostOpenFolderSchema.safeParse(await json(c.req.raw));
|
|
253
|
+
if (!body.success)
|
|
254
|
+
return invalid(c);
|
|
255
|
+
try {
|
|
256
|
+
await open(body.data.path);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
const refused = error instanceof HostFolderRefused;
|
|
260
|
+
return c.json({
|
|
261
|
+
type: "error",
|
|
262
|
+
kind: "unavailable",
|
|
263
|
+
message: (error instanceof Error
|
|
264
|
+
? error.message
|
|
265
|
+
: "The Slopify host helper could not open the folder. Copy the path into your file manager instead.")
|
|
266
|
+
.replaceAll(token, "[redacted]")
|
|
267
|
+
.slice(0, 4096),
|
|
268
|
+
}, refused ? 403 : 503);
|
|
269
|
+
}
|
|
270
|
+
return c.json({ opened: true });
|
|
271
|
+
});
|
|
247
272
|
app.onError((error, c) => c.json(fault(error, token), 503));
|
|
248
273
|
app.notFound((c) => c.json({
|
|
249
274
|
type: "error",
|
|
@@ -33,10 +33,14 @@ export function revisionFileRoutes(deps) {
|
|
|
33
33
|
if (!result.ok)
|
|
34
34
|
return unavailable(c, result.reason);
|
|
35
35
|
const value = result.download;
|
|
36
|
+
// `?inline=1` lets the project page open a PDF in a browser tab instead of saving
|
|
37
|
+
// it. Only PDFs: anything else the browser might render stays a download.
|
|
38
|
+
const inline = c.req.query("inline") === "1" && value.contentType === "application/pdf";
|
|
36
39
|
return c.body(Readable.toWeb(createReadStream(value.path)), 200, {
|
|
37
40
|
"content-type": value.contentType,
|
|
38
41
|
"content-length": String(value.bytes),
|
|
39
|
-
"content-disposition":
|
|
42
|
+
"content-disposition": `${inline ? "inline" : "attachment"}; filename="${value.filename}"`,
|
|
43
|
+
"x-content-type-options": "nosniff",
|
|
40
44
|
});
|
|
41
45
|
});
|
|
42
46
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
4
|
+
import { readState, receiptSchema } from "../edge/docker-projects/state.js";
|
|
5
|
+
import { HostFolderRefused } from "../kernel/ports/host-cli.js";
|
|
6
|
+
import { hasCode } from "./paths.js";
|
|
7
|
+
const again = "run the Docker launcher again (npx @gentbajko/slopify@latest --docker)";
|
|
8
|
+
/** Starts the desktop's opener and returns once it is running; the file manager is not awaited. */
|
|
9
|
+
export function launchXdgOpen(directory) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const child = spawn("xdg-open", [directory], {
|
|
12
|
+
detached: true,
|
|
13
|
+
shell: false,
|
|
14
|
+
stdio: "ignore",
|
|
15
|
+
});
|
|
16
|
+
child.once("error", (error) => {
|
|
17
|
+
reject(new Error(hasCode(error, "ENOENT")
|
|
18
|
+
? "xdg-open isn't installed on this machine, so the Slopify host helper can't open a file manager. Install xdg-utils (for example sudo pacman -S xdg-utils or sudo apt install xdg-utils), then use Open folder again."
|
|
19
|
+
: `The Slopify host helper could not start xdg-open to open ${directory}. Check that you are signed in to this computer's desktop, then use Open folder again.`));
|
|
20
|
+
});
|
|
21
|
+
child.once("spawn", () => {
|
|
22
|
+
child.unref();
|
|
23
|
+
resolve();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** Only folders inside a launcher receipt's project folder, reached through real folders you own, are opened. */
|
|
28
|
+
export function createHostFolderOpener(deps) {
|
|
29
|
+
async function receipts() {
|
|
30
|
+
const found = [];
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
const root = await lstat(deps.root);
|
|
34
|
+
if (!root.isDirectory() || root.isSymbolicLink() || root.uid !== deps.uid)
|
|
35
|
+
return [];
|
|
36
|
+
entries = await readdir(deps.root, { withFileTypes: true });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (!entry.isDirectory())
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
const receipt = await readState(join(deps.root, entry.name, "receipt.json"), receiptSchema, deps.uid);
|
|
46
|
+
if (receipt)
|
|
47
|
+
found.push(receipt);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// An unreadable or damaged receipt grants nothing; the others still count.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
async function verify(projects, identity, r) {
|
|
56
|
+
const root = await lstat(projects, { bigint: true }).catch(() => undefined);
|
|
57
|
+
if (!root?.isDirectory() ||
|
|
58
|
+
root.isSymbolicLink() ||
|
|
59
|
+
root.uid !== BigInt(deps.uid) ||
|
|
60
|
+
String(root.dev) !== identity.dev ||
|
|
61
|
+
String(root.ino) !== identity.ino)
|
|
62
|
+
throw new HostFolderRefused(`The Slopify host helper did not open this folder because the project folder ${projects} was moved or replaced since the Docker launcher set it up. Put the original folder back, or ${again}.`);
|
|
63
|
+
let current = projects;
|
|
64
|
+
for (const part of r === "" ? [] : r.split(sep)) {
|
|
65
|
+
current = join(current, part);
|
|
66
|
+
const s = await lstat(current).catch(() => undefined);
|
|
67
|
+
if (!s?.isDirectory() || s.isSymbolicLink() || s.uid !== deps.uid)
|
|
68
|
+
throw new HostFolderRefused(`The Slopify host helper did not open ${current} because it is missing, a link or a file, or isn't owned by you. Use Re-run section on the project page to make the file again, or copy the path into your file manager.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return async (path) => {
|
|
72
|
+
if (!isAbsolute(path) || /[\p{Cc}]/u.test(path))
|
|
73
|
+
throw new HostFolderRefused(`The Slopify host helper did not open this folder because Slopify sent a path it can't use. Update Slopify: ${again}.`);
|
|
74
|
+
let refusal;
|
|
75
|
+
for (const receipt of await receipts()) {
|
|
76
|
+
const r = relative(receipt.projects, path);
|
|
77
|
+
if (r === ".." || r.startsWith(`..${sep}`) || isAbsolute(r))
|
|
78
|
+
continue;
|
|
79
|
+
try {
|
|
80
|
+
await verify(receipt.projects, receipt.directoryIdentity, r);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (!(error instanceof HostFolderRefused))
|
|
84
|
+
throw error;
|
|
85
|
+
refusal = error;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Open the normalised path that was checked, never the caller's spelling of it.
|
|
89
|
+
await deps.launch(join(receipt.projects, r));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
throw (refusal ??
|
|
93
|
+
new HostFolderRefused(`The Slopify host helper did not open ${path} because it isn't inside a project folder set up by the Docker launcher. Copy the path into your file manager, or if you moved your project folder, ${again}.`));
|
|
94
|
+
};
|
|
95
|
+
}
|
package/dist/host-cli/runtime.js
CHANGED
|
@@ -9,6 +9,7 @@ import { nodeCodexModels } from "../adapters/llm/codex-models.js";
|
|
|
9
9
|
import { geminiLlm } from "../adapters/llm/gemini.js";
|
|
10
10
|
import { nodeGeminiModels } from "../adapters/llm/gemini-models.js";
|
|
11
11
|
import { providerError } from "../kernel/ports/model.js";
|
|
12
|
+
import { createHostFolderOpener } from "./open-folder.js";
|
|
12
13
|
import { createHostStatus } from "./status.js";
|
|
13
14
|
export function createHostRuntime(deps) {
|
|
14
15
|
const status = createHostStatus(deps);
|
|
@@ -32,6 +33,7 @@ export function createHostRuntime(deps) {
|
|
|
32
33
|
};
|
|
33
34
|
return {
|
|
34
35
|
status,
|
|
36
|
+
...(deps.folders === undefined ? {} : { openFolder: createHostFolderOpener(deps.folders) }),
|
|
35
37
|
llm: (id) => ({
|
|
36
38
|
id,
|
|
37
39
|
capabilities: { streams: true, reportsUsage: true, webSearch: true },
|
|
@@ -20,9 +20,24 @@ export function migrate(db, clock) {
|
|
|
20
20
|
if (version === undefined || applied.has(version)) {
|
|
21
21
|
continue;
|
|
22
22
|
}
|
|
23
|
+
const sql = readFileSync(new URL(file, migrationsDir), "utf8");
|
|
24
|
+
// A migration that rebuilds a table other tables reference must run with enforcement
|
|
25
|
+
// off: dropping the old table would otherwise cascade into every row that points at it.
|
|
26
|
+
// SQLite ignores this pragma inside a transaction, so it is set around one, and every
|
|
27
|
+
// reference is checked before the rebuild commits.
|
|
28
|
+
const rebuild = sql.split("\n", 1)[0]?.trim() === foreignKeysOff;
|
|
29
|
+
const enforced = rebuild && foreignKeysOn(db);
|
|
30
|
+
if (enforced)
|
|
31
|
+
db.exec("PRAGMA foreign_keys = OFF");
|
|
23
32
|
db.exec("BEGIN");
|
|
24
33
|
try {
|
|
25
|
-
db.exec(
|
|
34
|
+
db.exec(sql);
|
|
35
|
+
if (rebuild) {
|
|
36
|
+
const broken = db.prepare("PRAGMA foreign_key_check").all();
|
|
37
|
+
if (broken.length > 0) {
|
|
38
|
+
throw new Error(`Slopify couldn't upgrade its database: migration ${file} would leave ${broken.length} broken links between saved records, so it was undone and your data was not changed. Go back to the previous version of Slopify (npx @gentbajko/slopify@<previous version>) and report this on GitHub with the Download diagnostics file from Settings.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
26
41
|
db.prepare("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)").run(version, clock.now().toISOString());
|
|
27
42
|
db.exec("COMMIT");
|
|
28
43
|
}
|
|
@@ -30,8 +45,17 @@ export function migrate(db, clock) {
|
|
|
30
45
|
db.exec("ROLLBACK");
|
|
31
46
|
throw error;
|
|
32
47
|
}
|
|
48
|
+
finally {
|
|
49
|
+
if (enforced)
|
|
50
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
51
|
+
}
|
|
33
52
|
}
|
|
34
53
|
}
|
|
54
|
+
// The first line of such a migration, compared without its line ending.
|
|
55
|
+
const foreignKeysOff = "-- foreign-keys: off";
|
|
56
|
+
function foreignKeysOn(db) {
|
|
57
|
+
return db.prepare("PRAGMA foreign_keys").get()?.foreign_keys === 1;
|
|
58
|
+
}
|
|
35
59
|
function versionOf(file) {
|
|
36
60
|
const version = Number(file.slice(0, 4));
|
|
37
61
|
if (!Number.isInteger(version) || version < 1) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
-- foreign-keys: off
|
|
2
|
+
-- SQLite cannot widen a CHECK constraint in place, so the two tables that list the stage
|
|
3
|
+
-- kinds are rebuilt. `stages` is referenced by attempts, stage_pieces and revision_work with
|
|
4
|
+
-- ON DELETE CASCADE: migrate.ts turns enforcement off for this file, or dropping the old
|
|
5
|
+
-- table would empty those three, and checks every reference before it commits.
|
|
6
|
+
CREATE TABLE stages_document_upgrade (id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK(kind IN ('research','article','audio','images','thumbnail','video','document')), source TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('pending','running','done','failed','canceled','provided','skipped')), failure_reason TEXT, attempt_count INTEGER NOT NULL DEFAULT 0, progress_current INTEGER, progress_total INTEGER, started_at TEXT, finished_at TEXT, UNIQUE(project_id, kind));
|
|
7
|
+
INSERT INTO stages_document_upgrade (id,project_id,kind,source,state,failure_reason,attempt_count,progress_current,progress_total,started_at,finished_at)
|
|
8
|
+
SELECT id,project_id,kind,source,state,failure_reason,attempt_count,progress_current,progress_total,started_at,finished_at FROM stages;
|
|
9
|
+
DROP TABLE stages;
|
|
10
|
+
ALTER TABLE stages_document_upgrade RENAME TO stages;
|
|
11
|
+
CREATE UNIQUE INDEX stages_project_identity ON stages(project_id,id,kind);
|
|
12
|
+
|
|
13
|
+
-- Every existing project gets the new stage switched off, so nothing it already finished
|
|
14
|
+
-- shows as waiting.
|
|
15
|
+
INSERT INTO stages (id,project_id,kind,source,state)
|
|
16
|
+
SELECT id || '-document', id, 'document', 'off', 'skipped' FROM projects;
|
|
17
|
+
|
|
18
|
+
CREATE TABLE revision_pieces_document_upgrade (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
project_id TEXT NOT NULL,
|
|
21
|
+
revision_id TEXT NOT NULL,
|
|
22
|
+
piece_key TEXT NOT NULL,
|
|
23
|
+
stage_kind TEXT NOT NULL CHECK(stage_kind IN
|
|
24
|
+
('research','article','audio','images','thumbnail','video','document')),
|
|
25
|
+
asset_id TEXT,
|
|
26
|
+
fingerprint TEXT NOT NULL,
|
|
27
|
+
descriptor TEXT NOT NULL CHECK(json_valid(descriptor)),
|
|
28
|
+
publication_id TEXT,
|
|
29
|
+
selected INTEGER NOT NULL CHECK(selected IN (0,1)),
|
|
30
|
+
created_at TEXT NOT NULL,
|
|
31
|
+
FOREIGN KEY(project_id, revision_id)
|
|
32
|
+
REFERENCES project_revisions(project_id, id) ON DELETE CASCADE,
|
|
33
|
+
FOREIGN KEY(project_id, asset_id)
|
|
34
|
+
REFERENCES project_assets(project_id, id) ON DELETE CASCADE
|
|
35
|
+
);
|
|
36
|
+
INSERT INTO revision_pieces_document_upgrade (id,project_id,revision_id,piece_key,stage_kind,asset_id,fingerprint,descriptor,publication_id,selected,created_at)
|
|
37
|
+
SELECT id,project_id,revision_id,piece_key,stage_kind,asset_id,fingerprint,descriptor,publication_id,selected,created_at FROM revision_pieces;
|
|
38
|
+
DROP TABLE revision_pieces;
|
|
39
|
+
ALTER TABLE revision_pieces_document_upgrade RENAME TO revision_pieces;
|
|
40
|
+
CREATE UNIQUE INDEX revision_pieces_selected ON revision_pieces(revision_id, piece_key)
|
|
41
|
+
WHERE selected = 1;
|
|
42
|
+
CREATE INDEX revision_pieces_revision ON revision_pieces(revision_id, created_at, id);
|
|
43
|
+
CREATE UNIQUE INDEX revision_pieces_publication ON revision_pieces(revision_id, publication_id, piece_key)
|
|
44
|
+
WHERE publication_id IS NOT NULL;
|