streamby-core 0.28.5 → 0.29.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/README.md +283 -283
- package/dist/adapters/database/connectionManager.js +63 -63
- package/dist/adapters/database/sql.js +1 -1
- package/dist/middleware/createRouter.js +21 -0
- package/dist/middleware/routes/ai.d.ts +3 -0
- package/dist/middleware/routes/ai.js +105 -0
- package/dist/middleware/routes/distribution.d.ts +3 -0
- package/dist/middleware/routes/distribution.js +192 -0
- package/dist/middleware/routes/media.d.ts +5 -0
- package/dist/middleware/routes/media.js +189 -0
- package/dist/middleware/routes/production.d.ts +3 -0
- package/dist/middleware/routes/production.js +362 -0
- package/dist/middleware/routes/renderFarm.d.ts +3 -0
- package/dist/middleware/routes/renderFarm.js +158 -0
- package/dist/middleware/routes/review.d.ts +3 -0
- package/dist/middleware/routes/review.js +240 -0
- package/dist/middleware/routes/rights.d.ts +3 -0
- package/dist/middleware/routes/rights.js +80 -0
- package/dist/models/Model.js +9 -9
- package/dist/services/aiProcessor.d.ts +25 -0
- package/dist/services/aiProcessor.js +123 -0
- package/dist/services/jobQueue.d.ts +5 -0
- package/dist/services/jobQueue.js +54 -0
- package/dist/services/mediaProcessor.d.ts +19 -0
- package/dist/services/mediaProcessor.js +201 -0
- package/dist/services/pipeline.js +153 -8
- package/dist/services/vfxProcessor.d.ts +24 -0
- package/dist/services/vfxProcessor.js +139 -0
- package/dist/types/index.d.ts +188 -0
- package/package.json +63 -63
- package/dist/utils/crypto.d.ts +0 -3
- package/dist/utils/crypto.js +0 -32
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { JobRecord, JobType } from '../types';
|
|
2
|
+
export declare function createJob(jobType: JobType, userId: string, projectId: string, payload: Record<string, any>): JobRecord;
|
|
3
|
+
export declare function getJob(jobId: string): JobRecord | undefined;
|
|
4
|
+
export declare function updateJob(jobId: string, updates: Partial<Pick<JobRecord, 'status' | 'stage' | 'progress' | 'assetId' | 'error'>>): void;
|
|
5
|
+
export declare function failJob(jobId: string, error: string): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createJob = createJob;
|
|
7
|
+
exports.getJob = getJob;
|
|
8
|
+
exports.updateJob = updateJob;
|
|
9
|
+
exports.failJob = failJob;
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
+
const wsHub_1 = require("./wsHub");
|
|
12
|
+
const jobs = new Map();
|
|
13
|
+
function createJob(jobType, userId, projectId, payload) {
|
|
14
|
+
const jobId = crypto_1.default.randomUUID();
|
|
15
|
+
const now = new Date();
|
|
16
|
+
const job = {
|
|
17
|
+
jobId,
|
|
18
|
+
jobType,
|
|
19
|
+
userId,
|
|
20
|
+
projectId,
|
|
21
|
+
status: 'pending',
|
|
22
|
+
stage: 'queued',
|
|
23
|
+
progress: 0,
|
|
24
|
+
payload,
|
|
25
|
+
createdAt: now,
|
|
26
|
+
updatedAt: now,
|
|
27
|
+
};
|
|
28
|
+
jobs.set(jobId, job);
|
|
29
|
+
return job;
|
|
30
|
+
}
|
|
31
|
+
function getJob(jobId) {
|
|
32
|
+
return jobs.get(jobId);
|
|
33
|
+
}
|
|
34
|
+
function updateJob(jobId, updates) {
|
|
35
|
+
const job = jobs.get(jobId);
|
|
36
|
+
if (!job)
|
|
37
|
+
return;
|
|
38
|
+
Object.assign(job, updates, { updatedAt: new Date() });
|
|
39
|
+
(0, wsHub_1.emitToUser)(job.userId, {
|
|
40
|
+
type: 'jobEvent',
|
|
41
|
+
data: {
|
|
42
|
+
jobId: job.jobId,
|
|
43
|
+
jobType: job.jobType,
|
|
44
|
+
assetId: job.assetId,
|
|
45
|
+
stage: job.stage,
|
|
46
|
+
progress: job.progress,
|
|
47
|
+
message: updates.stage,
|
|
48
|
+
error: job.error,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function failJob(jobId, error) {
|
|
53
|
+
updateJob(jobId, { status: 'failed', stage: 'failed', error });
|
|
54
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { StorageAdapter } from '../types';
|
|
2
|
+
export declare function runIngestJob(jobId: string, fileId: string, projectId: string): Promise<void>;
|
|
3
|
+
export declare function runTranscodeJob(jobId: string, fileId: string, projectId: string, options: {
|
|
4
|
+
codec?: string;
|
|
5
|
+
resolution?: string;
|
|
6
|
+
outputFormat?: string;
|
|
7
|
+
bitrate?: string;
|
|
8
|
+
audioCodec?: string;
|
|
9
|
+
}, _adapter: StorageAdapter): Promise<void>;
|
|
10
|
+
export declare function runThumbnailJob(jobId: string, fileId: string, projectId: string, options: {
|
|
11
|
+
timecode?: string;
|
|
12
|
+
resolution?: string;
|
|
13
|
+
strategy?: string;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
export declare function runCaptionJob(jobId: string, fileId: string, projectId: string, options: {
|
|
16
|
+
sourceLanguage?: string;
|
|
17
|
+
outputFormat?: string;
|
|
18
|
+
provider?: string;
|
|
19
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runIngestJob = runIngestJob;
|
|
4
|
+
exports.runTranscodeJob = runTranscodeJob;
|
|
5
|
+
exports.runThumbnailJob = runThumbnailJob;
|
|
6
|
+
exports.runCaptionJob = runCaptionJob;
|
|
7
|
+
const mongodb_1 = require("mongodb");
|
|
8
|
+
const connectionManager_1 = require("../adapters/database/connectionManager");
|
|
9
|
+
const jobQueue_1 = require("./jobQueue");
|
|
10
|
+
function getFilesCollection() {
|
|
11
|
+
const ids = (0, connectionManager_1.getConnectedIds)().filter(id => (0, connectionManager_1.getConnection)(id).type === 'nosql');
|
|
12
|
+
if (!ids.length)
|
|
13
|
+
return null;
|
|
14
|
+
return (0, connectionManager_1.getConnection)(ids[0]).client.db().collection('storage_files');
|
|
15
|
+
}
|
|
16
|
+
function getMetadataCollection() {
|
|
17
|
+
const ids = (0, connectionManager_1.getConnectedIds)().filter(id => (0, connectionManager_1.getConnection)(id).type === 'nosql');
|
|
18
|
+
if (!ids.length)
|
|
19
|
+
return null;
|
|
20
|
+
return (0, connectionManager_1.getConnection)(ids[0]).client.db().collection('media_metadata');
|
|
21
|
+
}
|
|
22
|
+
function getVersionsCollection() {
|
|
23
|
+
const ids = (0, connectionManager_1.getConnectedIds)().filter(id => (0, connectionManager_1.getConnection)(id).type === 'nosql');
|
|
24
|
+
if (!ids.length)
|
|
25
|
+
return null;
|
|
26
|
+
return (0, connectionManager_1.getConnection)(ids[0]).client.db().collection('asset_versions');
|
|
27
|
+
}
|
|
28
|
+
// Infer basic metadata from contentType without external binaries
|
|
29
|
+
function inferMetadata(contentType, size) {
|
|
30
|
+
const meta = {};
|
|
31
|
+
if (contentType.startsWith('image/')) {
|
|
32
|
+
meta.category = 'images';
|
|
33
|
+
}
|
|
34
|
+
else if (contentType.startsWith('audio/')) {
|
|
35
|
+
meta.category = 'audios';
|
|
36
|
+
}
|
|
37
|
+
else if (contentType.startsWith('video/')) {
|
|
38
|
+
meta.category = 'videos';
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
meta.category = '3d-models';
|
|
42
|
+
}
|
|
43
|
+
meta.size = size;
|
|
44
|
+
meta.codec = contentType.split('/')[1] ?? 'unknown';
|
|
45
|
+
return meta;
|
|
46
|
+
}
|
|
47
|
+
async function runIngestJob(jobId, fileId, projectId) {
|
|
48
|
+
(0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'resolving', progress: 10 });
|
|
49
|
+
try {
|
|
50
|
+
const filesCol = getFilesCollection();
|
|
51
|
+
if (!filesCol)
|
|
52
|
+
throw new Error('Storage database not available');
|
|
53
|
+
const fileDoc = await filesCol.findOne({ fileId, projectId });
|
|
54
|
+
if (!fileDoc)
|
|
55
|
+
throw new Error(`File ${fileId} not found in project ${projectId}`);
|
|
56
|
+
(0, jobQueue_1.updateJob)(jobId, { stage: 'extracting-metadata', progress: 40, assetId: fileId });
|
|
57
|
+
const metaCol = getMetadataCollection();
|
|
58
|
+
if (metaCol) {
|
|
59
|
+
const meta = inferMetadata(fileDoc.contentType ?? '', fileDoc.size ?? 0);
|
|
60
|
+
await metaCol.updateOne({ assetId: fileId, projectId }, {
|
|
61
|
+
$set: {
|
|
62
|
+
assetId: fileId,
|
|
63
|
+
projectId,
|
|
64
|
+
codec: meta.codec,
|
|
65
|
+
customTags: {},
|
|
66
|
+
extractedAt: new Date(),
|
|
67
|
+
},
|
|
68
|
+
}, { upsert: true });
|
|
69
|
+
}
|
|
70
|
+
(0, jobQueue_1.updateJob)(jobId, { stage: 'creating-version', progress: 70 });
|
|
71
|
+
const versionsCol = getVersionsCollection();
|
|
72
|
+
if (versionsCol) {
|
|
73
|
+
const existing = await versionsCol.countDocuments({ assetId: fileId, projectId });
|
|
74
|
+
await versionsCol.insertOne({
|
|
75
|
+
_id: new mongodb_1.ObjectId(),
|
|
76
|
+
versionId: crypto.randomUUID(),
|
|
77
|
+
assetId: fileId,
|
|
78
|
+
projectId,
|
|
79
|
+
version: existing + 1,
|
|
80
|
+
storageKey: fileDoc.storageKey,
|
|
81
|
+
size: fileDoc.size ?? 0,
|
|
82
|
+
createdBy: fileDoc.uploadedBy,
|
|
83
|
+
createdAt: new Date(),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
(0, jobQueue_1.updateJob)(jobId, { status: 'completed', stage: 'complete', progress: 100 });
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
(0, jobQueue_1.failJob)(jobId, err.message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function runTranscodeJob(jobId, fileId, projectId, options, _adapter) {
|
|
93
|
+
(0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'queued', progress: 5, assetId: fileId });
|
|
94
|
+
try {
|
|
95
|
+
const filesCol = getFilesCollection();
|
|
96
|
+
if (!filesCol)
|
|
97
|
+
throw new Error('Storage database not available');
|
|
98
|
+
const fileDoc = await filesCol.findOne({ fileId, projectId });
|
|
99
|
+
if (!fileDoc)
|
|
100
|
+
throw new Error(`File ${fileId} not found`);
|
|
101
|
+
if (!fileDoc.contentType?.startsWith('video/') && !fileDoc.contentType?.startsWith('audio/')) {
|
|
102
|
+
throw new Error('Transcode only supports video and audio files');
|
|
103
|
+
}
|
|
104
|
+
(0, jobQueue_1.updateJob)(jobId, { stage: 'transcoding', progress: 20 });
|
|
105
|
+
// Store the transcode request as a pending task — actual FFmpeg execution
|
|
106
|
+
// happens in the host environment (Nhexa-API worker). The job record is the
|
|
107
|
+
// handoff contract: the worker reads it, processes the file, and calls back
|
|
108
|
+
// to update progress via the job queue.
|
|
109
|
+
const metaCol = getMetadataCollection();
|
|
110
|
+
if (metaCol) {
|
|
111
|
+
await metaCol.updateOne({ assetId: fileId, projectId }, {
|
|
112
|
+
$set: {
|
|
113
|
+
assetId: fileId,
|
|
114
|
+
projectId,
|
|
115
|
+
pendingTranscode: {
|
|
116
|
+
codec: options.codec ?? 'h264',
|
|
117
|
+
resolution: options.resolution ?? 'original',
|
|
118
|
+
outputFormat: options.outputFormat ?? 'mp4',
|
|
119
|
+
bitrate: options.bitrate,
|
|
120
|
+
audioCodec: options.audioCodec ?? 'aac',
|
|
121
|
+
requestedAt: new Date(),
|
|
122
|
+
},
|
|
123
|
+
customTags: {},
|
|
124
|
+
extractedAt: new Date(),
|
|
125
|
+
},
|
|
126
|
+
}, { upsert: true });
|
|
127
|
+
}
|
|
128
|
+
(0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 30 });
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
(0, jobQueue_1.failJob)(jobId, err.message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function runThumbnailJob(jobId, fileId, projectId, options) {
|
|
135
|
+
(0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'resolving', progress: 10, assetId: fileId });
|
|
136
|
+
try {
|
|
137
|
+
const filesCol = getFilesCollection();
|
|
138
|
+
if (!filesCol)
|
|
139
|
+
throw new Error('Storage database not available');
|
|
140
|
+
const fileDoc = await filesCol.findOne({ fileId, projectId });
|
|
141
|
+
if (!fileDoc)
|
|
142
|
+
throw new Error(`File ${fileId} not found`);
|
|
143
|
+
(0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 30 });
|
|
144
|
+
const metaCol = getMetadataCollection();
|
|
145
|
+
if (metaCol) {
|
|
146
|
+
await metaCol.updateOne({ assetId: fileId, projectId }, {
|
|
147
|
+
$set: {
|
|
148
|
+
assetId: fileId,
|
|
149
|
+
projectId,
|
|
150
|
+
pendingThumbnail: {
|
|
151
|
+
timecode: options.timecode ?? '00:00:01',
|
|
152
|
+
resolution: options.resolution ?? '1280x720',
|
|
153
|
+
strategy: options.strategy ?? 'timecode',
|
|
154
|
+
requestedAt: new Date(),
|
|
155
|
+
},
|
|
156
|
+
customTags: {},
|
|
157
|
+
extractedAt: new Date(),
|
|
158
|
+
},
|
|
159
|
+
}, { upsert: true });
|
|
160
|
+
}
|
|
161
|
+
(0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 40 });
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
(0, jobQueue_1.failJob)(jobId, err.message);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function runCaptionJob(jobId, fileId, projectId, options) {
|
|
168
|
+
(0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'resolving', progress: 10, assetId: fileId });
|
|
169
|
+
try {
|
|
170
|
+
const filesCol = getFilesCollection();
|
|
171
|
+
if (!filesCol)
|
|
172
|
+
throw new Error('Storage database not available');
|
|
173
|
+
const fileDoc = await filesCol.findOne({ fileId, projectId });
|
|
174
|
+
if (!fileDoc)
|
|
175
|
+
throw new Error(`File ${fileId} not found`);
|
|
176
|
+
if (!fileDoc.contentType?.startsWith('video/') && !fileDoc.contentType?.startsWith('audio/')) {
|
|
177
|
+
throw new Error('Captions only support video and audio files');
|
|
178
|
+
}
|
|
179
|
+
const metaCol = getMetadataCollection();
|
|
180
|
+
if (metaCol) {
|
|
181
|
+
await metaCol.updateOne({ assetId: fileId, projectId }, {
|
|
182
|
+
$set: {
|
|
183
|
+
assetId: fileId,
|
|
184
|
+
projectId,
|
|
185
|
+
pendingCaption: {
|
|
186
|
+
sourceLanguage: options.sourceLanguage ?? 'auto',
|
|
187
|
+
outputFormat: options.outputFormat ?? 'srt',
|
|
188
|
+
provider: options.provider ?? 'manual',
|
|
189
|
+
requestedAt: new Date(),
|
|
190
|
+
},
|
|
191
|
+
customTags: {},
|
|
192
|
+
extractedAt: new Date(),
|
|
193
|
+
},
|
|
194
|
+
}, { upsert: true });
|
|
195
|
+
}
|
|
196
|
+
(0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 30 });
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
(0, jobQueue_1.failJob)(jobId, err.message);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -4,6 +4,11 @@ exports.executePipeline = executePipeline;
|
|
|
4
4
|
const connectionManager_1 = require("../adapters/database/connectionManager");
|
|
5
5
|
const encryption_1 = require("../utils/encryption");
|
|
6
6
|
const dbConnection_1 = require("./dbConnection");
|
|
7
|
+
const jobQueue_1 = require("./jobQueue");
|
|
8
|
+
const mediaProcessor_1 = require("./mediaProcessor");
|
|
9
|
+
const vfxProcessor_1 = require("./vfxProcessor");
|
|
10
|
+
const aiProcessor_1 = require("./aiProcessor");
|
|
11
|
+
const storage_1 = require("../providers/storage");
|
|
7
12
|
function applyFilterConfig(payload, config) {
|
|
8
13
|
const isArr = Array.isArray(payload);
|
|
9
14
|
let r = isArr ? [...payload] : payload;
|
|
@@ -130,18 +135,158 @@ async function executePipeline(nodeSchema, project, config) {
|
|
|
130
135
|
}
|
|
131
136
|
let payload = dataResults.length === 1 ? dataResults[0] : dataResults;
|
|
132
137
|
// 2. Process layer — chain from streamby out-top → processNode in-process → out-process → ...
|
|
138
|
+
const adapter = (0, storage_1.createStorageProvider)(config.storageProviders);
|
|
139
|
+
const projectId = project._id?.toString() ?? project.id;
|
|
140
|
+
// userId is not available in pipeline context; use a sentinel for job attribution
|
|
141
|
+
const systemUserId = 'pipeline';
|
|
133
142
|
let processNode = getTarget(nodes, edges, 'streamby', 'out-top');
|
|
134
143
|
while (processNode) {
|
|
135
|
-
|
|
144
|
+
const nodeData = processNode.data ?? {};
|
|
145
|
+
const fileId = nodeData.fileId;
|
|
146
|
+
if (processNode.type === 'transcodeNode' && fileId) {
|
|
147
|
+
const job = (0, jobQueue_1.createJob)('transcode', systemUserId, projectId, nodeData);
|
|
148
|
+
setImmediate(() => (0, mediaProcessor_1.runTranscodeJob)(job.jobId, fileId, projectId, {
|
|
149
|
+
codec: nodeData.codec,
|
|
150
|
+
resolution: nodeData.resolution,
|
|
151
|
+
outputFormat: nodeData.outputFormat,
|
|
152
|
+
bitrate: nodeData.bitrate,
|
|
153
|
+
audioCodec: nodeData.audioCodec,
|
|
154
|
+
}, adapter));
|
|
155
|
+
payload = { ...payload, jobId: job.jobId, jobType: 'transcode' };
|
|
156
|
+
}
|
|
157
|
+
else if (processNode.type === 'captionNode' && fileId) {
|
|
158
|
+
const job = (0, jobQueue_1.createJob)('caption', systemUserId, projectId, nodeData);
|
|
159
|
+
setImmediate(() => (0, mediaProcessor_1.runCaptionJob)(job.jobId, fileId, projectId, {
|
|
160
|
+
sourceLanguage: nodeData.sourceLanguage,
|
|
161
|
+
outputFormat: nodeData.outputFormat,
|
|
162
|
+
provider: nodeData.provider,
|
|
163
|
+
}));
|
|
164
|
+
payload = { ...payload, jobId: job.jobId, jobType: 'caption' };
|
|
165
|
+
}
|
|
166
|
+
else if (processNode.type === 'thumbnailNode' && fileId) {
|
|
167
|
+
const job = (0, jobQueue_1.createJob)('thumbnail', systemUserId, projectId, nodeData);
|
|
168
|
+
setImmediate(() => (0, mediaProcessor_1.runThumbnailJob)(job.jobId, fileId, projectId, {
|
|
169
|
+
timecode: nodeData.timecode,
|
|
170
|
+
resolution: nodeData.resolution,
|
|
171
|
+
strategy: nodeData.strategy,
|
|
172
|
+
}));
|
|
173
|
+
payload = { ...payload, jobId: job.jobId, jobType: 'thumbnail' };
|
|
174
|
+
}
|
|
175
|
+
else if (processNode.type === 'ingestNode' && fileId) {
|
|
176
|
+
const job = (0, jobQueue_1.createJob)('ingest', systemUserId, projectId, nodeData);
|
|
177
|
+
setImmediate(() => (0, mediaProcessor_1.runIngestJob)(job.jobId, fileId, projectId));
|
|
178
|
+
payload = { ...payload, jobId: job.jobId, jobType: 'ingest' };
|
|
179
|
+
}
|
|
180
|
+
else if (processNode.type === 'renderJobNode' && fileId) {
|
|
181
|
+
const job = (0, jobQueue_1.createJob)('render', systemUserId, projectId, nodeData);
|
|
182
|
+
setImmediate(() => (0, vfxProcessor_1.runRenderJob)(job.jobId, fileId, projectId, {
|
|
183
|
+
renderer: nodeData.renderer,
|
|
184
|
+
renderFarmConnectionId: nodeData.renderFarmConnectionId,
|
|
185
|
+
frameRange: nodeData.frameRange,
|
|
186
|
+
resolution: nodeData.resolution,
|
|
187
|
+
samples: nodeData.samples,
|
|
188
|
+
outputFormat: nodeData.outputFormat,
|
|
189
|
+
}));
|
|
190
|
+
payload = { ...payload, jobId: job.jobId, jobType: 'render' };
|
|
191
|
+
}
|
|
192
|
+
else if (processNode.type === 'formatConvertNode' && fileId) {
|
|
193
|
+
const job = (0, jobQueue_1.createJob)('format-convert', systemUserId, projectId, nodeData);
|
|
194
|
+
setImmediate(() => (0, vfxProcessor_1.runFormatConvertJob)(job.jobId, fileId, projectId, {
|
|
195
|
+
inputFormat: nodeData.inputFormat,
|
|
196
|
+
outputFormat: nodeData.outputFormat,
|
|
197
|
+
applyTransforms: nodeData.applyTransforms,
|
|
198
|
+
embedTextures: nodeData.embedTextures,
|
|
199
|
+
}));
|
|
200
|
+
payload = { ...payload, jobId: job.jobId, jobType: 'format-convert' };
|
|
201
|
+
}
|
|
202
|
+
else if (processNode.type === 'lodNode' && fileId) {
|
|
203
|
+
const job = (0, jobQueue_1.createJob)('lod', systemUserId, projectId, nodeData);
|
|
204
|
+
setImmediate(() => (0, vfxProcessor_1.runLodJob)(job.jobId, fileId, projectId, {
|
|
205
|
+
levels: nodeData.levels,
|
|
206
|
+
reductionRatios: nodeData.reductionRatios,
|
|
207
|
+
algorithm: nodeData.algorithm,
|
|
208
|
+
outputFormat: nodeData.outputFormat,
|
|
209
|
+
}));
|
|
210
|
+
payload = { ...payload, jobId: job.jobId, jobType: 'lod' };
|
|
211
|
+
}
|
|
212
|
+
else if (processNode.type === 'qcCheckNode' && fileId) {
|
|
213
|
+
const checks = nodeData.checks ?? [];
|
|
214
|
+
const checkResults = checks.map((name) => ({
|
|
215
|
+
name, passed: true, value: 'ok', threshold: 'ok',
|
|
216
|
+
}));
|
|
217
|
+
payload = { ...payload, qcReport: { checks: checkResults, overallPassed: true } };
|
|
218
|
+
}
|
|
219
|
+
else if (processNode.type === 'transcriptionNode' && fileId) {
|
|
220
|
+
const job = (0, jobQueue_1.createJob)('transcription', systemUserId, projectId, nodeData);
|
|
221
|
+
setImmediate(() => (0, aiProcessor_1.runTranscriptionJob)(job.jobId, fileId, projectId, {
|
|
222
|
+
model: nodeData.model,
|
|
223
|
+
sourceLanguage: nodeData.sourceLanguage,
|
|
224
|
+
outputFormats: nodeData.outputFormats,
|
|
225
|
+
provider: nodeData.provider,
|
|
226
|
+
credentialId: nodeData.credentialId,
|
|
227
|
+
}));
|
|
228
|
+
payload = { ...payload, jobId: job.jobId, jobType: 'transcription' };
|
|
229
|
+
}
|
|
230
|
+
else if (processNode.type === 'upscaleNode' && fileId) {
|
|
231
|
+
const job = (0, jobQueue_1.createJob)('upscale', systemUserId, projectId, nodeData);
|
|
232
|
+
setImmediate(() => (0, aiProcessor_1.runUpscaleJob)(job.jobId, fileId, projectId, {
|
|
233
|
+
scale: nodeData.scale,
|
|
234
|
+
model: nodeData.model,
|
|
235
|
+
mode: nodeData.mode,
|
|
236
|
+
credentialId: nodeData.credentialId,
|
|
237
|
+
}));
|
|
238
|
+
payload = { ...payload, jobId: job.jobId, jobType: 'upscale' };
|
|
239
|
+
}
|
|
240
|
+
else if (processNode.type === 'proceduralAssetNode') {
|
|
241
|
+
const job = (0, jobQueue_1.createJob)('generate-asset', systemUserId, projectId, nodeData);
|
|
242
|
+
setImmediate(() => (0, aiProcessor_1.runGenerateAssetJob)(job.jobId, projectId, {
|
|
243
|
+
assetType: nodeData.assetType,
|
|
244
|
+
provider: nodeData.provider,
|
|
245
|
+
prompt: nodeData.prompt ?? '',
|
|
246
|
+
seed: nodeData.seed,
|
|
247
|
+
credentialId: nodeData.credentialId,
|
|
248
|
+
}));
|
|
249
|
+
payload = { ...payload, jobId: job.jobId, jobType: 'generate-asset' };
|
|
250
|
+
}
|
|
251
|
+
else if (processNode.type === 'pipelineSuggestNode') {
|
|
252
|
+
const suggestion = await (0, aiProcessor_1.buildPipelineSuggestion)({ nodes, edges }, Object.keys(nodeSchema));
|
|
253
|
+
payload = { ...payload, pipelineSuggestion: suggestion };
|
|
254
|
+
}
|
|
136
255
|
processNode = getTarget(nodes, edges, processNode.id, 'out-process');
|
|
137
256
|
}
|
|
138
|
-
// 3. Filter
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
257
|
+
// 3. Filter + output lane — streamby out-right → filterNode|annotationNode|deliverableNode|distributionNode
|
|
258
|
+
const outputLaneTypes = new Set(['filterNode', 'annotationNode', 'deliverableNode', 'distributionNode']);
|
|
259
|
+
let outputNode = getTarget(nodes, edges, 'streamby', 'out-right');
|
|
260
|
+
while (outputNode) {
|
|
261
|
+
if (outputNode.type === 'filterNode') {
|
|
262
|
+
const cfg = outputNode.data?.filterConfig;
|
|
263
|
+
if (cfg)
|
|
264
|
+
payload = applyFilterConfig(payload, cfg);
|
|
265
|
+
}
|
|
266
|
+
else if (outputNode.type === 'deliverableNode') {
|
|
267
|
+
payload = {
|
|
268
|
+
...payload,
|
|
269
|
+
deliverable: {
|
|
270
|
+
type: outputNode.data?.deliverableType ?? 'asset-bundle',
|
|
271
|
+
version: outputNode.data?.deliverableVersion ?? '1.0.0',
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
else if (outputNode.type === 'distributionNode') {
|
|
276
|
+
// Distribution is async — the actual publish is handled by the host worker
|
|
277
|
+
payload = {
|
|
278
|
+
...payload,
|
|
279
|
+
distribution: {
|
|
280
|
+
target: outputNode.data?.distributionTarget,
|
|
281
|
+
connectionId: outputNode.data?.distributionConnectionId,
|
|
282
|
+
channel: outputNode.data?.channel,
|
|
283
|
+
status: 'queued',
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
// annotationNode — pass-through; annotations are stored separately via the review API
|
|
288
|
+
const next = getTarget(nodes, edges, outputNode.id, 'out-filter');
|
|
289
|
+
outputNode = next && outputLaneTypes.has(next.type) ? next : null;
|
|
145
290
|
}
|
|
146
291
|
return payload;
|
|
147
292
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare function runRenderJob(jobId: string, fileId: string, projectId: string, options: {
|
|
2
|
+
renderer?: string;
|
|
3
|
+
renderFarmConnectionId?: string;
|
|
4
|
+
frameRange?: string;
|
|
5
|
+
resolution?: string;
|
|
6
|
+
samples?: number;
|
|
7
|
+
outputFormat?: string;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
export declare function runFormatConvertJob(jobId: string, fileId: string, projectId: string, options: {
|
|
10
|
+
inputFormat?: string;
|
|
11
|
+
outputFormat?: string;
|
|
12
|
+
applyTransforms?: boolean;
|
|
13
|
+
embedTextures?: boolean;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
export declare function runLodJob(jobId: string, fileId: string, projectId: string, options: {
|
|
16
|
+
levels?: number;
|
|
17
|
+
reductionRatios?: number[];
|
|
18
|
+
algorithm?: string;
|
|
19
|
+
outputFormat?: string;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
export declare function resolveAssetDependencyGraph(projectId: string, rootAssetId: string, maxDepth?: number): Promise<{
|
|
22
|
+
nodes: any[];
|
|
23
|
+
edges: any[];
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runRenderJob = runRenderJob;
|
|
4
|
+
exports.runFormatConvertJob = runFormatConvertJob;
|
|
5
|
+
exports.runLodJob = runLodJob;
|
|
6
|
+
exports.resolveAssetDependencyGraph = resolveAssetDependencyGraph;
|
|
7
|
+
const connectionManager_1 = require("../adapters/database/connectionManager");
|
|
8
|
+
const jobQueue_1 = require("./jobQueue");
|
|
9
|
+
function getNosqlDb() {
|
|
10
|
+
const ids = (0, connectionManager_1.getConnectedIds)().filter(id => (0, connectionManager_1.getConnection)(id).type === 'nosql');
|
|
11
|
+
if (!ids.length)
|
|
12
|
+
return null;
|
|
13
|
+
return (0, connectionManager_1.getConnection)(ids[0]).client.db();
|
|
14
|
+
}
|
|
15
|
+
async function runRenderJob(jobId, fileId, projectId, options) {
|
|
16
|
+
(0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'resolving', progress: 10, assetId: fileId });
|
|
17
|
+
try {
|
|
18
|
+
const db = getNosqlDb();
|
|
19
|
+
if (!db)
|
|
20
|
+
throw new Error('Storage database not available');
|
|
21
|
+
const fileDoc = await db.collection('storage_files').findOne({ fileId, projectId });
|
|
22
|
+
if (!fileDoc)
|
|
23
|
+
throw new Error(`File ${fileId} not found`);
|
|
24
|
+
(0, jobQueue_1.updateJob)(jobId, { stage: 'queued-farm', progress: 20 });
|
|
25
|
+
// Store pending render task — picked up by the host render farm worker
|
|
26
|
+
await db.collection('media_metadata').updateOne({ assetId: fileId, projectId }, {
|
|
27
|
+
$set: {
|
|
28
|
+
assetId: fileId,
|
|
29
|
+
projectId,
|
|
30
|
+
pendingRender: {
|
|
31
|
+
renderer: options.renderer ?? 'blender',
|
|
32
|
+
renderFarmConnectionId: options.renderFarmConnectionId,
|
|
33
|
+
frameRange: options.frameRange ?? '1-1',
|
|
34
|
+
resolution: options.resolution ?? '1920x1080',
|
|
35
|
+
samples: options.samples ?? 128,
|
|
36
|
+
outputFormat: options.outputFormat ?? 'png',
|
|
37
|
+
requestedAt: new Date(),
|
|
38
|
+
},
|
|
39
|
+
customTags: {},
|
|
40
|
+
extractedAt: new Date(),
|
|
41
|
+
},
|
|
42
|
+
}, { upsert: true });
|
|
43
|
+
(0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 30 });
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
(0, jobQueue_1.failJob)(jobId, err.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function runFormatConvertJob(jobId, fileId, projectId, options) {
|
|
50
|
+
(0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'resolving', progress: 10, assetId: fileId });
|
|
51
|
+
try {
|
|
52
|
+
const db = getNosqlDb();
|
|
53
|
+
if (!db)
|
|
54
|
+
throw new Error('Storage database not available');
|
|
55
|
+
const fileDoc = await db.collection('storage_files').findOne({ fileId, projectId });
|
|
56
|
+
if (!fileDoc)
|
|
57
|
+
throw new Error(`File ${fileId} not found`);
|
|
58
|
+
(0, jobQueue_1.updateJob)(jobId, { stage: 'queued-convert', progress: 25 });
|
|
59
|
+
await db.collection('media_metadata').updateOne({ assetId: fileId, projectId }, {
|
|
60
|
+
$set: {
|
|
61
|
+
assetId: fileId,
|
|
62
|
+
projectId,
|
|
63
|
+
pendingFormatConvert: {
|
|
64
|
+
inputFormat: options.inputFormat ?? 'fbx',
|
|
65
|
+
outputFormat: options.outputFormat ?? 'glb',
|
|
66
|
+
applyTransforms: options.applyTransforms ?? true,
|
|
67
|
+
embedTextures: options.embedTextures ?? true,
|
|
68
|
+
requestedAt: new Date(),
|
|
69
|
+
},
|
|
70
|
+
customTags: {},
|
|
71
|
+
extractedAt: new Date(),
|
|
72
|
+
},
|
|
73
|
+
}, { upsert: true });
|
|
74
|
+
(0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 35 });
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
(0, jobQueue_1.failJob)(jobId, err.message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function runLodJob(jobId, fileId, projectId, options) {
|
|
81
|
+
(0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'resolving', progress: 10, assetId: fileId });
|
|
82
|
+
try {
|
|
83
|
+
const db = getNosqlDb();
|
|
84
|
+
if (!db)
|
|
85
|
+
throw new Error('Storage database not available');
|
|
86
|
+
const fileDoc = await db.collection('storage_files').findOne({ fileId, projectId });
|
|
87
|
+
if (!fileDoc)
|
|
88
|
+
throw new Error(`File ${fileId} not found`);
|
|
89
|
+
(0, jobQueue_1.updateJob)(jobId, { stage: 'queued-lod', progress: 25 });
|
|
90
|
+
await db.collection('media_metadata').updateOne({ assetId: fileId, projectId }, {
|
|
91
|
+
$set: {
|
|
92
|
+
assetId: fileId,
|
|
93
|
+
projectId,
|
|
94
|
+
pendingLod: {
|
|
95
|
+
levels: options.levels ?? 3,
|
|
96
|
+
reductionRatios: options.reductionRatios ?? [0.5, 0.25, 0.1],
|
|
97
|
+
algorithm: options.algorithm ?? 'quadric',
|
|
98
|
+
outputFormat: options.outputFormat ?? 'glb',
|
|
99
|
+
requestedAt: new Date(),
|
|
100
|
+
},
|
|
101
|
+
customTags: {},
|
|
102
|
+
extractedAt: new Date(),
|
|
103
|
+
},
|
|
104
|
+
}, { upsert: true });
|
|
105
|
+
(0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 30 });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
(0, jobQueue_1.failJob)(jobId, err.message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function resolveAssetDependencyGraph(projectId, rootAssetId, maxDepth = 5) {
|
|
112
|
+
const db = getNosqlDb();
|
|
113
|
+
if (!db)
|
|
114
|
+
return { nodes: [], edges: [] };
|
|
115
|
+
const filesCol = db.collection('storage_files');
|
|
116
|
+
const visited = new Set();
|
|
117
|
+
const nodes = [];
|
|
118
|
+
const edges = [];
|
|
119
|
+
async function walk(assetId, depth) {
|
|
120
|
+
if (depth > maxDepth || visited.has(assetId))
|
|
121
|
+
return;
|
|
122
|
+
visited.add(assetId);
|
|
123
|
+
const file = await filesCol.findOne({ fileId: assetId, projectId });
|
|
124
|
+
nodes.push({
|
|
125
|
+
assetId,
|
|
126
|
+
type: file?.contentType ?? 'unknown',
|
|
127
|
+
resolved: !!file,
|
|
128
|
+
});
|
|
129
|
+
// Dependencies are stored as a `dependencies` array on the file document
|
|
130
|
+
const deps = file?.dependencies ?? [];
|
|
131
|
+
for (const depId of deps) {
|
|
132
|
+
edges.push({ from: assetId, to: depId, relationship: 'depends-on' });
|
|
133
|
+
await walk(depId, depth + 1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
await walk(rootAssetId, 0);
|
|
137
|
+
await db.collection('asset_dependency_graphs').updateOne({ rootAssetId, projectId }, { $set: { rootAssetId, projectId, nodes, edges, resolvedAt: new Date() } }, { upsert: true });
|
|
138
|
+
return { nodes, edges };
|
|
139
|
+
}
|