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