remote-rdr / app.js
shiveshnavin's picture
Update dockerfile
8533410
raw
history blame
16.5 kB
import express from 'express';
import fs, {
existsSync,
readFileSync,
writeFileSync,
copyFileSync,
readdirSync,
mkdirSync,
createReadStream,
createWriteStream,
} from 'fs';
import path, {resolve as _resolve, join} from 'path';
import {exec} from 'child_process';
import kill from 'tree-kill';
import {fileURLToPath} from 'url';
import {dirname} from 'path';
import RenderRouter from './routes.js';
import pkg from 'common-utils';
const {
UnzipFiles,
ZipFiles,
Utils,
FileUploader,
PerformanceRecorder,
Auditlog,
Vault,
} = pkg;
const OracleStorage = Utils.readFileToObject('storage.json');
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const axios = Utils.getAxios();
if (process.env.is_pm2) {
console.log('Disabling render logs to stdout as env is pm2');
}
function modifyFiles(originalManuscript, originalManuscriptFile) {
let fname = join(__dirname, './src/youtube/SequentialScene.orig.tsx');
let fnameTarget = join(__dirname, './src/youtube/SequentialScene.tsx');
let SequentialSceneText = readFileSync(fname).toString();
let seqScene = '';
let script = originalManuscript.transcript;
for (let index = 0; index < script.length; index++) {
if (index > 0)
seqScene = seqScene + `\n{getTransitionScene(contents[${index}])}\n`;
seqScene = seqScene + `\n{getScene(contents[${index}])}\n`;
}
generateSubtitles(originalManuscript);
writeFileSync(
originalManuscriptFile,
JSON.stringify(originalManuscript, null, 2)
);
writeFileSync(
fnameTarget,
SequentialSceneText.replace('{getScene(contents[0])}', seqScene)
);
}
function generateSubtitles(originalManuscript) {
let transcript = originalManuscript.transcript;
for (let index = 0; index < transcript.length; index++) {
const section = transcript[index];
section.total_syllables = getDurationForSentenceByPhenone(
section.text.trim()
);
let speakerSyllablesPs =
section.total_syllables / section.durationInSeconds;
let calc = 0;
section.subtitles = section.text.split('.').map((t) => {
let c = getDurationForSentenceByPhenone(t.trim()) / speakerSyllablesPs;
calc += c;
return {
text: t.trim(),
expectedDurationSec: c,
};
});
section.calculate_duration_sec = calc;
}
return originalManuscript;
}
function getDurationForSentenceByPhenone(text) {
return text.length; //syllableCount(text)
}
function checkUnzipExists() {
return new Promise((resolve, reject) => {
const checkCommand = 'command -v unzip || which unzip';
exec(checkCommand, (error, stdout, stderr) => {
if (error || stderr || !stdout.trim()) {
resolve(false); // Return false if unzip was not found
} else {
resolve(true); // Return true if unzip exists in the PATH
}
});
});
}
async function UnzipFilesLocal(zipFilePath, destinationDirectory) {
try {
if (await checkUnzipExists())
return await new Promise((resolve, reject) => {
const fileName = path.basename(zipFilePath);
const unzipCommand = `unzip -o ${fileName} -d ${destinationDirectory}`;
exec(
unzipCommand,
{cwd: path.dirname(zipFilePath)},
(error, stdout, stderr) => {
if (error) {
console.error(`Error executing unzip command: ${error.message}`);
reject(error);
return;
}
if (stderr) {
console.error(`Unzip command stderr: ${stderr}`);
}
console.log(`Unzip command stdout: ${stdout}`);
resolve();
}
);
});
else return await UnzipFiles(zipFilePath, destinationDirectory);
} catch (error_1) {
console.error('Unzip check failed:', error_1.message);
throw error_1;
}
}
async function extract(filePath) {
await UnzipFilesLocal(filePath, path.join(__dirname, 'public'));
}
async function generateOutputBundle(jobId) {
let outDir = join(__dirname, 'out');
let outFile = join(__dirname, 'out', `${jobId}.zip`);
let manuFile = join(__dirname, `public/original_manuscript.json`);
copyFileSync(manuFile, join(__dirname, 'out', `original_manuscript.json`));
let outputFiles = readdirSync(outDir).map((fname) => {
const filePath = join(outDir, fname);
return filePath;
});
await ZipFiles(outputFiles, outFile);
return outFile;
}
async function notify(
jobId,
origManu,
outputUrl,
bundleUrl,
status,
time_taken
) {
origManu.meta.render = {
output_url: outputUrl,
status: status,
time_taken: time_taken,
id: jobId,
};
origManu.bundleUrl = bundleUrl;
if (origManu.meta.callback_url) {
try {
let cbRes = await axios.post(origManu.meta.callback_url, origManu.meta);
console.log('On Render Completed Callback', cbRes.data);
clear();
} catch (e) {
console.log('Error in render callback', e.message);
}
} else {
console.log('No callback url provided, skipping on-render webhook');
}
}
const app = express();
function clear() {
try {
const foldersToPreserve = ['assets', 'mp3'];
Utils.clearFolder(join(__dirname, './public'), foldersToPreserve);
// Utils.clearFolder(join(__dirname, "./out"))
Utils.clearFolder(join(__dirname, './uploads'));
} catch (e) {}
}
app.use(RenderRouter);
app.all('/clear', async (req, res) => {
clear();
res.send('Cache Cleared');
});
app.all('/config', (req, res) => {
res.send({
transitions: JSON.parse(
readFileSync('public/assets/transitions.json').toString()
),
});
});
var currentRenderJobId = undefined;
let waiter = new Promise((resolve) => {
resolve();
});
app.all('/busy', async (req, res) => {
res.send({
job_id: currentRenderJobId,
busy: currentRenderJobId != undefined,
});
});
let audit = Auditlog.get();
const uploader = new FileUploader('oracle', OracleStorage.semibit_media);
app.all('/render', async (req, res) => {
let event = audit.event('render');
console.log('waiting to process render request', req.originalUrl);
await waiter;
console.log('initializing render', req.originalUrl);
let resolver = () => {
currentRenderJobId = undefined;
};
let timeout = setTimeout(() => {
Utils.log(req, 'Renderer freed after timeout');
resolver();
}, 1 * 60 * 60 * 1000);
waiter = new Promise((resolve) => {
resolver = () => {
clearTimeout(timeout);
currentRenderJobId = undefined;
resolve();
};
});
try {
clear();
const perf = new PerformanceRecorder();
perf.reset();
let bundleUrl = req.query.fileUrl;
let skipRender = req.query.skipRender;
let fileName = decodeURIComponent(Utils.getFileName(bundleUrl));
let filePath = req.query.filePath || join(__dirname, `public/${fileName}`);
if (bundleUrl) {
console.log('Downloading remote asset bundle from', bundleUrl);
try {
await Utils.downloadFile(bundleUrl, filePath, true);
} catch (e) {
resolver();
return res.status(500).send({
status: 'FAILED',
message: 'Unable to download bundle.' + e.message,
});
}
console.log('Downloaded remote asset bundle', 'to', filePath);
}
if (existsSync(filePath)) {
await extract(filePath);
let manuFile = join(__dirname, `public/original_manuscript.json`);
if (!existsSync) {
resolver();
res.status(400);
return res.send({
message:
'The asset bundle dosent contain a original_manuscript.json at root',
});
}
let manuObj;
let manuStr;
try {
const manuData = await new Promise((resolve, reject) => {
fs.readFile(manuFile, 'utf-8', function (err, data) {
if (err) return reject(err);
resolve(data);
});
});
manuStr = manuData.toString();
manuObj = JSON.parse(manuStr);
} catch (e) {
resolver();
console.log('error reading bundle');
console.log(e);
return res.status(500).send(manuStr);
}
currentRenderJobId = manuObj.id;
let jobID = manuObj.id;
modifyFiles(manuObj, manuFile);
console.log('Callback', manuObj?.meta.callback_url);
if (skipRender != '1') {
Utils.clearFolder(join(__dirname, 'out'));
res.writeHead(200, jobID, {
job_id: jobID,
'content-type': 'application/json',
});
let cb = async (result, cachedUrl) => {
event.commit(result != 0 ? 'FAILED' : 'COMPLETED');
if (result != 0) {
resolver();
res.write(
JSON.stringify({
status: 'FAILED',
job_id: jobID,
url: undefined,
time_taken: perf.elapsedString(),
})
);
res.end();
return;
}
perf.end();
let uploadResult;
console.log('Took', perf.elapsedString(), 'to render');
if (cachedUrl) {
uploadResult = {
url: cachedUrl,
};
} else {
let outFile = await generateOutputBundle(jobID);
uploadResult = await uploader.upload(outFile);
}
sendToObserver(jobID, uploadResult.url);
clear();
res.write(
JSON.stringify({
status: result,
job_id: jobID,
url: uploadResult.url,
time_taken: perf.elapsedString(),
})
);
res.end();
resolver();
};
if (req.query.async == 1) {
cb = async (result, cachedUrl) => {
event.commit(result != 0 ? 'FAILED' : 'COMPLETED');
if (result != 0) {
notify(
jobID,
manuObj,
undefined,
bundleUrl,
'FAILED',
perf.elapsedString()
);
resolver();
return;
}
console.log('Took', perf.elapsedString(), 'to render');
let uploadResult;
if (cachedUrl) {
uploadResult = {
url: cachedUrl,
};
} else {
let outFile = await generateOutputBundle(jobID);
uploadResult = await uploader.upload(outFile);
}
sendToObserver(jobID, uploadResult.url);
await notify(
jobID,
manuObj,
uploadResult.url,
bundleUrl,
'COMPLETED',
perf.elapsedString()
);
resolver();
};
res.write(
JSON.stringify({
status: 'SCHEDULED',
message: 'render started',
job_id: jobID,
callback_url: manuObj.meta.callback_url,
})
);
res.end();
}
// check if already processed
let globalDisableCache = await Utils.getKeyAsync('disable_cache');
if (req.query.disable_cache != '1' && globalDisableCache != '1') {
let oZipName = jobID + '.zip';
let uploadedFileUrl = uploader.targetUploadUrl(oZipName);
let opZip = path.join(__dirname, 'out/' + oZipName);
if (fs.existsSync(opZip)) {
console.log('rendered output already exists: ' + opZip);
cb(0);
resolver();
return;
}
try {
let resp = await axios.head(uploadedFileUrl);
if (resp.status == 200) {
console.log(
'rendered output already uploaded: ' + uploadedFileUrl
);
return cb(0, uploadedFileUrl);
}
} catch (e) {
console.log('error checking for upload cache at ', uploadedFileUrl);
}
} else {
Utils.log(req, `Render Caching disabled. Performing full render`);
}
let npmScript = req.query.media_type == 'image' ? 'render-still' : 'render-build';
if (manuObj.meta.posterImage) {
doRenderPoster(jobID, () => {
doRender(jobID, manuObj.meta.renderComposition, cb, req.query.media_type);
});
} else {
doRender(jobID, manuObj.meta.renderComposition, cb, req.query.media_type);
}
} else {
notify(
jobID,
manuObj,
undefined,
bundleUrl,
'SKIPPED',
perf.elapsedString()
);
console.log('Skipping render');
resolver();
res.send({
status: 'COMPLETED',
message: 'render skipped',
job_id: jobID,
});
}
} else {
resolver();
res.status(400);
return res.send({message: 'Unable to locate asset bundle'});
}
} catch (e) {
resolver();
res.status(500).send({
message: e.message,
});
}
});
app.use('/public', express.static(join(__dirname, 'public')));
app.use('/out', express.static(join(__dirname, 'out')));
const uploadDir = join(__dirname, 'uploads');
const oracleAPI = OracleStorage.semibit_media.url;
if (!existsSync(uploadDir)) {
try {
mkdirSync(uploadDir);
} catch (e) {
console.log('Error in making uploads dir', e.message);
}
}
app.post('/upload', async (req, res) => {
try {
const file = req.files.file;
const uploadUrl = oracleAPI + encodeURIComponent(file.name);
const readStream = createReadStream(file.path);
const writeStream = createWriteStream(join(uploadDir, file.name));
readStream.pipe(writeStream);
await new Promise((resolve) => {
writeStream.on('finish', resolve);
});
const fileData = readFileSync(join(uploadDir, file.name));
await axios.put(uploadUrl, fileData);
res.send({
message: 'File upload success',
size: file.length / (1024 * 1024) + 'mb',
url: uploadUrl,
});
} catch (error) {
console.error('Error uploading file:', error);
res.status(500).send({message: 'Error uploading file'});
}
});
function sendToObserver(jobId, data) {
if (app.observer) {
app.observer(jobId, {
jod_id: jobId,
message_type: 'info',
message: data,
});
}
}
var curVideoChildProc;
var curPosterChildProc;
app.use('/kill', (req, res) => {
try {
if (curPosterChildProc) {
kill(curPosterChildProc.pid, 'SIGKILL');
}
} catch (e) {
console.log('Error while killing poster proc', e.message);
}
try {
if (curVideoChildProc) {
kill(curVideoChildProc.pid, 'SIGKILL');
}
} catch (e) {
console.log('Error while killing video proc', e.message);
}
res.send({
message: 'killed',
});
});
function doRender(jobId, composition, statusCb, npmScript = 'render-build') {
npmScript = 'render-build';
const renderComposition = composition || 'SemibitComposition';
let cmd = `npm run ${npmScript} -- ${renderComposition} out/video.mp4`;
const childProcess = exec(cmd);
curVideoChildProc = childProcess;
console.log('Starting video render. ' + cmd);
let updateCounter = 0;
childProcess.stdout.on('data', (data) => {
sendToObserver(jobId, data);
if (!process.env.is_pm2) console.log(data.toString());
if (updateCounter++ % 100 == 0 || updateCounter < 5) {
console.log(data.split('\n')[0]);
}
});
childProcess.stderr.on('data', (data) => {
sendToObserver(jobId, data);
console.error(data.toString());
});
// Listen for the completion of the child process
childProcess.on('close', (code) => {
console.log('Render video finished');
sendToObserver(jobId, code === 0 ? 'completed' : 'failed');
if (statusCb) {
statusCb(code);
}
if (code === 0) {
console.log(`'${npmScript}' completed successfully.`);
} else {
console.error(`'${npmScript}' failed with code ${code}.`);
}
});
}
function doRenderPoster(jobId, statusCb) {
const npmScript = 'render-still';
const childProcess = exec(`npm run ${npmScript} -- out/poster.jpg`);
curPosterChildProc = childProcess;
console.log('Starting poster render');
childProcess.stdout.on('data', (data) => {
sendToObserver(jobId, data);
if (!process.env.is_pm2) console.log(data.toString());
});
childProcess.stderr.on('data', (data) => {
sendToObserver(jobId, data);
console.error(data.toString());
});
// Listen for the completion of the child process
childProcess.on('close', (code) => {
console.log('Render poster finished');
sendToObserver(jobId, code === 0 ? 'completed' : 'failed');
if (statusCb) {
statusCb(code);
}
if (code === 0) {
console.log(`'${npmScript}' completed successfully.`);
} else {
console.error(`'${npmScript}' failed with code ${code}.`);
}
});
}
let indexFile = join(__dirname, 'index.html');
// app.get('/execute', (req, res) => {
// const command = decodeURIComponent(req.query.cmd);
// console.log(command);
// exec(command, (error, stdout, stderr) => {
// if (error) {
// console.error(`Error executing command: ${error.message}`);
// res.status(500).json({error: error.message});
// return;
// }
// if (stderr) {
// console.error(`Command stderr: ${stderr}`);
// }
// console.log(stdout);
// res.send(stdout);
// });
// });
app.get('/', (req, res) => {
res.sendFile(indexFile);
});
writeFileSync(
indexFile,
readFileSync(indexFile).toString().replace('___ORACLE_API_BASE___', oracleAPI)
);
export default app;
// doRender()
if (process.env.MODIFY_FILES) {
let filePath = 'public/@export_bc064e55b5.zip';
extract(filePath).then(() => {
let manuFile = join(__dirname, 'public/original_manuscript.json');
let manuObj = JSON.parse(readFileSync(manuFile).toString());
modifyFiles(manuObj, manuFile);
console.log('Sequence Generated');
});
}