Spaces:
Running
Running
| 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; | |