Spaces:
Sleeping
Sleeping
Commit
·
5a54bc2
1
Parent(s):
721d060
Add sync API
Browse files- app.js +31 -26
- common-utils +1 -1
- package-lock.json +0 -0
- package.json +4 -1
- renderer.js +84 -0
- routes.js +124 -0
- start.sh +1 -1
app.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
| 1 |
import express from 'express';
|
| 2 |
-
import stream from 'stream';
|
| 3 |
-
import bodyParser from 'body-parser';
|
| 4 |
import fs, {
|
| 5 |
existsSync,
|
| 6 |
-
unlinkSync,
|
| 7 |
readFileSync,
|
| 8 |
writeFileSync,
|
| 9 |
copyFileSync,
|
|
@@ -13,16 +10,23 @@ import fs, {
|
|
| 13 |
createWriteStream,
|
| 14 |
} from 'fs';
|
| 15 |
import path, {resolve as _resolve, join} from 'path';
|
| 16 |
-
import {
|
| 17 |
-
import {ZipFiles, UnzipFiles} from 'common-utils';
|
| 18 |
-
import OracleStorage from './common-creds/oracle/storage.json' assert {type: 'json'};
|
| 19 |
-
import {spawn, exec, ChildProcess} from 'child_process';
|
| 20 |
-
import {FireStoreDB} from 'multi-db-orm';
|
| 21 |
-
import {syllableCount} from 'syllable-count-english';
|
| 22 |
import kill from 'tree-kill';
|
| 23 |
import {fileURLToPath} from 'url';
|
| 24 |
import {dirname} from 'path';
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
const __filename = fileURLToPath(import.meta.url);
|
| 27 |
const __dirname = dirname(__filename);
|
| 28 |
const axios = Utils.getAxios();
|
|
@@ -179,6 +183,7 @@ function clear() {
|
|
| 179 |
Utils.clearFolder(join(__dirname, './uploads'));
|
| 180 |
} catch (e) {}
|
| 181 |
}
|
|
|
|
| 182 |
app.all('/clear', async (req, res) => {
|
| 183 |
clear();
|
| 184 |
res.send('Cache Cleared');
|
|
@@ -584,22 +589,22 @@ function doRenderPoster(jobId, statusCb) {
|
|
| 584 |
|
| 585 |
let indexFile = join(__dirname, 'index.html');
|
| 586 |
|
| 587 |
-
app.get('/execute', (req, res) => {
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
});
|
| 603 |
|
| 604 |
app.get('/', (req, res) => {
|
| 605 |
res.sendFile(indexFile);
|
|
|
|
| 1 |
import express from 'express';
|
|
|
|
|
|
|
| 2 |
import fs, {
|
| 3 |
existsSync,
|
|
|
|
| 4 |
readFileSync,
|
| 5 |
writeFileSync,
|
| 6 |
copyFileSync,
|
|
|
|
| 10 |
createWriteStream,
|
| 11 |
} from 'fs';
|
| 12 |
import path, {resolve as _resolve, join} from 'path';
|
| 13 |
+
import {exec} from 'child_process';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
import kill from 'tree-kill';
|
| 15 |
import {fileURLToPath} from 'url';
|
| 16 |
import {dirname} from 'path';
|
| 17 |
+
import RenderRouter from './routes.js';
|
| 18 |
+
|
| 19 |
+
import pkg from 'common-utils';
|
| 20 |
+
const {
|
| 21 |
+
UnzipFiles,
|
| 22 |
+
ZipFiles,
|
| 23 |
+
Utils,
|
| 24 |
+
FileUploader,
|
| 25 |
+
PerformanceRecorder,
|
| 26 |
+
Auditlog,
|
| 27 |
+
Vault,
|
| 28 |
+
} = pkg;
|
| 29 |
+
const OracleStorage = Vault.get().readCredSyncJson('oracle/storage.json');
|
| 30 |
const __filename = fileURLToPath(import.meta.url);
|
| 31 |
const __dirname = dirname(__filename);
|
| 32 |
const axios = Utils.getAxios();
|
|
|
|
| 183 |
Utils.clearFolder(join(__dirname, './uploads'));
|
| 184 |
} catch (e) {}
|
| 185 |
}
|
| 186 |
+
app.use(RenderRouter);
|
| 187 |
app.all('/clear', async (req, res) => {
|
| 188 |
clear();
|
| 189 |
res.send('Cache Cleared');
|
|
|
|
| 589 |
|
| 590 |
let indexFile = join(__dirname, 'index.html');
|
| 591 |
|
| 592 |
+
// app.get('/execute', (req, res) => {
|
| 593 |
+
// const command = decodeURIComponent(req.query.cmd);
|
| 594 |
+
// console.log(command);
|
| 595 |
+
// exec(command, (error, stdout, stderr) => {
|
| 596 |
+
// if (error) {
|
| 597 |
+
// console.error(`Error executing command: ${error.message}`);
|
| 598 |
+
// res.status(500).json({error: error.message});
|
| 599 |
+
// return;
|
| 600 |
+
// }
|
| 601 |
+
// if (stderr) {
|
| 602 |
+
// console.error(`Command stderr: ${stderr}`);
|
| 603 |
+
// }
|
| 604 |
+
// console.log(stdout);
|
| 605 |
+
// res.send(stdout);
|
| 606 |
+
// });
|
| 607 |
+
// });
|
| 608 |
|
| 609 |
app.get('/', (req, res) => {
|
| 610 |
res.sendFile(indexFile);
|
common-utils
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
Subproject commit
|
|
|
|
| 1 |
+
Subproject commit a07eb36a227970fc69f0cb70e00e8b8a2306697b
|
package-lock.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
CHANGED
|
@@ -53,8 +53,10 @@
|
|
| 53 |
"@remotion/zod-types": "4.0.19",
|
| 54 |
"@types/react": "^18.0.26",
|
| 55 |
"@types/web": "^0.0.86",
|
|
|
|
| 56 |
"axios": "^1.4.0",
|
| 57 |
"body-parser": "^1.20.2",
|
|
|
|
| 58 |
"command-line-args": "^6.0.0",
|
| 59 |
"common-utils": "file:common-utils",
|
| 60 |
"express": "^4.18.2",
|
|
@@ -85,6 +87,7 @@
|
|
| 85 |
"tree-kill": "^1.2.2",
|
| 86 |
"ts-node-dev": "^2.0.0",
|
| 87 |
"typescript": "^4.9.4",
|
|
|
|
| 88 |
"which": "^3.0.1",
|
| 89 |
"ws": "^8.13.0",
|
| 90 |
"zod": "^3.21.4"
|
|
@@ -93,4 +96,4 @@
|
|
| 93 |
"devDependencies": {
|
| 94 |
"@types/lodash": "^4.14.199"
|
| 95 |
}
|
| 96 |
-
}
|
|
|
|
| 53 |
"@remotion/zod-types": "4.0.19",
|
| 54 |
"@types/react": "^18.0.26",
|
| 55 |
"@types/web": "^0.0.86",
|
| 56 |
+
"archiver": "^7.0.1",
|
| 57 |
"axios": "^1.4.0",
|
| 58 |
"body-parser": "^1.20.2",
|
| 59 |
+
"bottleneck": "^2.19.5",
|
| 60 |
"command-line-args": "^6.0.0",
|
| 61 |
"common-utils": "file:common-utils",
|
| 62 |
"express": "^4.18.2",
|
|
|
|
| 87 |
"tree-kill": "^1.2.2",
|
| 88 |
"ts-node-dev": "^2.0.0",
|
| 89 |
"typescript": "^4.9.4",
|
| 90 |
+
"unzipper": "^0.12.3",
|
| 91 |
"which": "^3.0.1",
|
| 92 |
"ws": "^8.13.0",
|
| 93 |
"zod": "^3.21.4"
|
|
|
|
| 96 |
"devDependencies": {
|
| 97 |
"@types/lodash": "^4.14.199"
|
| 98 |
}
|
| 99 |
+
}
|
renderer.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {copyFileSync, existsSync, readdirSync, unlinkSync} from 'fs';
|
| 2 |
+
import {join} from 'path';
|
| 3 |
+
import pkg from 'common-utils';
|
| 4 |
+
import {exec} from 'child_process';
|
| 5 |
+
import {dirname} from 'path';
|
| 6 |
+
import {fileURLToPath} from 'url';
|
| 7 |
+
const {UnzipFiles, Utils, ZipFiles} = pkg;
|
| 8 |
+
|
| 9 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 10 |
+
const __dirname = dirname(__filename);
|
| 11 |
+
|
| 12 |
+
export async function explodeUrl(fileUrl, jobId, dir) {
|
| 13 |
+
const zipFile = `${dir}/exported-${jobId}.zip`;
|
| 14 |
+
await Utils.downloadFile(fileUrl, zipFile, true);
|
| 15 |
+
await UnzipFiles(zipFile, dir);
|
| 16 |
+
}
|
| 17 |
+
export async function listOutputFiles(jobId) {
|
| 18 |
+
let outDir = join(__dirname, 'out');
|
| 19 |
+
let manuFile = join(__dirname, `public/original_manuscript.json`);
|
| 20 |
+
copyFileSync(manuFile, join(__dirname, 'out', `original_manuscript.json`));
|
| 21 |
+
let outputFiles = readdirSync(outDir).map((fname) => {
|
| 22 |
+
const filePath = join(outDir, fname);
|
| 23 |
+
return filePath;
|
| 24 |
+
});
|
| 25 |
+
return outputFiles;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export async function generateOutputBundle(jobId) {
|
| 29 |
+
let outFile = join(__dirname, 'out', `output-${jobId}.zip`);
|
| 30 |
+
let outputFiles = await listOutputFiles(jobId);
|
| 31 |
+
if (existsSync(outFile)) {
|
| 32 |
+
unlinkSync(outFile);
|
| 33 |
+
}
|
| 34 |
+
await ZipFiles(outputFiles, outFile);
|
| 35 |
+
return outFile;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
export function doRender(jobId, composition, sendToObserver) {
|
| 39 |
+
const npmScript = 'render-build';
|
| 40 |
+
const renderComposition = composition || 'SemibitComposition';
|
| 41 |
+
let outFile = `out/${jobId}-video.mp4`;
|
| 42 |
+
let cmd = `npm run ${npmScript} -- ${renderComposition} `;
|
| 43 |
+
const childProcess = exec(cmd);
|
| 44 |
+
|
| 45 |
+
console.log('Starting video render. ' + cmd);
|
| 46 |
+
let updateCounter = 0;
|
| 47 |
+
childProcess.stdout.on('data', (data) => {
|
| 48 |
+
sendToObserver(jobId, data);
|
| 49 |
+
if (!process.env.is_pm2) console.log(data.toString());
|
| 50 |
+
if (updateCounter++ % 100 == 0 || updateCounter < 5) {
|
| 51 |
+
console.log(data.split('\n')[0]);
|
| 52 |
+
}
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
childProcess.stderr.on('data', (data) => {
|
| 56 |
+
sendToObserver(jobId, data);
|
| 57 |
+
console.error(data.toString());
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
return new Promise((resolve, reject) => {
|
| 61 |
+
childProcess.on('close', (code) => {
|
| 62 |
+
console.log('Render video finished');
|
| 63 |
+
sendToObserver(jobId, code === 0 ? 'completed' : 'failed');
|
| 64 |
+
if (code === 0) {
|
| 65 |
+
resolve(outFile);
|
| 66 |
+
console.log(`'${npmScript}' completed successfully.`);
|
| 67 |
+
} else {
|
| 68 |
+
reject({
|
| 69 |
+
message: `'${npmScript}' failed with code ${code}.`,
|
| 70 |
+
});
|
| 71 |
+
console.error(`'${npmScript}' failed with code ${code}.`);
|
| 72 |
+
}
|
| 73 |
+
});
|
| 74 |
+
});
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
export function clear(skipOutput, skipPublic, skipUploads) {
|
| 78 |
+
try {
|
| 79 |
+
const preserve = ['assets', 'mp3'];
|
| 80 |
+
if (!skipPublic) Utils.clearFolder(join(__dirname, './public'), preserve);
|
| 81 |
+
if (!skipOutput) Utils.clearFolder(join(__dirname, './out'));
|
| 82 |
+
if (!skipUploads) Utils.clearFolder(join(__dirname, './uploads'));
|
| 83 |
+
} catch (e) {}
|
| 84 |
+
}
|
routes.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {Router} from 'express';
|
| 2 |
+
import Bottleneck from 'bottleneck';
|
| 3 |
+
import {
|
| 4 |
+
clear,
|
| 5 |
+
doRender,
|
| 6 |
+
explodeUrl,
|
| 7 |
+
generateOutputBundle,
|
| 8 |
+
listOutputFiles,
|
| 9 |
+
} from './renderer.js';
|
| 10 |
+
import path, {dirname} from 'path';
|
| 11 |
+
import {fileURLToPath} from 'url';
|
| 12 |
+
import pkg from 'common-utils';
|
| 13 |
+
const {Utils, PerformanceRecorder, FileUploader, Vault} = pkg;
|
| 14 |
+
import bodyParser from 'body-parser';
|
| 15 |
+
|
| 16 |
+
const OracleStorage = Vault.get().readCredSyncJson('oracle/storage.json');
|
| 17 |
+
const RenderRouter = Router();
|
| 18 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 19 |
+
const __dirname = dirname(__filename);
|
| 20 |
+
const limiter = new Bottleneck({
|
| 21 |
+
maxConcurrent: 1,
|
| 22 |
+
});
|
| 23 |
+
RenderRouter.use(bodyParser.json());
|
| 24 |
+
RenderRouter.use(bodyParser.urlencoded());
|
| 25 |
+
RenderRouter.post('/api/render-sync', async (req, res) => {
|
| 26 |
+
let fileUrl = req.body.fileUrl;
|
| 27 |
+
let targetUrl = req.body.targetUrl;
|
| 28 |
+
let skipClear = req.body.skip_clear;
|
| 29 |
+
let skipRender = req.body.skip_render;
|
| 30 |
+
let zip = req.body.zip ?? true;
|
| 31 |
+
|
| 32 |
+
if (!fileUrl) {
|
| 33 |
+
return res.status(400).send({
|
| 34 |
+
message:
|
| 35 |
+
'Missing `fileUrl` in body. Required params `fileUrl`, `targetUrl`. Optionally pass `zip` to skip zipping and directly upload the files to targetUrl/{fileName}, `skip_clear` if you dont want to clear the folders after render, `skip_render` is you want to skip rendering, useful with `skip_clear` ',
|
| 36 |
+
});
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
if (!targetUrl) {
|
| 40 |
+
return res.status(400).send({
|
| 41 |
+
message:
|
| 42 |
+
'Missing `targetUrl` in body. The result will be uploaded to `targetUrl`',
|
| 43 |
+
});
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// make sure only i request is being processed at a time
|
| 47 |
+
// set headers appoprately to hint that timeout must be large
|
| 48 |
+
|
| 49 |
+
let jobId = req.body.jobId || Utils.generateUID(fileUrl);
|
| 50 |
+
res.setHeader('X-Job-Id', jobId);
|
| 51 |
+
res.setTimeout(0);
|
| 52 |
+
res.setHeader('Connection', 'keep-alive');
|
| 53 |
+
if (!skipClear) {
|
| 54 |
+
clear();
|
| 55 |
+
}
|
| 56 |
+
let logs = [];
|
| 57 |
+
try {
|
| 58 |
+
async function run() {
|
| 59 |
+
let pref = new PerformanceRecorder();
|
| 60 |
+
await explodeUrl(fileUrl, jobId, path.join(__dirname, 'public'));
|
| 61 |
+
let originalManuscript = Utils.readFileToObject(
|
| 62 |
+
path.join(__dirname, `public/original_manuscript.json`)
|
| 63 |
+
);
|
| 64 |
+
if (!skipRender) {
|
| 65 |
+
await doRender(
|
| 66 |
+
jobId,
|
| 67 |
+
originalManuscript.meta.renderComposition,
|
| 68 |
+
(jobId, log) => {
|
| 69 |
+
logs.push(log);
|
| 70 |
+
}
|
| 71 |
+
);
|
| 72 |
+
}
|
| 73 |
+
const uploader = new FileUploader('oracle', {
|
| 74 |
+
url: targetUrl + jobId + '/',
|
| 75 |
+
});
|
| 76 |
+
uploader.creds.url = uploader.creds.url;
|
| 77 |
+
if (zip) {
|
| 78 |
+
let outFile = await generateOutputBundle(jobId);
|
| 79 |
+
let uploadResult = await uploader.upload(outFile);
|
| 80 |
+
if (!skipClear) {
|
| 81 |
+
clear();
|
| 82 |
+
}
|
| 83 |
+
return {
|
| 84 |
+
response_time: pref.elapsed(),
|
| 85 |
+
urls: [uploadResult.url],
|
| 86 |
+
url: uploadResult.url,
|
| 87 |
+
original_manuscript: originalManuscript,
|
| 88 |
+
};
|
| 89 |
+
} else {
|
| 90 |
+
let outFiles = await listOutputFiles(jobId);
|
| 91 |
+
let urls = await Promise.all(
|
| 92 |
+
outFiles.map((file) => {
|
| 93 |
+
return uploader.upload(file);
|
| 94 |
+
})
|
| 95 |
+
).then((results) => {
|
| 96 |
+
return results.map((result) => result.url);
|
| 97 |
+
});
|
| 98 |
+
let url =
|
| 99 |
+
urls.find((u) => u.includes('.mp4')) ||
|
| 100 |
+
urls.find((u) => u.includes('.jpg')) ||
|
| 101 |
+
urls.find((u) => u.includes('.jpeg')) ||
|
| 102 |
+
urls.find((u) => u.includes('.png'));
|
| 103 |
+
return {
|
| 104 |
+
response_time: pref.elapsed(),
|
| 105 |
+
urls: urls,
|
| 106 |
+
url,
|
| 107 |
+
original_manuscript: originalManuscript,
|
| 108 |
+
};
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
const result = await limiter.schedule(run);
|
| 113 |
+
res.status(200).json({jobId, success: true, ...result});
|
| 114 |
+
} catch (err) {
|
| 115 |
+
console.error(`Render job ${jobId} failed:`, err.message);
|
| 116 |
+
res.status(500).json({
|
| 117 |
+
jobId,
|
| 118 |
+
success: false,
|
| 119 |
+
message: err.message + ' Details : ' + logs.join('\n'),
|
| 120 |
+
});
|
| 121 |
+
}
|
| 122 |
+
});
|
| 123 |
+
|
| 124 |
+
export default RenderRouter;
|
start.sh
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
export DISPLAY=:
|
| 2 |
|
| 3 |
|
| 4 |
# sudo nano /etc/systemd/system/media-render-farm.service
|
|
|
|
| 1 |
+
export DISPLAY=:10.0 && is_pm2=1 node server.js
|
| 2 |
|
| 3 |
|
| 4 |
# sudo nano /etc/systemd/system/media-render-farm.service
|