runline 0.21.0 → 0.22.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/plugins/_shared/shiftUpload.js +118 -0
- package/dist/plugins/shiftAtlas/src/graph.js +2 -7
- package/dist/plugins/shiftAtlas/src/shared.js +1 -1
- package/dist/plugins/shiftObjects/src/index.js +20 -0
- package/dist/plugins/{shiftLabs → shiftObjects}/src/objects.js +11 -74
- package/dist/plugins/shiftOcr/src/index.js +49 -25
- package/dist/plugins/shiftPages/src/index.js +19 -0
- package/dist/plugins/{shiftLabs → shiftPages}/src/pages.js +2 -8
- package/dist/plugins/shiftPages/src/shared.js +7 -0
- package/dist/plugins/shiftTranscription/src/index.js +20 -0
- package/dist/plugins/shiftTranscription/src/shared.js +3 -0
- package/dist/plugins/{shiftLabs → shiftTranscription}/src/transcription.js +5 -63
- package/dist/plugins/{shiftLabs → shiftWork}/src/index.js +8 -10
- package/dist/plugins/{shiftLabs → shiftWork}/src/issue-views.js +1 -13
- package/dist/plugins/{shiftLabs → shiftWork}/src/issues.js +5 -17
- package/dist/plugins/{shiftLabs → shiftWork}/src/projects.js +1 -9
- package/dist/plugins/{shiftLabs → shiftWork}/src/shared.js +1 -9
- package/package.json +1 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { openAsBlob } from "node:fs";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { extname } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* The Shift cloud signed-grant upload arc, shared by every plugin that
|
|
6
|
+
* moves local file bytes into the cloud (shiftTranscription assets,
|
|
7
|
+
* shiftObjects objects): stat + size-gate the file, create the server
|
|
8
|
+
* record with { contentType, sizeBytes }, PUT the bytes to the grant
|
|
9
|
+
* URL with the grant's headers, then POST /complete. The subtle parts
|
|
10
|
+
* live here once: the openAsBlob streaming fallback, grant-header
|
|
11
|
+
* merging, and the size ceilings.
|
|
12
|
+
*
|
|
13
|
+
* shiftOcr deliberately does not use this arc — it inlines small files
|
|
14
|
+
* as base64 data URLs instead of uploading through a grant.
|
|
15
|
+
*/
|
|
16
|
+
/** Mirrors the public service contracts' 5 GiB upload ceiling. */
|
|
17
|
+
export const SIGNED_UPLOAD_MAX_BYTES = 5 * 1024 * 1024 * 1024;
|
|
18
|
+
/**
|
|
19
|
+
* Runtimes without fs.openAsBlob fall back to buffering; refuse to
|
|
20
|
+
* buffer files that would strain the host process.
|
|
21
|
+
*/
|
|
22
|
+
const MAX_BUFFERED_BYTES = 512 * 1024 * 1024;
|
|
23
|
+
/** Audio and video formats the transcription service accepts. */
|
|
24
|
+
export const AUDIO_VIDEO_MEDIA_TYPES = {
|
|
25
|
+
aac: "audio/aac",
|
|
26
|
+
flac: "audio/flac",
|
|
27
|
+
m4a: "audio/m4a",
|
|
28
|
+
mov: "video/quicktime",
|
|
29
|
+
mp3: "audio/mpeg",
|
|
30
|
+
mp4: "video/mp4",
|
|
31
|
+
mpeg: "video/mpeg",
|
|
32
|
+
mpg: "video/mpeg",
|
|
33
|
+
oga: "audio/ogg",
|
|
34
|
+
ogg: "audio/ogg",
|
|
35
|
+
opus: "audio/ogg",
|
|
36
|
+
wav: "audio/wav",
|
|
37
|
+
wave: "audio/wav",
|
|
38
|
+
webm: "video/webm",
|
|
39
|
+
};
|
|
40
|
+
/** Common document, image, and media formats for durable objects. */
|
|
41
|
+
export const GENERAL_MEDIA_TYPES = {
|
|
42
|
+
csv: "text/csv",
|
|
43
|
+
doc: "application/msword",
|
|
44
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
45
|
+
gif: "image/gif",
|
|
46
|
+
html: "text/html",
|
|
47
|
+
jpeg: "image/jpeg",
|
|
48
|
+
jpg: "image/jpeg",
|
|
49
|
+
json: "application/json",
|
|
50
|
+
md: "text/markdown",
|
|
51
|
+
mp3: "audio/mpeg",
|
|
52
|
+
mp4: "video/mp4",
|
|
53
|
+
pdf: "application/pdf",
|
|
54
|
+
png: "image/png",
|
|
55
|
+
txt: "text/plain",
|
|
56
|
+
wav: "audio/wav",
|
|
57
|
+
webp: "image/webp",
|
|
58
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
59
|
+
zip: "application/zip",
|
|
60
|
+
};
|
|
61
|
+
/** Extension-based media type lookup; undefined when unknown. */
|
|
62
|
+
export function mediaTypeFromPath(path, types) {
|
|
63
|
+
return types[extname(path).slice(1).toLowerCase()];
|
|
64
|
+
}
|
|
65
|
+
/** Like {@link mediaTypeFromPath}, but unknown extensions are an error. */
|
|
66
|
+
export function requireMediaType(path, types) {
|
|
67
|
+
const mediaType = mediaTypeFromPath(path, types);
|
|
68
|
+
if (!mediaType) {
|
|
69
|
+
throw new Error(`Unsupported media extension for ${path}. Supported: ${Object.keys(types).join(", ")}`);
|
|
70
|
+
}
|
|
71
|
+
return mediaType;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Gate a local file before any network call: it must exist, be a real
|
|
75
|
+
* file, be non-empty, and fit the service ceiling. Returns its size.
|
|
76
|
+
* The label keeps each plugin's established error vocabulary
|
|
77
|
+
* ("File is empty" vs "Media file is empty").
|
|
78
|
+
*/
|
|
79
|
+
export async function statUploadFile(path, maxBytes, label = "File") {
|
|
80
|
+
const file = await stat(path);
|
|
81
|
+
if (!file.isFile())
|
|
82
|
+
throw new Error(`${path} is not a file`);
|
|
83
|
+
if (file.size === 0)
|
|
84
|
+
throw new Error(`${label} is empty`);
|
|
85
|
+
if (file.size > maxBytes) {
|
|
86
|
+
throw new Error(`${label} is larger than the ${maxBytes}-byte service limit`);
|
|
87
|
+
}
|
|
88
|
+
return file.size;
|
|
89
|
+
}
|
|
90
|
+
async function fileBody(path, sizeBytes) {
|
|
91
|
+
try {
|
|
92
|
+
return await openAsBlob(path);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
if (sizeBytes > MAX_BUFFERED_BYTES) {
|
|
96
|
+
throw new Error("This runtime cannot stream uploads and the file is too large to buffer.");
|
|
97
|
+
}
|
|
98
|
+
return await readFile(path);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* PUT a local file's bytes to a signed grant. Grant headers win;
|
|
103
|
+
* content-type is filled in only when the grant leaves it unset.
|
|
104
|
+
*/
|
|
105
|
+
export async function putThroughGrant(grant, path, contentType, sizeBytes, label = "Upload") {
|
|
106
|
+
const headers = new Headers(grant.headers);
|
|
107
|
+
if (!headers.has("content-type")) {
|
|
108
|
+
headers.set("content-type", contentType);
|
|
109
|
+
}
|
|
110
|
+
const response = await fetch(grant.url, {
|
|
111
|
+
method: grant.method,
|
|
112
|
+
headers,
|
|
113
|
+
body: await fileBody(path, sizeBytes),
|
|
114
|
+
});
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
throw new Error(`${label} failed: HTTP ${response.status}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { ATLAS_BASE, cursorSchema, enumSchema, GRAPH_ACTOR_KIND, GRAPH_ASSIGNMENT_ROLE, GRAPH_BINDING_EXECUTOR, GRAPH_BINDING_STATUS, GRAPH_CHANGE_ENTITY_TYPE, GRAPH_EVIDENCE_KIND, GRAPH_JOURNEY_STATUS, GRAPH_RELATION_KIND, GRAPH_TASK_MODE, idSchema, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, } from "./shared.js";
|
|
2
|
+
import { ATLAS_BASE, cursorSchema, enumSchema, GRAPH_ACTOR_KIND, GRAPH_ASSIGNMENT_ROLE, GRAPH_BINDING_EXECUTOR, GRAPH_BINDING_STATUS, GRAPH_CHANGE_ENTITY_TYPE, GRAPH_EVIDENCE_KIND, GRAPH_JOURNEY_STATUS, GRAPH_RELATION_KIND, GRAPH_TASK_MODE, idSchema, listParams, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, withQuery, } from "./shared.js";
|
|
3
3
|
/**
|
|
4
4
|
* The operational graph behind the Atlas view: a business spec of how
|
|
5
5
|
* a company operates, in its own language. Four fixed levels — line →
|
|
@@ -121,12 +121,7 @@ export function registerGraphActions(rl) {
|
|
|
121
121
|
limit: t.Optional(t.Integer({ minimum: 1, maximum: 200 })),
|
|
122
122
|
}, STRICT_OBJECT),
|
|
123
123
|
async execute(input, ctx) {
|
|
124
|
-
|
|
125
|
-
for (const [key, value] of Object.entries(input)) {
|
|
126
|
-
if (value !== undefined)
|
|
127
|
-
params.set(key, String(value));
|
|
128
|
-
}
|
|
129
|
-
return request(ctx, `${ATLAS_BASE}/changes?${params}`);
|
|
124
|
+
return request(ctx, withQuery(`${ATLAS_BASE}/changes`, listParams(input)));
|
|
130
125
|
},
|
|
131
126
|
});
|
|
132
127
|
/* -------------------------------------------------------------- */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { cursorSchema, enumSchema, idSchema, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, } from "../../_shared/shiftCloud.js";
|
|
1
|
+
export { cursorSchema, enumSchema, idSchema, listParams, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, withQuery, } from "../../_shared/shiftCloud.js";
|
|
2
2
|
/** Base path of the operational graph service in the Shift cloud API. */
|
|
3
3
|
export const ATLAS_BASE = "/v1/services/operational-graph";
|
|
4
4
|
export const GRAPH_ACTOR_KIND = ["agent", "human_role"];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { registerObjectActions } from "./objects.js";
|
|
3
|
+
/**
|
|
4
|
+
* Shift Objects — the organization's durable object bucket in the
|
|
5
|
+
* Shift cloud: upload binary files through signed grants, mint
|
|
6
|
+
* download grants, and record append-only provenance links. The API
|
|
7
|
+
* key is the tenant authority; the cloud derives the organization from
|
|
8
|
+
* it (SHFT-852), so no org ID is sent.
|
|
9
|
+
*/
|
|
10
|
+
export default function shiftObjects(rl) {
|
|
11
|
+
rl.setName("shiftObjects");
|
|
12
|
+
rl.setVersion("0.1.0");
|
|
13
|
+
rl.setConnectionSchema(t.Object({
|
|
14
|
+
apiKey: t.String({
|
|
15
|
+
description: "Shift Labs API key",
|
|
16
|
+
env: "SHIFT_LABS_API_KEY",
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
registerObjectActions(rl);
|
|
20
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { basename, extname } from "node:path";
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename } from "node:path";
|
|
4
3
|
import * as t from "typebox";
|
|
5
|
-
import { enumSchema, pathSegment, request } from "
|
|
4
|
+
import { enumSchema, listParams, pathSegment, request, withQuery, } from "../../_shared/shiftCloud.js";
|
|
5
|
+
import { GENERAL_MEDIA_TYPES, mediaTypeFromPath, putThroughGrant, SIGNED_UPLOAD_MAX_BYTES, statUploadFile, } from "../../_shared/shiftUpload.js";
|
|
6
6
|
const OBJECTS_BASE = "/v1/services/objects";
|
|
7
7
|
export const OBJECT_STATUS = ["pending_upload", "ready", "archived"];
|
|
8
8
|
export const OBJECT_LINK_TARGET = [
|
|
@@ -11,49 +11,8 @@ export const OBJECT_LINK_TARGET = [
|
|
|
11
11
|
"eval_case",
|
|
12
12
|
"db_record",
|
|
13
13
|
];
|
|
14
|
-
/** Mirrors the public service contract's 5 GiB object ceiling. */
|
|
15
|
-
const MAX_OBJECT_BYTES = 5 * 1024 * 1024 * 1024;
|
|
16
14
|
const STRICT_OBJECT = { additionalProperties: false };
|
|
17
15
|
const Id = t.String({ minLength: 1, pattern: "\\S" });
|
|
18
|
-
const MEDIA_TYPES = {
|
|
19
|
-
csv: "text/csv",
|
|
20
|
-
doc: "application/msword",
|
|
21
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
22
|
-
gif: "image/gif",
|
|
23
|
-
html: "text/html",
|
|
24
|
-
jpeg: "image/jpeg",
|
|
25
|
-
jpg: "image/jpeg",
|
|
26
|
-
json: "application/json",
|
|
27
|
-
md: "text/markdown",
|
|
28
|
-
mp3: "audio/mpeg",
|
|
29
|
-
mp4: "video/mp4",
|
|
30
|
-
pdf: "application/pdf",
|
|
31
|
-
png: "image/png",
|
|
32
|
-
txt: "text/plain",
|
|
33
|
-
wav: "audio/wav",
|
|
34
|
-
webp: "image/webp",
|
|
35
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
36
|
-
zip: "application/zip",
|
|
37
|
-
};
|
|
38
|
-
function mediaTypeFor(path, override) {
|
|
39
|
-
if (override)
|
|
40
|
-
return override;
|
|
41
|
-
return (MEDIA_TYPES[extname(path).slice(1).toLowerCase()] ??
|
|
42
|
-
"application/octet-stream");
|
|
43
|
-
}
|
|
44
|
-
async function fileBody(path, sizeBytes) {
|
|
45
|
-
try {
|
|
46
|
-
return await openAsBlob(path);
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
// Runtimes without fs.openAsBlob fall back to buffering; refuse to
|
|
50
|
-
// buffer files that would strain the host process.
|
|
51
|
-
if (sizeBytes > 512 * 1024 * 1024) {
|
|
52
|
-
throw new Error("This runtime cannot stream uploads and the file is too large to buffer.");
|
|
53
|
-
}
|
|
54
|
-
return await readFile(path);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
16
|
function linkSchema() {
|
|
58
17
|
return t.Object({
|
|
59
18
|
targetType: enumSchema("Link target type", OBJECT_LINK_TARGET),
|
|
@@ -108,21 +67,16 @@ export function registerObjectActions(rl) {
|
|
|
108
67
|
}, STRICT_OBJECT),
|
|
109
68
|
async execute(input, ctx) {
|
|
110
69
|
const fields = input;
|
|
111
|
-
const contentType =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (file.size === 0)
|
|
116
|
-
throw new Error("File is empty");
|
|
117
|
-
if (file.size > MAX_OBJECT_BYTES) {
|
|
118
|
-
throw new Error(`File is larger than the ${MAX_OBJECT_BYTES}-byte service limit`);
|
|
119
|
-
}
|
|
70
|
+
const contentType = fields.contentType ??
|
|
71
|
+
mediaTypeFromPath(fields.path, GENERAL_MEDIA_TYPES) ??
|
|
72
|
+
"application/octet-stream";
|
|
73
|
+
const sizeBytes = await statUploadFile(fields.path, SIGNED_UPLOAD_MAX_BYTES);
|
|
120
74
|
const created = await request(ctx, `${OBJECTS_BASE}/objects`, {
|
|
121
75
|
method: "POST",
|
|
122
76
|
// The API key is the tenant authority; no organizationId is sent.
|
|
123
77
|
body: JSON.stringify({
|
|
124
78
|
contentType,
|
|
125
|
-
sizeBytes
|
|
79
|
+
sizeBytes,
|
|
126
80
|
filename: fields.filename ?? basename(fields.path),
|
|
127
81
|
workspaceId: fields.workspaceId,
|
|
128
82
|
sessionId: fields.sessionId,
|
|
@@ -130,18 +84,7 @@ export function registerObjectActions(rl) {
|
|
|
130
84
|
metadata: fields.metadata,
|
|
131
85
|
}),
|
|
132
86
|
});
|
|
133
|
-
|
|
134
|
-
if (!uploadHeaders.has("content-type")) {
|
|
135
|
-
uploadHeaders.set("content-type", contentType);
|
|
136
|
-
}
|
|
137
|
-
const upload = await fetch(created.upload.url, {
|
|
138
|
-
method: created.upload.method,
|
|
139
|
-
headers: uploadHeaders,
|
|
140
|
-
body: await fileBody(fields.path, file.size),
|
|
141
|
-
});
|
|
142
|
-
if (!upload.ok) {
|
|
143
|
-
throw new Error(`Object upload failed: HTTP ${upload.status}`);
|
|
144
|
-
}
|
|
87
|
+
await putThroughGrant(created.upload, fields.path, contentType, sizeBytes, "Object upload");
|
|
145
88
|
return request(ctx, `${OBJECTS_BASE}/objects/${pathSegment(created.object.id)}/complete`, { method: "POST" });
|
|
146
89
|
},
|
|
147
90
|
});
|
|
@@ -155,13 +98,7 @@ export function registerObjectActions(rl) {
|
|
|
155
98
|
...provenanceFields,
|
|
156
99
|
}, STRICT_OBJECT),
|
|
157
100
|
async execute(input, ctx) {
|
|
158
|
-
const
|
|
159
|
-
for (const [key, value] of Object.entries(input)) {
|
|
160
|
-
if (value !== undefined)
|
|
161
|
-
params.set(key, String(value));
|
|
162
|
-
}
|
|
163
|
-
const query = params.toString();
|
|
164
|
-
const body = await request(ctx, `${OBJECTS_BASE}/objects${query ? `?${query}` : ""}`);
|
|
101
|
+
const body = await request(ctx, withQuery(`${OBJECTS_BASE}/objects`, listParams(input)));
|
|
165
102
|
return body.objects;
|
|
166
103
|
},
|
|
167
104
|
});
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import { readFile
|
|
2
|
-
import { extname } from "node:path";
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, extname } from "node:path";
|
|
3
3
|
import * as t from "typebox";
|
|
4
4
|
import { request, STRICT_OBJECT } from "../../_shared/shiftCloud.js";
|
|
5
|
+
import { putThroughGrant, SIGNED_UPLOAD_MAX_BYTES, statUploadFile, } from "../../_shared/shiftUpload.js";
|
|
5
6
|
/**
|
|
6
7
|
* Shift OCR — text and structured-field extraction from images and
|
|
7
8
|
* PDFs through the Shift cloud OCR service. The API key is the tenant
|
|
8
9
|
* authority; no organization ID is ever sent (SHFT-852).
|
|
10
|
+
*
|
|
11
|
+
* Local files route by size: small ones inline as base64 data URLs
|
|
12
|
+
* (one round-trip); larger ones ride the signed-grant arc into the
|
|
13
|
+
* organization's durable object bucket and extract by objectId
|
|
14
|
+
* (SHFT-924), so bytes go to S3, never through the API.
|
|
9
15
|
*/
|
|
10
16
|
const IMAGE_TYPES = {
|
|
11
17
|
avif: "image/avif",
|
|
@@ -20,10 +26,11 @@ const DOCUMENT_TYPES = {
|
|
|
20
26
|
};
|
|
21
27
|
/**
|
|
22
28
|
* Raw-file ceiling for inlining as a base64 data URL. Base64 inflates
|
|
23
|
-
* by 4/3, so 20 MiB raw stays well inside provider request limits
|
|
29
|
+
* by 4/3, so 20 MiB raw stays well inside provider request limits;
|
|
30
|
+
* anything larger uploads to the object bucket instead.
|
|
24
31
|
*/
|
|
25
|
-
const
|
|
26
|
-
async function documentFromPath(path) {
|
|
32
|
+
const MAX_INLINE_BYTES = 20 * 1024 * 1024;
|
|
33
|
+
async function documentFromPath(ctx, path) {
|
|
27
34
|
const extension = extname(path).slice(1).toLowerCase();
|
|
28
35
|
const imageType = IMAGE_TYPES[extension];
|
|
29
36
|
const documentType = DOCUMENT_TYPES[extension];
|
|
@@ -33,19 +40,27 @@ async function documentFromPath(path) {
|
|
|
33
40
|
...Object.keys(DOCUMENT_TYPES),
|
|
34
41
|
].join(", ")}`);
|
|
35
42
|
}
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
const mediaType = (imageType ?? documentType);
|
|
44
|
+
const sizeBytes = await statUploadFile(path, SIGNED_UPLOAD_MAX_BYTES);
|
|
45
|
+
if (sizeBytes <= MAX_INLINE_BYTES) {
|
|
46
|
+
const url = `data:${mediaType};base64,${(await readFile(path)).toString("base64")}`;
|
|
47
|
+
return imageType
|
|
48
|
+
? { type: "image", url }
|
|
49
|
+
: { type: "document", url, name: basename(path) };
|
|
43
50
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
:
|
|
51
|
+
// Large file: into the durable bucket through a signed grant, then
|
|
52
|
+
// extract by reference — the object persists as provenance.
|
|
53
|
+
const created = await request(ctx, "/v1/services/objects/objects", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
contentType: mediaType,
|
|
57
|
+
sizeBytes,
|
|
58
|
+
filename: basename(path),
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
await putThroughGrant(created.upload, path, mediaType, sizeBytes);
|
|
62
|
+
await request(ctx, `/v1/services/objects/objects/${created.object.id}/complete`, { method: "POST" });
|
|
63
|
+
return { type: "object", objectId: created.object.id };
|
|
49
64
|
}
|
|
50
65
|
function documentFromUrl(url, kind) {
|
|
51
66
|
if (kind)
|
|
@@ -75,9 +90,10 @@ export default function shiftOcr(rl) {
|
|
|
75
90
|
rl.registerAction("ocr.extract", {
|
|
76
91
|
access: "write",
|
|
77
92
|
description: "Extract markdown text — and optionally schema-constrained JSON — " +
|
|
78
|
-
"from an image or PDF. Pass a local file path (
|
|
79
|
-
"
|
|
80
|
-
"
|
|
93
|
+
"from an image or PDF. Pass a local file path (small files inline; " +
|
|
94
|
+
"large ones upload to the org's object bucket automatically), an " +
|
|
95
|
+
"https URL, or the objectId of a stored object. Provide schema to " +
|
|
96
|
+
"get structured fields back instead of parsing the text yourself.",
|
|
81
97
|
inputSchema: t.Object({
|
|
82
98
|
path: t.Optional(t.String({
|
|
83
99
|
minLength: 1,
|
|
@@ -89,6 +105,11 @@ export default function shiftOcr(rl) {
|
|
|
89
105
|
pattern: "\\S",
|
|
90
106
|
description: "https or data URL of the image or PDF",
|
|
91
107
|
})),
|
|
108
|
+
objectId: t.Optional(t.String({
|
|
109
|
+
minLength: 1,
|
|
110
|
+
pattern: "\\S",
|
|
111
|
+
description: "ID of a ready object in the organization's durable bucket",
|
|
112
|
+
})),
|
|
92
113
|
kind: t.Optional(t.Union([t.Literal("image"), t.Literal("document")], {
|
|
93
114
|
description: "Override for URLs whose extension does not reveal the type. " +
|
|
94
115
|
"Inferred from the extension otherwise.",
|
|
@@ -114,15 +135,18 @@ export default function shiftOcr(rl) {
|
|
|
114
135
|
}, STRICT_OBJECT),
|
|
115
136
|
async execute(input, ctx) {
|
|
116
137
|
const fields = input;
|
|
117
|
-
|
|
118
|
-
|
|
138
|
+
const sources = [fields.path, fields.url, fields.objectId].filter((value) => value !== undefined);
|
|
139
|
+
if (sources.length !== 1) {
|
|
140
|
+
throw new Error("Provide exactly one of path, url, or objectId");
|
|
119
141
|
}
|
|
120
142
|
if (fields.prompt && !fields.schema) {
|
|
121
143
|
throw new Error("prompt requires schema");
|
|
122
144
|
}
|
|
123
|
-
const document = fields.
|
|
124
|
-
?
|
|
125
|
-
:
|
|
145
|
+
const document = fields.objectId
|
|
146
|
+
? { type: "object", objectId: fields.objectId }
|
|
147
|
+
: fields.path
|
|
148
|
+
? await documentFromPath(ctx, fields.path)
|
|
149
|
+
: documentFromUrl(fields.url, fields.kind);
|
|
126
150
|
return await request(ctx, "/v1/services/ocr/extract", {
|
|
127
151
|
method: "POST",
|
|
128
152
|
body: JSON.stringify({
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { registerPageActions } from "./pages.js";
|
|
3
|
+
/**
|
|
4
|
+
* Shift Pages — hosted HTML pages in the Shift cloud: draft, publish,
|
|
5
|
+
* archive, share with invited viewers, and resolve authenticated
|
|
6
|
+
* render URLs. The API key is the tenant authority; the cloud derives
|
|
7
|
+
* the organization from it (SHFT-852), so no org ID is sent.
|
|
8
|
+
*/
|
|
9
|
+
export default function shiftPages(rl) {
|
|
10
|
+
rl.setName("shiftPages");
|
|
11
|
+
rl.setVersion("0.1.0");
|
|
12
|
+
rl.setConnectionSchema(t.Object({
|
|
13
|
+
apiKey: t.String({
|
|
14
|
+
description: "Shift Labs API key",
|
|
15
|
+
env: "SHIFT_LABS_API_KEY",
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
registerPageActions(rl);
|
|
19
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { enumSchema, idSchema, PAGE_STATUS, PAGE_VISIBILITY, pageRenderUrl, pathSegment, request, STRICT_OBJECT, timestampSchema, } from "./shared.js";
|
|
2
|
+
import { enumSchema, idSchema, listParams, PAGE_STATUS, PAGE_VISIBILITY, pageRenderUrl, pathSegment, request, STRICT_OBJECT, timestampSchema, withQuery, } from "./shared.js";
|
|
3
3
|
const PageSlug = t.String({
|
|
4
4
|
minLength: 1,
|
|
5
5
|
maxLength: 120,
|
|
@@ -19,13 +19,7 @@ export function registerPageActions(rl) {
|
|
|
19
19
|
})),
|
|
20
20
|
}, STRICT_OBJECT),
|
|
21
21
|
async execute(input, ctx) {
|
|
22
|
-
const
|
|
23
|
-
const params = new URLSearchParams();
|
|
24
|
-
if (fields.status)
|
|
25
|
-
params.set("status", String(fields.status));
|
|
26
|
-
if (fields.limit)
|
|
27
|
-
params.set("limit", String(fields.limit));
|
|
28
|
-
const body = await request(ctx, `/v1/pages?${params}`);
|
|
22
|
+
const body = await request(ctx, withQuery("/v1/pages", listParams(input)));
|
|
29
23
|
return body.pages;
|
|
30
24
|
},
|
|
31
25
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { baseUrl, pathSegment } from "../../_shared/shiftCloud.js";
|
|
2
|
+
export { enumSchema, idSchema, listParams, pathSegment, request, STRICT_OBJECT, timestampSchema, withQuery, } from "../../_shared/shiftCloud.js";
|
|
3
|
+
export const PAGE_STATUS = ["draft", "published", "archived"];
|
|
4
|
+
export const PAGE_VISIBILITY = ["org", "invited"];
|
|
5
|
+
export function pageRenderUrl(organizationId, slug) {
|
|
6
|
+
return new URL(`/pages/${pathSegment(organizationId)}/${pathSegment(slug)}`, baseUrl()).toString();
|
|
7
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { registerTranscriptionActions } from "./transcription.js";
|
|
3
|
+
/**
|
|
4
|
+
* Shift Transcription — audio and video transcription through the
|
|
5
|
+
* Shift cloud: upload media, run jobs (optionally diarized), and read
|
|
6
|
+
* transcripts as text, subtitles, or timestamped JSON. The API key is
|
|
7
|
+
* the tenant authority; the cloud derives the organization from it
|
|
8
|
+
* (SHFT-852), so no org ID is sent.
|
|
9
|
+
*/
|
|
10
|
+
export default function shiftTranscription(rl) {
|
|
11
|
+
rl.setName("shiftTranscription");
|
|
12
|
+
rl.setVersion("0.1.0");
|
|
13
|
+
rl.setConnectionSchema(t.Object({
|
|
14
|
+
apiKey: t.String({
|
|
15
|
+
description: "Shift Labs API key",
|
|
16
|
+
env: "SHIFT_LABS_API_KEY",
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
registerTranscriptionActions(rl);
|
|
20
|
+
}
|
|
@@ -1,50 +1,10 @@
|
|
|
1
|
-
import { openAsBlob } from "node:fs";
|
|
2
|
-
import { readFile, stat } from "node:fs/promises";
|
|
3
|
-
import { extname } from "node:path";
|
|
4
1
|
import * as t from "typebox";
|
|
2
|
+
import { AUDIO_VIDEO_MEDIA_TYPES, putThroughGrant, requireMediaType, SIGNED_UPLOAD_MAX_BYTES, statUploadFile, } from "../../_shared/shiftUpload.js";
|
|
5
3
|
import { enumSchema, pathSegment, request, TRANSCRIPT_FORMAT, TRANSCRIPTION_LANGUAGE, } from "./shared.js";
|
|
6
|
-
const MEDIA_TYPES = {
|
|
7
|
-
aac: "audio/aac",
|
|
8
|
-
flac: "audio/flac",
|
|
9
|
-
m4a: "audio/m4a",
|
|
10
|
-
mov: "video/quicktime",
|
|
11
|
-
mp3: "audio/mpeg",
|
|
12
|
-
mp4: "video/mp4",
|
|
13
|
-
mpeg: "video/mpeg",
|
|
14
|
-
mpg: "video/mpeg",
|
|
15
|
-
oga: "audio/ogg",
|
|
16
|
-
ogg: "audio/ogg",
|
|
17
|
-
opus: "audio/ogg",
|
|
18
|
-
wav: "audio/wav",
|
|
19
|
-
wave: "audio/wav",
|
|
20
|
-
webm: "video/webm",
|
|
21
|
-
};
|
|
22
|
-
/** Mirrors the public service contract's 5 GiB source-object ceiling. */
|
|
23
|
-
const MAX_MEDIA_BYTES = 5 * 1024 * 1024 * 1024;
|
|
24
4
|
/** Transcript previews returned into the sandbox are capped, not truncated silently. */
|
|
25
5
|
const MAX_TRANSCRIPT_BYTES = 2 * 1024 * 1024;
|
|
26
6
|
const STRICT_OBJECT = { additionalProperties: false };
|
|
27
7
|
const Id = t.String({ minLength: 1, pattern: "\\S" });
|
|
28
|
-
function mediaTypeFor(path) {
|
|
29
|
-
const mediaType = MEDIA_TYPES[extname(path).slice(1).toLowerCase()];
|
|
30
|
-
if (!mediaType) {
|
|
31
|
-
throw new Error(`Unsupported media extension for ${path}. Supported: ${Object.keys(MEDIA_TYPES).join(", ")}`);
|
|
32
|
-
}
|
|
33
|
-
return mediaType;
|
|
34
|
-
}
|
|
35
|
-
async function fileBody(path, sizeBytes) {
|
|
36
|
-
try {
|
|
37
|
-
return await openAsBlob(path);
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
// Runtimes without fs.openAsBlob fall back to buffering; refuse to
|
|
41
|
-
// buffer files that would strain the host process.
|
|
42
|
-
if (sizeBytes > 512 * 1024 * 1024) {
|
|
43
|
-
throw new Error("This runtime cannot stream uploads and the file is too large to buffer.");
|
|
44
|
-
}
|
|
45
|
-
return await readFile(path);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
8
|
export function registerTranscriptionActions(rl) {
|
|
49
9
|
rl.registerAction("transcription.transcribe", {
|
|
50
10
|
access: "write",
|
|
@@ -76,35 +36,17 @@ export function registerTranscriptionActions(rl) {
|
|
|
76
36
|
}, STRICT_OBJECT),
|
|
77
37
|
async execute(input, ctx) {
|
|
78
38
|
const fields = input;
|
|
79
|
-
const contentType =
|
|
80
|
-
const
|
|
81
|
-
if (!file.isFile())
|
|
82
|
-
throw new Error(`${fields.path} is not a file`);
|
|
83
|
-
if (file.size === 0)
|
|
84
|
-
throw new Error("Media file is empty");
|
|
85
|
-
if (file.size > MAX_MEDIA_BYTES) {
|
|
86
|
-
throw new Error(`Media file is larger than the ${MAX_MEDIA_BYTES}-byte service limit`);
|
|
87
|
-
}
|
|
39
|
+
const contentType = requireMediaType(fields.path, AUDIO_VIDEO_MEDIA_TYPES);
|
|
40
|
+
const sizeBytes = await statUploadFile(fields.path, SIGNED_UPLOAD_MAX_BYTES, "Media file");
|
|
88
41
|
const created = await request(ctx, "/v1/services/transcription/assets", {
|
|
89
42
|
method: "POST",
|
|
90
43
|
// The API key is the tenant authority; no organizationId is sent.
|
|
91
44
|
body: JSON.stringify({
|
|
92
45
|
contentType,
|
|
93
|
-
sizeBytes
|
|
46
|
+
sizeBytes,
|
|
94
47
|
}),
|
|
95
48
|
});
|
|
96
|
-
|
|
97
|
-
if (!uploadHeaders.has("content-type")) {
|
|
98
|
-
uploadHeaders.set("content-type", contentType);
|
|
99
|
-
}
|
|
100
|
-
const upload = await fetch(created.upload.url, {
|
|
101
|
-
method: created.upload.method,
|
|
102
|
-
headers: uploadHeaders,
|
|
103
|
-
body: await fileBody(fields.path, file.size),
|
|
104
|
-
});
|
|
105
|
-
if (!upload.ok) {
|
|
106
|
-
throw new Error(`Media upload failed: HTTP ${upload.status}`);
|
|
107
|
-
}
|
|
49
|
+
await putThroughGrant(created.upload, fields.path, contentType, sizeBytes, "Media upload");
|
|
108
50
|
await request(ctx, `/v1/services/transcription/assets/${pathSegment(created.asset.id)}/complete`, { method: "POST" });
|
|
109
51
|
const job = await request(ctx, "/v1/services/transcription/jobs", {
|
|
110
52
|
method: "POST",
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
2
|
import { registerIssueViewActions } from "./issue-views.js";
|
|
3
3
|
import { registerIssueActions } from "./issues.js";
|
|
4
|
-
import { registerObjectActions } from "./objects.js";
|
|
5
|
-
import { registerPageActions } from "./pages.js";
|
|
6
4
|
import { registerProjectActions } from "./projects.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Shift Work — the work-tracking domain of the Shift cloud: Projects,
|
|
7
|
+
* Issues (with dependencies and comments), and saved Issue Views.
|
|
8
|
+
* The API key is the tenant authority for every action; the cloud
|
|
9
|
+
* derives the organization from it (SHFT-852), so no org ID is sent.
|
|
10
|
+
*/
|
|
11
|
+
export default function shiftWork(rl) {
|
|
12
|
+
rl.setName("shiftWork");
|
|
10
13
|
rl.setVersion("0.1.0");
|
|
11
14
|
rl.setConnectionSchema(t.Object({
|
|
12
|
-
// The API key is the tenant authority for every action; the cloud
|
|
13
|
-
// derives the organization from it (SHFT-852), so no org ID is needed.
|
|
14
15
|
apiKey: t.String({
|
|
15
16
|
description: "Shift Labs API key",
|
|
16
17
|
env: "SHIFT_LABS_API_KEY",
|
|
@@ -19,7 +20,4 @@ export default function shiftLabs(rl) {
|
|
|
19
20
|
registerProjectActions(rl);
|
|
20
21
|
registerIssueActions(rl);
|
|
21
22
|
registerIssueViewActions(rl);
|
|
22
|
-
registerPageActions(rl);
|
|
23
|
-
registerTranscriptionActions(rl);
|
|
24
|
-
registerObjectActions(rl);
|
|
25
23
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { cursorSchema, enumSchema, ISSUE_PRIORITY, ISSUE_STATUS, ISSUE_VIEW_GROUP, ISSUE_VIEW_LAYOUT, ISSUE_VIEW_SORT, ISSUE_VIEW_VISIBILITY, idSchema, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, } from "./shared.js";
|
|
2
|
+
import { cursorSchema, enumSchema, ISSUE_PRIORITY, ISSUE_STATUS, ISSUE_VIEW_GROUP, ISSUE_VIEW_LAYOUT, ISSUE_VIEW_SORT, ISSUE_VIEW_VISIBILITY, idSchema, listParams, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, withQuery, } from "./shared.js";
|
|
3
3
|
const viewFields = {
|
|
4
4
|
name: t.String({
|
|
5
5
|
minLength: 1,
|
|
@@ -143,18 +143,6 @@ export function registerIssueViewActions(rl) {
|
|
|
143
143
|
},
|
|
144
144
|
});
|
|
145
145
|
}
|
|
146
|
-
function listParams(input) {
|
|
147
|
-
const params = new URLSearchParams();
|
|
148
|
-
for (const [key, value] of Object.entries((input ?? {}))) {
|
|
149
|
-
if (value !== undefined)
|
|
150
|
-
params.set(key, String(value));
|
|
151
|
-
}
|
|
152
|
-
return params;
|
|
153
|
-
}
|
|
154
|
-
function withQuery(path, params) {
|
|
155
|
-
const query = params.toString();
|
|
156
|
-
return query ? `${path}?${query}` : path;
|
|
157
|
-
}
|
|
158
146
|
function assertDateOrder(startAfter, dueBefore) {
|
|
159
147
|
if (typeof startAfter === "string" &&
|
|
160
148
|
typeof dueBefore === "string" &&
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { cursorSchema, enumSchema, ISSUE_PRIORITY, ISSUE_SOURCE, ISSUE_STATUS, idSchema, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, } from "./shared.js";
|
|
2
|
+
import { cursorSchema, enumSchema, ISSUE_PRIORITY, ISSUE_SOURCE, ISSUE_STATUS, idSchema, listParams, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, withQuery, } from "./shared.js";
|
|
3
3
|
const issueListFields = {
|
|
4
4
|
status: t.Optional(enumSchema("Issue status", ISSUE_STATUS)),
|
|
5
5
|
projectId: t.Optional(idSchema("Project ID")),
|
|
@@ -14,25 +14,13 @@ const issueListFields = {
|
|
|
14
14
|
description: "Max results, default 50",
|
|
15
15
|
})),
|
|
16
16
|
};
|
|
17
|
-
function issueListParams(input) {
|
|
18
|
-
const params = new URLSearchParams();
|
|
19
|
-
for (const [key, value] of Object.entries((input ?? {}))) {
|
|
20
|
-
if (value !== undefined)
|
|
21
|
-
params.set(key, String(value));
|
|
22
|
-
}
|
|
23
|
-
return params;
|
|
24
|
-
}
|
|
25
|
-
function withQuery(path, params) {
|
|
26
|
-
const query = params.toString();
|
|
27
|
-
return query ? `${path}?${query}` : path;
|
|
28
|
-
}
|
|
29
17
|
export function registerIssueActions(rl) {
|
|
30
18
|
rl.registerAction("issue.list", {
|
|
31
19
|
access: "read",
|
|
32
20
|
description: "List the first page of Shift Labs Issues for the API key's organization.",
|
|
33
21
|
inputSchema: t.Object(issueListFields, STRICT_OBJECT),
|
|
34
22
|
async execute(input, ctx) {
|
|
35
|
-
const body = await request(ctx, `/v1/issues?${
|
|
23
|
+
const body = await request(ctx, `/v1/issues?${listParams(input)}`);
|
|
36
24
|
return body.issues;
|
|
37
25
|
},
|
|
38
26
|
});
|
|
@@ -41,7 +29,7 @@ export function registerIssueActions(rl) {
|
|
|
41
29
|
description: "List a cursor-paginated page of Shift Labs Issues.",
|
|
42
30
|
inputSchema: t.Object(issueListFields, STRICT_OBJECT),
|
|
43
31
|
async execute(input, ctx) {
|
|
44
|
-
return request(ctx, `/v1/issues?${
|
|
32
|
+
return request(ctx, `/v1/issues?${listParams(input)}`);
|
|
45
33
|
},
|
|
46
34
|
});
|
|
47
35
|
rl.registerAction("issue.get", {
|
|
@@ -148,7 +136,7 @@ export function registerIssueActions(rl) {
|
|
|
148
136
|
}, STRICT_OBJECT),
|
|
149
137
|
async execute(input, ctx) {
|
|
150
138
|
const { id, ...pagination } = input;
|
|
151
|
-
const body = await request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`,
|
|
139
|
+
const body = await request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`, listParams(pagination)));
|
|
152
140
|
return body.dependencies;
|
|
153
141
|
},
|
|
154
142
|
});
|
|
@@ -162,7 +150,7 @@ export function registerIssueActions(rl) {
|
|
|
162
150
|
}, STRICT_OBJECT),
|
|
163
151
|
async execute(input, ctx) {
|
|
164
152
|
const { id, ...pagination } = input;
|
|
165
|
-
return request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`,
|
|
153
|
+
return request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`, listParams(pagination)));
|
|
166
154
|
},
|
|
167
155
|
});
|
|
168
156
|
rl.registerAction("issue.dependency.add", {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { cursorSchema, enumSchema, idSchema, PROJECT_STATUS, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, } from "./shared.js";
|
|
2
|
+
import { cursorSchema, enumSchema, idSchema, listParams, PROJECT_STATUS, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, } from "./shared.js";
|
|
3
3
|
const projectListFields = {
|
|
4
4
|
status: t.Optional(enumSchema("Project status", PROJECT_STATUS)),
|
|
5
5
|
leadUserId: t.Optional(idSchema("Lead user ID")),
|
|
@@ -7,14 +7,6 @@ const projectListFields = {
|
|
|
7
7
|
cursor: t.Optional(cursorSchema()),
|
|
8
8
|
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
|
9
9
|
};
|
|
10
|
-
function listParams(input) {
|
|
11
|
-
const params = new URLSearchParams();
|
|
12
|
-
for (const [key, value] of Object.entries((input ?? {}))) {
|
|
13
|
-
if (value !== undefined)
|
|
14
|
-
params.set(key, String(value));
|
|
15
|
-
}
|
|
16
|
-
return params;
|
|
17
|
-
}
|
|
18
10
|
export function registerProjectActions(rl) {
|
|
19
11
|
rl.registerAction("project.list", {
|
|
20
12
|
access: "read",
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export { baseUrl, cursorSchema, enumDescription, enumSchema, idSchema, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, } from "../../_shared/shiftCloud.js";
|
|
3
|
-
export function pageRenderUrl(organizationId, slug) {
|
|
4
|
-
return new URL(`/pages/${pathSegment(organizationId)}/${pathSegment(slug)}`, baseUrl()).toString();
|
|
5
|
-
}
|
|
1
|
+
export { cursorSchema, enumDescription, enumSchema, idSchema, listParams, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, withQuery, } from "../../_shared/shiftCloud.js";
|
|
6
2
|
export const ISSUE_STATUS = [
|
|
7
3
|
"backlog",
|
|
8
4
|
"todo",
|
|
@@ -45,7 +41,3 @@ export const ISSUE_VIEW_SORT = [
|
|
|
45
41
|
"dueAt",
|
|
46
42
|
"updatedAt",
|
|
47
43
|
];
|
|
48
|
-
export const PAGE_STATUS = ["draft", "published", "archived"];
|
|
49
|
-
export const PAGE_VISIBILITY = ["org", "invited"];
|
|
50
|
-
export const TRANSCRIPTION_LANGUAGE = ["auto", "en", "he"];
|
|
51
|
-
export const TRANSCRIPT_FORMAT = ["txt", "srt", "vtt", "json"];
|