remote-rdr / routes.js
shiveshnavin's picture
Add hooks
b1a7870
import { Router } from 'express';
import Bottleneck from 'bottleneck';
import {
clear,
doRender,
explodeUrl,
generateOutputBundle,
getNpmScript,
listOutputFiles,
} from './renderer.js';
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import pkg from 'common-utils';
const { Utils, PerformanceRecorder, FileUploader, Vault } = pkg;
import bodyParser from 'body-parser';
import { existsSync } from 'fs';
import { applyPluginsPostrender, applyPluginsPrerender } from './server-plugins/apply.js';
import { startChildProcess } from './proxy-renderer.js';
import { CaptionRenderer } from './utils/CaptionRender.js';
import { AvatarRenderer } from './utils/AvatarRender.js';
const RenderRouter = Router();
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const limiter = new Bottleneck({
maxConcurrent: 1,
});
RenderRouter.use(bodyParser.json());
RenderRouter.use(bodyParser.urlencoded());
const renderSyncRequestStatuses = new Map();
class RenderController {
stop() {
throw new Error('Render not stoppable !');
}
}
RenderRouter.get('/api/jobs', async (req, res) => {
const jobIds = Array.from(renderSyncRequestStatuses.keys());
const jobStatuses = jobIds.map((jobId) => {
const jobStatus = renderSyncRequestStatuses.get(jobId);
return {
jobId: jobId,
status: jobStatus.statusMessage,
...jobStatus.result
};
});
return res.status(200).json(jobStatuses);
});
RenderRouter.get('/api/jobs/:jobId/stop', async (req, res) => {
const jobId = req.params.jobId;
if (!jobId) {
return res.status(400).json({ message: 'Missing jobId in request body.' });
}
if (jobId == 'all') {
for (const [jobId, jobStatus] of renderSyncRequestStatuses.entries()) {
if (jobStatus.controller && jobStatus.controller.stop) {
try {
jobStatus.controller.stopped = true;
jobStatus.controller.stop();
} catch (e) {
console.error(`Failed to stop job ${jobId}: ${e.message}`);
}
}
renderSyncRequestStatuses.delete(jobId);
}
return res.status(200).json({ message: 'All jobs stopped successfully.' });
}
const jobStatus = renderSyncRequestStatuses.get(jobId);
if (!jobStatus) {
return res.status(404).json({ message: `Job with ID ${jobId} not found.` });
}
if (jobStatus.controller && jobStatus.controller.stop) {
try {
jobStatus.controller.stopped = true;
jobStatus.controller.stop();
res
.status(200)
.json({ message: `Job ${jobId} stopped successfully.` });
} catch (e) {
res
.status(400)
.json({ message: `Job ${jobId} stoping failed. ${e.message}` });
}
} else {
res
.status(400)
.json({ message: `Job ${jobId} is not currently running or cannot be stopped.` });
}
renderSyncRequestStatuses.delete(jobId);
});
RenderRouter.get('/api/jobs/:jobId', async (req, res) => {
const jobId = req.params.jobId;
const jobStatus = renderSyncRequestStatuses.get(jobId);
if (jobStatus) {
return res.status(200).json({
message: jobStatus.message,
statusMessage: jobStatus.statusMessage,
...jobStatus.result
});
} else {
return res.status(404).json({ message: `Job with ID ${jobId} not found.` });
}
});
RenderRouter.post('/api/v1/start-preview', async (req, res) => {
startChildProcess()
res.status(200).json({ message: 'Starting...' });
});
RenderRouter.post('/api/render-sync', async (req, res) => {
const jobId = req.body.jobId || Utils.generateUID(req.body.fileUrl);
// delete all jobs that finished more than 24 hours ago
const now = Date.now();
for (const [jobId, jobStatus] of renderSyncRequestStatuses.entries()) {
if (jobStatus.finished && (now - jobStatus.finished) > 24 * 60 * 60 * 1000) {
renderSyncRequestStatuses.delete(jobId);
}
}
const controller = new RenderController();
if (renderSyncRequestStatuses.has(jobId) && !req.body.force && !req.query.force &&
renderSyncRequestStatuses.get(jobId).statusMessage == 'IN_PROGRESS'
) {
let job = renderSyncRequestStatuses.get(jobId);
return res.status(202).json({
jobId,
message: 'Job already in progress or queued.',
...job.result
});
}
let fileUrl = req.body.fileUrl;
let targetUrl = req.body.targetUrl;
let skipClear = req.body.skip_clear;
let skipRender = req.body.skip_render;
let zip = req.body.zip ?? true;
// {name,...options}[]
let plugins = req.body.plugins || [];
if (!fileUrl) {
return res.status(400).send({
message:
'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` ',
});
}
if (!targetUrl) {
return res.status(400).send({
message:
'Missing `targetUrl` in body. The result will be uploaded to `targetUrl`',
});
}
// make sure only 1 request is being processed at a time
// set headers appropriately to hint that timeout must be large
res.setHeader('X-Job-Id', jobId);
res.setTimeout(0);
res.setHeader('Connection', 'keep-alive');
const perf = new PerformanceRecorder();
let logs = [];
try {
const run = async () => {
const dir = path.join(__dirname, 'public')
const zipFile = path.join(dir, `exported-${jobId}.zip`)
if (!existsSync(zipFile) || req.body.force || req.query.force) {
if (!skipClear) {
clear();
}
await explodeUrl(fileUrl, jobId, dir, zipFile);
}
else {
console.log(`Job ${jobId} assets already exploded to public. Not downloading again.`)
}
let renderMethod = req.body.method || req.query.method || 'cli';
const originalManuscriptPath = path.join(__dirname, `public`, `original_manuscript.json`)
let originalManuscript = Utils.readFileToObject(originalManuscriptPath);
await applyPluginsPrerender([...plugins, { name: 'flatten-paths' }], originalManuscript, originalManuscriptPath, jobId)
renderMethod = originalManuscript.meta?.generationConfig?.extras?.renderMethod || renderMethod
if (!skipRender) {
if (renderMethod == 'media-with-caption-only') {
let renderer = new AvatarRenderer();
renderer.validateCaption(originalManuscript);
await renderer.doRender(jobId,
originalManuscript,
(log) => {
logs.push(log);
},
getNpmScript(req.query.media_type),
req.body.caption_only,
req.body.caption_only,
controller);
}
else if (renderMethod == 'caption-only') {
let renderer = new CaptionRenderer();
renderer.validateCaption(originalManuscript);
await renderer.doRender(jobId,
originalManuscript,
(log) => {
logs.push(log);
},
getNpmScript(req.query.media_type),
req.body.caption_only,
req.body.caption_only,
controller);
}
else if (renderMethod == 'cli' || req.query.media_type == 'image') {
await doRender(
jobId,
originalManuscript,
(jobId, log) => {
logs.push(log);
},
getNpmScript(req.query.media_type),
undefined,
undefined,
controller
);
}
else if (renderMethod === 'all') {
let errorMsg = ''
let renderComplete = false;
try {
console.log('Render start via CLI')
await doRender(
jobId,
originalManuscript,
(jobId, log) => {
logs.push(log);
},
getNpmScript(req.query.media_type),
undefined,
undefined,
controller
)
renderComplete = true;
} catch (e) {
console.log('Render via CLI failed', e.message)
errorMsg = "CLI: " + e.message
if (controller.stopped) {
throw new Error('Failed to render. Aborted. ' + errorMsg)
}
}
if (!renderComplete) {
try {
console.log('Render start via Proxy')
await doRender(
jobId,
originalManuscript,
(jobId, log) => {
logs.push(log);
},
getNpmScript(req.query.media_type),
undefined,
{
...req.body.proxy,
"framesPerChunk": req.body.proxy?.framesPerChunk || 99999
},
controller
)
renderComplete = true;
} catch (e) {
console.log('Render via Proxy failed', e.message)
errorMsg = errorMsg + "\n\nProxy: " + e.message
if (controller.stopped) {
throw new Error('Failed to render. Aborted. ' + errorMsg)
}
}
}
if (!renderComplete) {
try {
console.log('Render start via Proxy Chunked')
await doRender(
jobId,
originalManuscript,
(jobId, log) => {
logs.push(log);
},
getNpmScript(req.query.media_type),
undefined,
{
...req.body.proxy
},
controller
)
renderComplete = true;
} catch (e) {
console.log('Render via Proxy Chunked failed', e.message)
errorMsg = errorMsg + "\n\nProxyChunked: " + e.message
if (controller.stopped) {
throw new Error('Failed to render. Aborted. ' + errorMsg)
}
}
}
if (!renderComplete) {
try {
console.log('Render start via SSR')
await doRender(
jobId,
originalManuscript,
(jobId, log) => {
logs.push(log);
},
getNpmScript(req.query.media_type),
req.body.ssr || {},
undefined,
controller
)
renderComplete = true;
} catch (e) {
console.log('Render via SSR failed', e.message)
errorMsg = errorMsg + "\n\nSSR: " + e.message
if (controller.stopped) {
throw new Error('Failed to render. Aborted. ' + errorMsg)
}
}
}
if (!renderComplete) {
throw new Error('All render methods exhausted. Failed to render. ' + errorMsg)
}
}
else if (renderMethod === 'ssr') {
await doRender(
jobId,
originalManuscript,
(jobId, log) => {
logs.push(log);
},
getNpmScript(req.query.media_type),
req.body.ssr || {},
undefined,
controller
);
} else if (renderMethod === 'proxy') {
await doRender(
jobId,
originalManuscript,
(jobId, log) => {
logs.push(log);
},
getNpmScript(req.query.media_type),
undefined,
{
...req.body.proxy,
"framesPerChunk": req.body.proxy?.framesPerChunk || 99999
},
controller
);
} else if (renderMethod === 'proxy-chunked') {
await doRender(
jobId,
originalManuscript,
(jobId, log) => {
logs.push(log);
},
getNpmScript(req.query.media_type),
undefined,
req.body.proxy || {},
controller
);
}
}
const uploader = new FileUploader('oracle', {
url: targetUrl + jobId + '/',
});
uploader.creds.url = uploader.creds.url;
let outFiles = await listOutputFiles(jobId);
await applyPluginsPostrender([...plugins, { name: 'hook' }], originalManuscript, originalManuscriptPath, jobId, outFiles)
outFiles = await listOutputFiles(jobId);
console.log('Render complete, took', perf.elapsedString(), 'uploading... to target folder: ' + targetUrl + jobId + '/')
console.log('Files to upload: ', outFiles)
if (zip) {
let outFile = await generateOutputBundle(jobId, outFiles);
let uploadResult = await uploader.upload(outFile);
if (!skipClear) {
clear();
}
return {
response_time: perf.elapsed(),
urls: [uploadResult.url],
url: uploadResult.url,
original_manuscript: originalManuscript,
};
} else {
let urls = await Promise.all(
outFiles.map((file) => {
return uploader.upload(file);
})
).then((results) => {
return results.map((result) => result.url);
});
let url =
urls.find((u) => u.includes('.mp4')) ||
urls.find((u) => u.includes('.webm')) ||
urls.find((u) => u.includes('.mov')) ||
urls.find((u) => u.includes('.avi')) ||
urls.find((u) => u.includes('.jpg')) ||
urls.find((u) => u.includes('.jpeg')) ||
urls.find((u) => u.includes('.png'));
return {
response_time: perf.elapsed(),
urls: urls,
url,
original_manuscript: originalManuscript,
};
}
}
renderSyncRequestStatuses.set(jobId,
{
id: jobId,
controller,
statusMessage: 'IN_PROGRESS',
result: undefined
}
);
const result = await limiter.schedule(run);
let job = renderSyncRequestStatuses.get(jobId);
if (job) {
delete job.controller
renderSyncRequestStatuses.set(jobId,
{
finished: Date.now(),
id: jobId,
response_time: perf.elapsedSeconds(),
statusMessage: 'COMPLETED',
result: { jobId, success: true, ...result }
}
);
}
res.status(200).json({ jobId, success: true, response_time: perf.elapsedSeconds(), ...result });
} catch (err) {
console.error(`Render job ${jobId} failed:`, err.message, err);
let job = renderSyncRequestStatuses.get(jobId);
if (job) {
delete job.controller
renderSyncRequestStatuses.set(jobId,
{
finished: Date.now(),
id: jobId,
message: err.message + ' Details : ' + logs.join('\n'),
response_time: perf.elapsedSeconds(),
statusMessage: 'FAILED',
result: { jobId, success: false }
}
);
}
res.status(500).json({
jobId,
success: false,
message: err.message + ' Details : ' + logs.join('\n'),
});
}
});
export default RenderRouter;